diff --git a/.github/scripts/validate_templates.py b/.github/scripts/validate_templates.py index 8629434..82a96e9 100755 --- a/.github/scripts/validate_templates.py +++ b/.github/scripts/validate_templates.py @@ -17,7 +17,7 @@ def validate_json_file(templates_json_path: Path) -> dict: """Validate templates.json is valid JSON and return parsed data.""" try: - with open(templates_json_path, 'r', encoding='utf-8') as f: + with open(templates_json_path, "r", encoding="utf-8") as f: data = json.load(f) print(f"✓ {templates_json_path} is valid JSON") return data @@ -34,35 +34,35 @@ def validate_template_entry(name: str, config: dict, repo_root: Path) -> list[st errors = [] # Check required fields - if 'file' not in config: + if "file" not in config: errors.append(f"Template '{name}' missing required field 'file'") - if 'description' not in config: + if "description" not in config: errors.append(f"Template '{name}' missing required field 'description'") - if 'file' not in config: + if "file" not in config: return errors # Can't continue without file field # Check main file exists - main_file = repo_root / config['file'] + main_file = repo_root / config["file"] if not main_file.exists(): errors.append(f"Template '{name}': main file '{config['file']}' does not exist") # Validate featured field if present (optional) - if 'featured' in config: - if not isinstance(config['featured'], bool): + if "featured" in config: + if not isinstance(config["featured"], bool): errors.append(f"Template '{name}': 'featured' field must be a boolean") # Validate author field if present (optional) - if 'author' in config: - author = config['author'] + if "author" in config: + author = config["author"] if not isinstance(author, dict): errors.append(f"Template '{name}': 'author' field must be an object") else: # All author fields are optional, but validate types if present optional_author_fields = { - 'name': str, - 'github_profile': str, - 'last_modified_date': str + "name": str, + "github_profile": str, + "last_modified_date": str, } for field, expected_type in optional_author_fields.items(): if field in author and not isinstance(author[field], expected_type): @@ -71,31 +71,34 @@ def validate_template_entry(name: str, config: dict, repo_root: Path) -> list[st ) # Check files array if present - if 'files' in config: - for file_spec in config['files']: - if 'source' not in file_spec: + if "files" in config: + for file_spec in config["files"]: + if "source" not in file_spec: errors.append(f"Template '{name}': file entry missing 'source' field") continue - if 'dest' not in file_spec: + if "dest" not in file_spec: errors.append(f"Template '{name}': file entry missing 'dest' field") continue - source_path = repo_root / file_spec['source'] + source_path = repo_root / file_spec["source"] if not source_path.exists(): - errors.append(f"Template '{name}': source file '{file_spec['source']}' does not exist") + errors.append( + f"Template '{name}': source file '{file_spec['source']}' does not exist" + ) # For complex templates, check required files exist - is_complex = len(config['files']) > 1 + is_complex = len(config["files"]) > 1 if is_complex: - template_dir = Path(config['file']).parent - required_files = ['README.md', 'pyproject.toml.template', '.env.example.template'] + template_dir = Path(config["file"]).parent + required_files = [ + "README.md", + "pyproject.toml.template", + ".env.example.template", + ] for required in required_files: # Check if it's in the files array - found = any( - Path(f['source']).name == required - for f in config['files'] - ) + found = any(Path(f["source"]).name == required for f in config["files"]) if not found: errors.append( f"Template '{name}': complex template missing '{required}' in files array" @@ -106,7 +109,7 @@ def validate_template_entry(name: str, config: dict, repo_root: Path) -> list[st def main(): repo_root = Path(__file__).parent.parent.parent - templates_json = repo_root / 'templates.json' + templates_json = repo_root / "templates.json" print("Validating template registry...\n") @@ -126,7 +129,7 @@ def main(): print(f"✗ {error}") # Summary - print(f"\n{'='*60}") + print(f"\n{'=' * 60}") if all_errors: print(f"Validation failed with {len(all_errors)} error(s)") sys.exit(1) @@ -135,5 +138,5 @@ def main(): sys.exit(0) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/.github/scripts/verify_template_output.py b/.github/scripts/verify_template_output.py index d17f269..abc510d 100755 --- a/.github/scripts/verify_template_output.py +++ b/.github/scripts/verify_template_output.py @@ -12,18 +12,18 @@ def main(): repo_root = Path(__file__).parent.parent.parent - templates_json = repo_root / 'templates.json' + templates_json = repo_root / "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") + print("✓ templates.json is valid JSON") except json.JSONDecodeError as e: print(f"✗ templates.json is invalid JSON: {e}") sys.exit(1) except FileNotFoundError: - print(f"✗ templates.json not found") + print("✗ templates.json not found") sys.exit(1) errors = [] @@ -33,39 +33,44 @@ def main(): print(f"\nValidating template: {template_name}") # Check required fields - if 'file' not in config: + if "file" not in config: errors.append(f"✗ {template_name}: missing 'file' field") print(errors[-1]) continue - if 'description' not in config: + if "description" not in config: errors.append(f"✗ {template_name}: missing 'description' field") print(errors[-1]) # Check main file exists - main_file = repo_root / config['file'] + 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'): + 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}") + errors.append( + f"✗ {template_name}: Python compilation error in {config['file']}: {e}" + ) print(errors[-1]) else: 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 "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") + errors.append( + f"✗ {template_name}: file spec missing 'source' field" + ) print(errors[-1]) continue @@ -74,20 +79,23 @@ def main(): print(f" ✓ File exists: {source}") # Try to compile Python files - if source.endswith('.py'): + 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}") + 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) + print("\n" + "=" * 50) if errors: print(f"✗ Validation failed with {len(errors)} error(s)") for error in errors: @@ -98,5 +106,5 @@ def main(): sys.exit(0) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/agentmail/main.py b/agentmail/main.py index d174c9d..b3dd38e 100644 --- a/agentmail/main.py +++ b/agentmail/main.py @@ -11,7 +11,7 @@ load_dotenv() -from browser_use import Agent, Browser, ChatBrowserUse, models +from browser_use import Agent, Browser, ChatBrowserUse from email_tools import EmailTools TASK = """ diff --git a/scheduler/.env.example.template b/scheduler/.env.example.template new file mode 100644 index 0000000..bcf7178 --- /dev/null +++ b/scheduler/.env.example.template @@ -0,0 +1,21 @@ +# 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 + +# Scheduler Interval in minutes (optional - default: 5) +SCHEDULER_INTERVAL_MINUTES=5 + +# Directory containing agent scripts (optional - default: agents) +# All .py files in this directory will be automatically discovered and run +# Prefix files with _ to disable them (e.g., _disabled_script.py) +SCHEDULER_SCRIPTS_DIR=agents diff --git a/scheduler/README.md b/scheduler/README.md new file mode 100644 index 0000000..af30a60 --- /dev/null +++ b/scheduler/README.md @@ -0,0 +1,190 @@ +# 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 Settings +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) + +# Optional - Scheduler Settings +SCHEDULER_INTERVAL_MINUTES=5 # Interval between scheduled runs (default: 5) +SCHEDULER_SCRIPTS_DIR=agents # Directory containing agent scripts (default: agents) +``` + +## Usage + +### Run Once + +Run a single script once: +```bash +uv run agents/x.py # Run X.com agent +uv run agents/linkedin.py # Run LinkedIn agent +``` + +The example tasks: +- **agents/x.py**: Navigates to X.com and extracts the 5 newest tweets +- **agents/linkedin.py**: Navigates to LinkedIn and extracts the 5 newest posts + +Both require authentication via cloud profile. + +### Run on Schedule + +Run all agents in the `agents/` directory concurrently every 5 minutes (or custom interval): +```bash +uv run main.py +``` + +The scheduler will: +- **Auto-discover** all `.py` files in the `agents/` directory +- Execute all discovered scripts simultaneously as subprocesses +- Run at regular intervals (default: 5 minutes) +- Display the final output from each agent after completion +- Log each execution with timestamps +- Handle errors gracefully and continue running + +**How auto-discovery works:** + +The scheduler automatically finds and runs ALL Python files in the `agents/` directory. No configuration needed! + +**To add a new agent:** +1. Create your script in `agents/` (e.g., `agents/instagram.py`) +2. That's it! It will run automatically on the next cycle + +**To disable an agent temporarily:** + +Prefix the filename with `_` (underscore): +```bash +mv agents/linkedin.py agents/_linkedin.py # Disabled +mv agents/_linkedin.py agents/linkedin.py # Re-enabled +``` + +**Customize the scripts directory:** + +Edit `SCHEDULER_SCRIPTS_DIR` in your `.env` file: +```bash +SCHEDULER_SCRIPTS_DIR=my-agents # Use a different directory +``` + +**Customize the interval:** + +Update `SCHEDULER_INTERVAL_MINUTES` in your `.env` file: +```bash +SCHEDULER_INTERVAL_MINUTES=10 # Run every 10 minutes instead of 5 +``` + +Stop the scheduler by pressing `Ctrl+C`. + +## 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/scheduler/agents/gmail.py b/scheduler/agents/gmail.py new file mode 100644 index 0000000..15574e2 --- /dev/null +++ b/scheduler/agents/gmail.py @@ -0,0 +1,91 @@ +""" +Browser-use agent with cloud sandbox configuration for Gmail. + +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 Gmail and retrieve recent emails. + +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 os +import json +from pathlib import Path + +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 https://mail.google.com/mail/u/0/#inbox and extract " + "the top 5 most recent emails from my inbox including sender name, " + "subject, preview text, and timestamp. " + "Provide a brief summary of what these emails are about." + ) + + agent = Agent( + browser=browser, + task=task, + llm=llm, + ) + + history = await agent.run() + + # Extract final result BEFORE returning (while still in sandbox) + return {"result": history.final_result() or "No result from agent"} + + +async def run(): + """Wrapper function that calls main and writes results to file.""" + output_file = Path("/tmp") / "gmail.py_result.json" + + try: + # Call the sandbox-decorated function (returns dict from sandbox) + result = await main() + + result_data = { + "script": "gmail.py", + "success": bool(result and result.get("result")), + "result": result.get("result") + if result + else "No result returned from agent", + } + + with open(output_file, "w") as f: + json.dump(result_data, f, indent=2) + + except Exception as e: + # Write error result + import traceback + + error_data = { + "script": "gmail.py", + "success": False, + "result": f"Error: {str(e)}\n\nTraceback:\n{traceback.format_exc()}", + } + with open(output_file, "w") as f: + json.dump(error_data, f, indent=2) + + +if __name__ == "__main__": + import asyncio + + asyncio.run(run()) diff --git a/scheduler/agents/x.py b/scheduler/agents/x.py new file mode 100644 index 0000000..07d1936 --- /dev/null +++ b/scheduler/agents/x.py @@ -0,0 +1,92 @@ +""" +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 os +import json +from pathlib import Path + +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 https://x.com/home, look at the following tab, and extract " + "the first 5 newest tweets with their metadata including author name, " + "author handle, content, timestamp, likes, and reposts. " + "Provide a brief summary of what these tweets are about." + ) + + agent = Agent( + browser=browser, + task=task, + llm=llm, + ) + + history = await agent.run() + + # Extract final result BEFORE returning (while still in sandbox) + return {"result": history.final_result() or "No result from agent"} + + +async def run(): + """Wrapper function that calls main and writes results to file.""" + output_file = Path("/tmp") / "x.py_result.json" + + try: + # Call the sandbox-decorated function (returns dict from sandbox) + result = await main() + + result_data = { + "script": "x.py", + "success": bool(result and result.get("result")), + "result": result.get("result") + if result + else "No result returned from agent", + } + + with open(output_file, "w") as f: + json.dump(result_data, f, indent=2) + + except Exception as e: + # Write error result + import traceback + + error_data = { + "script": "x.py", + "success": False, + "result": f"Error: {str(e)}\n\nTraceback:\n{traceback.format_exc()}", + } + with open(output_file, "w") as f: + json.dump(error_data, f, indent=2) + + +if __name__ == "__main__": + import asyncio + + asyncio.run(run()) diff --git a/scheduler/main.py b/scheduler/main.py new file mode 100644 index 0000000..91a3d2f --- /dev/null +++ b/scheduler/main.py @@ -0,0 +1,268 @@ +""" +Scheduler that executes browser-use agents every N minutes. + +This script automatically discovers and runs all Python scripts in the agents +directory as concurrent subprocesses on a configurable interval, logging each +execution with timestamps and displaying final results. + +Configuration: +- SCHEDULER_INTERVAL_MINUTES: Time between executions (default: 5) +- SCHEDULER_SCRIPTS_DIR: Directory containing the scripts (default: agents) +- All other configuration is inherited from .env (API keys, profiles, etc.) + +Auto-discovery: +- All .py files in the scripts directory are automatically discovered and run +- Files starting with _ or . are ignored (use to disable scripts) +""" + +import asyncio +import os +import sys +from datetime import datetime +from pathlib import Path +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +# Configuration +INTERVAL_MINUTES = int(os.getenv("SCHEDULER_INTERVAL_MINUTES", 5)) +INTERVAL_SECONDS = INTERVAL_MINUTES * 60 + +# Scripts directory +SCRIPTS_DIR = os.getenv("SCHEDULER_SCRIPTS_DIR", "agents") + + +def log_message(message: str, level: str = "INFO"): + """Log a message with timestamp.""" + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + print(f"[{timestamp}] [{level}] {message}", flush=True) + + +async def run_script_subprocess(script_path: Path) -> tuple[str, bool, str]: + """ + Run a Python script as a subprocess and read its result from a JSON file. + + Returns: + tuple: (script_name, success, result_content) + """ + import json + + script_name = script_path.name + log_message(f"Starting {script_name}...", "INFO") + + # Define where the script will write its result + result_file = Path("/tmp") / f"{script_name}_result.json" + + # Clean up any existing result file + if result_file.exists(): + result_file.unlink() + + try: + # Run the script as a subprocess (output goes to /dev/null, we read the file) + process = await asyncio.create_subprocess_exec( + sys.executable, + str(script_path), + stdout=asyncio.subprocess.DEVNULL, + stderr=asyncio.subprocess.DEVNULL, + cwd=script_path.parent, + ) + + # Wait for completion + returncode = await process.wait() + + # Read the result from the JSON file + if result_file.exists(): + with open(result_file, "r") as f: + result_data = json.load(f) + + success = result_data.get("success", False) + result_content = result_data.get("result", "No result") + + if success: + log_message(f"{script_name} completed successfully", "SUCCESS") + else: + log_message(f"{script_name} completed with no result", "WARNING") + + return (script_name, success, result_content) + else: + # File doesn't exist - script failed or didn't write output + log_message( + f"{script_name} failed - no result file found (exit code: {returncode})", + "ERROR", + ) + return ( + script_name, + False, + f"Script exited with code {returncode}, no result file generated", + ) + + except Exception as e: + log_message(f"{script_name} execution failed: {str(e)}", "ERROR") + import traceback + + error_trace = traceback.format_exc() + log_message(error_trace, "ERROR") + return (script_name, False, str(e)) + + +async def run_all_scripts(scripts: list[Path]) -> dict: + """ + Run all scripts concurrently as subprocesses and display their outputs. + + Returns: + dict: Summary of execution results + """ + log_message(f"Running {len(scripts)} script(s) concurrently...") + + start_time = datetime.now() + + # Run all scripts concurrently + tasks = [run_script_subprocess(script) for script in scripts] + results = await asyncio.gather(*tasks, return_exceptions=True) + + end_time = datetime.now() + duration = (end_time - start_time).total_seconds() + + # Display outputs from each script + print("\n" + "=" * 80) + log_message("Agent Results:", "INFO") + print("=" * 80 + "\n") + + for result in results: + if isinstance(result, tuple): + script_name, success, output = result + + print(f"{'=' * 80}") + print(f" Script: {script_name}") + print(f" Status: {'✓ SUCCESS' if success else '✗ FAILED'}") + print(f"{'=' * 80}") + + if output: + print(output) + else: + print("(no output)") + + print() + + print("=" * 80) + + # Summarize results + successful = sum(1 for r in results if isinstance(r, tuple) and r[1]) + failed = len(results) - successful + + summary = { + "total": len(scripts), + "successful": successful, + "failed": failed, + "duration": duration, + "results": results, + } + + log_message( + f"Batch complete: {successful}/{len(scripts)} successful, " + f"{failed} failed, duration: {duration:.2f}s", + "SUCCESS" if failed == 0 else "WARNING", + ) + + return summary + + +def discover_scripts() -> list[Path]: + """ + Automatically discover all Python scripts in the scripts directory. + + Ignores files starting with _ or . (for disabled/hidden scripts). + + Returns: + list[Path]: List of discovered script paths + """ + base_dir = Path(__file__).parent + scripts_dir = base_dir / SCRIPTS_DIR + + # Check if scripts directory exists + if not scripts_dir.exists(): + log_message(f"Error: Scripts directory not found: {scripts_dir}", "ERROR") + log_message(f"Please create the directory: mkdir {SCRIPTS_DIR}", "ERROR") + return [] + + # Discover all .py files + discovered = [] + for script_path in scripts_dir.glob("*.py"): + # Ignore files starting with _ or . + if script_path.name.startswith(("_", ".")): + log_message(f"Skipping disabled script: {script_path.name}", "INFO") + continue + + discovered.append(script_path) + + # Sort for consistent ordering + discovered.sort(key=lambda p: p.name) + + return discovered + + +async def scheduler_loop(): + """Main scheduler loop that runs all discovered scripts every INTERVAL_MINUTES.""" + log_message( + f"Scheduler started - will run scripts every {INTERVAL_MINUTES} minutes" + ) + log_message("Press Ctrl+C to stop") + + # Discover scripts in the directory + scripts = discover_scripts() + + if not scripts: + log_message("Error: No scripts found to run!", "ERROR") + return + + script_names = [s.name for s in scripts] + log_message(f"Discovered {len(scripts)} script(s): {', '.join(script_names)}") + + execution_count = 0 + total_successful = 0 + total_failed = 0 + + try: + while True: + execution_count += 1 + log_message("=" * 60) + log_message(f"Execution batch #{execution_count} starting...") + + # Run all scripts + summary = await run_all_scripts(scripts) + + total_successful += summary["successful"] + total_failed += summary["failed"] + + # Wait for the next interval + log_message(f"Next execution in {INTERVAL_MINUTES} minutes...") + log_message("=" * 60) + await asyncio.sleep(INTERVAL_SECONDS) + + except KeyboardInterrupt: + log_message("=" * 60) + log_message("Scheduler stopped by user", "INFO") + log_message(f"Total batches: {execution_count}") + log_message(f"Total successful runs: {total_successful}") + log_message(f"Total failed runs: {total_failed}") + log_message("=" * 60) + except Exception as e: + log_message(f"Scheduler error: {str(e)}", "ERROR") + raise + + +def main(): + """Entry point for the scheduler.""" + log_message("=" * 60) + log_message("Browser-Use Multi-Agent Scheduler") + log_message(f"Interval: {INTERVAL_MINUTES} minutes") + log_message(f"Scripts directory: {SCRIPTS_DIR}") + log_message("=" * 60) + + # Run the scheduler + asyncio.run(scheduler_loop()) + + +if __name__ == "__main__": + main() diff --git a/scheduler/pyproject.toml.template b/scheduler/pyproject.toml.template new file mode 100644 index 0000000..2de7131 --- /dev/null +++ b/scheduler/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 @ git+https://github.com/ShawnPana/browser-use.git@main", + "python-dotenv", +] diff --git a/templates.json b/templates.json index 970a2b4..a692ef1 100644 --- a/templates.json +++ b/templates.json @@ -29,6 +29,7 @@ "sandbox": { "file": "sandbox/main.py", "description": "Cloud browser automation with @sandbox decorator for persistent auth and proxy routing", + "featured": true, "files": [ { "source": "sandbox/main.py", @@ -159,7 +160,6 @@ "job-application": { "file": "job-application/main.py", "description": "Automated job application form submission with resume upload", - "featured": true, "files": [ { "source": "job-application/main.py", @@ -292,7 +292,6 @@ "llm-arena": { "file": "llm-arena/main.py", "description": "Compare different AI models side-by-side in LLM Arena", - "featured": true, "files": [ { "source": "llm-arena/main.py", @@ -474,5 +473,70 @@ "github_profile": "https://github.com/kalil0321", "last_modified_date": "2025-11-07" } + }, + "scheduler": { + "file": "scheduler/main.py", + "description": "Multi-agent scheduler that runs browser automation tasks concurrently on configurable intervals", + "featured": true, + "files": [ + { + "source": "scheduler/main.py", + "dest": "main.py" + }, + { + "source": "scheduler/agents/gmail.py", + "dest": "agents/gmail.py" + }, + { + "source": "scheduler/agents/x.py", + "dest": "agents/x.py" + }, + { + "source": "scheduler/pyproject.toml.template", + "dest": "pyproject.toml" + }, + { + "source": "scheduler/.env.example.template", + "dest": ".env.example" + }, + { + "source": "gitignore.template", + "dest": ".gitignore" + }, + { + "source": "scheduler/README.md", + "dest": "README.md" + } + ], + "next_steps": [ + { + "title": "Navigate to project directory", + "commands": ["cd {template}"] + }, + { + "title": "Set up your API key and cloud settings", + "commands": [ + "cp .env.example .env", + "# Edit .env and add your BROWSER_USE_API_KEY and optional cloud settings" + ], + "note": "(Get your API key at https://cloud.browser-use.com/new-api-key)" + }, + { + "title": "Install dependencies", + "commands": ["uv sync"] + }, + { + "title": "Run the scheduler", + "commands": ["uv run {output}"] + }, + { + "footer": "📖 See README.md for adding custom agents, configuration, and troubleshooting\n\n🔄 The scheduler will auto-discover all .py files in the agents/ directory" + } + ], + "author": { + "name": "Shawn Pana", + "github_profile": "https://github.com/ShawnPana", + "last_modified_date": "2025-11-14" + } } }