From faf8eb8435d7546b365247ff08e40ce175850d7a Mon Sep 17 00:00:00 2001 From: ShawnPana Date: Tue, 11 Nov 2025 20:53:27 -0800 Subject: [PATCH 1/7] Add lint workflow for template code quality --- .github/workflows/lint.yml | 45 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..8695f55 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,45 @@ +name: lint + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +jobs: + lint-syntax: + name: syntax-errors + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v5 + - name: Check syntax errors in template files + run: | + uv run ruff check --no-fix --select PLE \ + *_template.py \ + */main.py \ + */email_tools.py \ + */launch_chrome_debug.py \ + */app/*.py \ + --exclude test-env + + lint-format: + name: code-format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v5 + - name: Check code formatting in template files + run: | + uv run ruff format --check \ + *_template.py \ + */main.py \ + */email_tools.py \ + */launch_chrome_debug.py \ + */app/*.py \ + --exclude test-env From eb3dfe28be8c9fdb013656204276d1079f2f42bc Mon Sep 17 00:00:00 2001 From: ShawnPana Date: Tue, 11 Nov 2025 20:56:32 -0800 Subject: [PATCH 2/7] Add validation workflow for template registry --- .github/scripts/validate_templates.py | 116 ++++++++++++++++++++++++++ .github/workflows/validate.yml | 24 ++++++ 2 files changed, 140 insertions(+) create mode 100755 .github/scripts/validate_templates.py create mode 100644 .github/workflows/validate.yml diff --git a/.github/scripts/validate_templates.py b/.github/scripts/validate_templates.py new file mode 100755 index 0000000..2c2c63f --- /dev/null +++ b/.github/scripts/validate_templates.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +""" +Validate templates.json and template file structure. + +This script checks: +1. templates.json is valid JSON +2. All referenced files exist +3. Complex templates have required files +4. Schema is correct +""" + +import json +import sys +from pathlib import Path + + +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: + data = json.load(f) + print(f"✓ {templates_json_path} is valid JSON") + return data + except json.JSONDecodeError as e: + print(f"✗ {templates_json_path} is not valid JSON: {e}") + sys.exit(1) + except FileNotFoundError: + print(f"✗ {templates_json_path} not found") + sys.exit(1) + + +def validate_template_entry(name: str, config: dict, repo_root: Path) -> list[str]: + """Validate a single template entry. Returns list of errors.""" + errors = [] + + # Check required fields + if 'file' not in config: + errors.append(f"Template '{name}' missing required field 'file'") + if 'description' not in config: + errors.append(f"Template '{name}' missing required field 'description'") + + if 'file' not in config: + return errors # Can't continue without file field + + # Check main file exists + main_file = repo_root / config['file'] + if not main_file.exists(): + errors.append(f"Template '{name}': main file '{config['file']}' does not exist") + + # Check files array if present + 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: + errors.append(f"Template '{name}': file entry missing 'dest' field") + continue + + 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") + + # For complex templates, check required files exist + 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'] + + 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'] + ) + if not found: + errors.append( + f"Template '{name}': complex template missing '{required}' in files array" + ) + + return errors + + +def main(): + repo_root = Path(__file__).parent.parent.parent + templates_json = repo_root / 'templates.json' + + print("Validating template registry...\n") + + # Validate JSON + templates = validate_json_file(templates_json) + + # Validate each template entry + all_errors = [] + for name, config in templates.items(): + errors = validate_template_entry(name, config, repo_root) + all_errors.extend(errors) + + if not errors: + print(f"✓ Template '{name}' is valid") + else: + for error in errors: + print(f"✗ {error}") + + # Summary + print(f"\n{'='*60}") + if all_errors: + print(f"Validation failed with {len(all_errors)} error(s)") + sys.exit(1) + else: + print(f"✓ All {len(templates)} templates are valid!") + sys.exit(0) + + +if __name__ == '__main__': + main() diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..bda370d --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,24 @@ +name: validate + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +jobs: + validate-templates: + name: validate-template-registry + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Validate templates.json and template files + run: python .github/scripts/validate_templates.py From 3538a469c21ff6763efc2fa08ec0f44e721920d3 Mon Sep 17 00:00:00 2001 From: ShawnPana Date: Tue, 11 Nov 2025 20:57:31 -0800 Subject: [PATCH 3/7] Add template initialization testing workflow --- .github/scripts/verify_template_output.py | 72 +++++++++++++++++++++++ .github/workflows/test-templates.yml | 64 ++++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100755 .github/scripts/verify_template_output.py create mode 100644 .github/workflows/test-templates.yml diff --git a/.github/scripts/verify_template_output.py b/.github/scripts/verify_template_output.py new file mode 100755 index 0000000..e6359cd --- /dev/null +++ b/.github/scripts/verify_template_output.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +""" +Verify that template initialization created the expected files. + +Usage: python verify_template_output.py +""" + +import json +import sys +from pathlib import Path + + +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") + 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") + sys.exit(1) + + print(f"✓ Template directory created: {template_dir}") + + # 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) + + # For complex templates, check all expected files + errors = [] + for file_spec in config['files']: + dest = file_spec['dest'] + expected_file = template_dir / dest + + if expected_file.exists(): + print(f"✓ File created: {dest}") + else: + errors.append(f"✗ Missing file: {dest}") + print(errors[-1]) + + if errors: + print(f"\n✗ {len(errors)} file(s) missing") + sys.exit(1) + else: + print(f"\n✓ All {len(config['files'])} expected files created") + sys.exit(0) + + +if __name__ == '__main__': + main() diff --git a/.github/workflows/test-templates.yml b/.github/workflows/test-templates.yml new file mode 100644 index 0000000..bac165a --- /dev/null +++ b/.github/workflows/test-templates.yml @@ -0,0 +1,64 @@ +name: test-templates + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +on: + push: + branches: + - main + pull_request: + 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 + 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 + + - name: Verify files were created + run: | + python3 .github/scripts/verify_template_output.py ${{ matrix.template }} + + - name: Compile generated Python files + 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)" From 50a6a438ec534d483a6d51bf3fae7a42036b706e Mon Sep 17 00:00:00 2001 From: ShawnPana Date: Tue, 11 Nov 2025 21:14:58 -0800 Subject: [PATCH 4/7] Fix CI workflows: install ruff and correct output filename --- .github/workflows/lint.yml | 4 ++-- .github/workflows/test-templates.yml | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8695f55..7fce018 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -20,7 +20,7 @@ jobs: - uses: astral-sh/setup-uv@v5 - name: Check syntax errors in template files run: | - uv run ruff check --no-fix --select PLE \ + uvx ruff check --no-fix --select PLE \ *_template.py \ */main.py \ */email_tools.py \ @@ -36,7 +36,7 @@ jobs: - uses: astral-sh/setup-uv@v5 - name: Check code formatting in template files run: | - uv run ruff format --check \ + uvx ruff format --check \ *_template.py \ */main.py \ */email_tools.py \ diff --git a/.github/workflows/test-templates.yml b/.github/workflows/test-templates.yml index bac165a..de4c69a 100644 --- a/.github/workflows/test-templates.yml +++ b/.github/workflows/test-templates.yml @@ -49,7 +49,10 @@ jobs: - name: Test template initialization working-directory: test-env run: | - uv run test_templates.py --template ${{ matrix.template }} --output test_output + 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: | From 33ca1b617871dcfa90130fef92e417ae32880d60 Mon Sep 17 00:00:00 2001 From: ShawnPana Date: Tue, 11 Nov 2025 21:18:32 -0800 Subject: [PATCH 5/7] Format template files with ruff --- advanced_template.py | 64 +++--- agentmail/email_tools.py | 341 ++++++++++++++++--------------- agentmail/main.py | 32 +-- default_template.py | 26 +-- job-application/main.py | 190 ++++++++--------- llm-arena/main.py | 40 +++- shopping/launch_chrome_debug.py | 349 +++++++++++++++++--------------- shopping/main.py | 112 +++++----- slack/app/main.py | 1 + slack/app/service.py | 27 +-- tools_template.py | 42 ++-- 11 files changed, 652 insertions(+), 572 deletions(-) diff --git a/advanced_template.py b/advanced_template.py index 5fdb845..594b913 100644 --- a/advanced_template.py +++ b/advanced_template.py @@ -17,35 +17,35 @@ async def main(): - browser = Browser( - use_cloud=False, - # headless=False, - # disable_security=False, - # extra_chromium_args=[], - # allowed_domains=None, - # prohibited_domains=None, - # cdp_url=None, - ) - - llm = ChatBrowserUse() - - agent = Agent( - task='Find the number of stars of the browser-use repository on GitHub', - llm=llm, - browser=browser, - # use_vision='auto', - # save_conversation_path=None, - # max_failures=3, - # generate_gif=False, - # max_actions_per_step=4, - # use_thinking=True, - # flash_mode=False, - # calculate_cost=False, - # step_timeout=180, - ) - - await agent.run() - - -if __name__ == '__main__': - asyncio.run(main()) + browser = Browser( + use_cloud=False, + # headless=False, + # disable_security=False, + # extra_chromium_args=[], + # allowed_domains=None, + # prohibited_domains=None, + # cdp_url=None, + ) + + llm = ChatBrowserUse() + + agent = Agent( + task="Find the number of stars of the browser-use repository on GitHub", + llm=llm, + browser=browser, + # use_vision='auto', + # save_conversation_path=None, + # max_failures=3, + # generate_gif=False, + # max_actions_per_step=4, + # use_thinking=True, + # flash_mode=False, + # calculate_cost=False, + # step_timeout=180, + ) + + await agent.run() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/agentmail/email_tools.py b/agentmail/email_tools.py index c691002..8f2dce8 100644 --- a/agentmail/email_tools.py +++ b/agentmail/email_tools.py @@ -2,7 +2,6 @@ Email management to enable 2fa. """ - import asyncio import logging @@ -15,165 +14,191 @@ # Configure basic logging if not already configured if not logging.getLogger().handlers: - logging.basicConfig(level=logging.INFO, format='%(levelname)s - %(name)s - %(message)s') + logging.basicConfig( + level=logging.INFO, format="%(levelname)s - %(name)s - %(message)s" + ) logger = logging.getLogger(__name__) class EmailTools(Tools): - def __init__( - self, - email_client: AsyncAgentMail | None = None, - email_timeout: int = 30, - inbox: Inbox | None = None, - ): - super().__init__() - self.email_client = email_client or AsyncAgentMail() - - self.email_timeout = email_timeout - - self.register_email_tools() - - self.inbox: Inbox | None = inbox - - def _serialize_message_for_llm(self, message: Message) -> str: - """Serialize a message for the LLM""" - # Use text if available, otherwise convert HTML to simple text - body_content = message.text - if not body_content and message.html: - body_content = self._html_to_text(message.html) - - msg = f'From: {message.from_}\nTo: {message.to}\nTimestamp: {message.timestamp.isoformat()}\nSubject: {message.subject}\nBody: {body_content}' - return msg - - def _html_to_text(self, html: str) -> str: - """Simple HTML to text conversion""" - import re - - # Remove script and style elements - handle spaces in closing tags - html = re.sub(r']*>.*?', '', html, flags=re.DOTALL | re.IGNORECASE) - html = re.sub(r']*>.*?', '', html, flags=re.DOTALL | re.IGNORECASE) - - # Remove HTML tags - html = re.sub(r'<[^>]+>', '', html) - - # Decode HTML entities - html = html.replace(' ', ' ') - html = html.replace('&', '&') - html = html.replace('<', '<') - html = html.replace('>', '>') - html = html.replace('"', '"') - html = html.replace(''', "'") - - # Clean up whitespace - html = re.sub(r'\s+', ' ', html) - html = html.strip() - - return html - - async def get_or_create_inbox_client(self) -> Inbox: - """ - Create a default inbox profile for this API key (assume that agent is on free tier) - - If you are not on free tier it is recommended to create 1 inbox per agent. - """ - if self.inbox: - return self.inbox - - return await self.create_inbox_client() - - async def create_inbox_client(self) -> Inbox: - """ - Create a default inbox profile for this API key (assume that agent is on free tier) - - If you are not on free tier it is recommended to create 1 inbox per agent. - """ - inbox = await self.email_client.inboxes.create() - self.inbox = inbox - return inbox - - async def wait_for_message(self, inbox_id: InboxId) -> Message: - """Wait for a message to be received in the inbox""" - async with self.email_client.websockets.connect() as ws: - await ws.send_subscribe(message=Subscribe(inbox_ids=[inbox_id])) - - try: - while True: - data = await asyncio.wait_for(ws.recv(), timeout=self.email_timeout) - if isinstance(data, MessageReceivedEvent): - await self.email_client.inboxes.messages.update( - inbox_id=inbox_id, message_id=data.message.message_id, remove_labels=['unread'] - ) - msg = data.message - logger.info(f'Received new message from: {msg.from_} with subject: {msg.subject}') - return msg - # If not MessageReceived, continue waiting for the next event - except TimeoutError: - raise TimeoutError(f'No email received in the inbox in {self.email_timeout}s') - - def register_email_tools(self): - """Register all email-related controller actions""" - - @self.action('Get email address for login. You can use this email to login to any service with email and password') - async def get_email_address() -> str: - """Get the email address of the inbox""" - inbox = await self.get_or_create_inbox_client() - logger.info(f'Email address: {inbox.inbox_id}') - return inbox.inbox_id - - @self.action( - 'Get the latest unread email from the inbox from the last max_age_minutes (default 5 minutes). Waits some seconds for new emails if none found. Use for 2FA codes.' - ) - async def get_latest_email(max_age_minutes: int = 5) -> str: - """ - 1. Check for unread emails within the last max_age_minutes - 2. If no recent unread email, wait 30 seconds for new email via websocket - """ - from datetime import datetime, timedelta, timezone - - inbox = await self.get_or_create_inbox_client() - - # Get unread emails - emails = await self.email_client.inboxes.messages.list(inbox_id=inbox.inbox_id, labels=['unread']) - # Filter unread emails by time window - use UTC timezone to match email timestamps - time_cutoff = datetime.now(timezone.utc) - timedelta(minutes=max_age_minutes) - logger.debug(f'Time cutoff: {time_cutoff}') - logger.info(f'Found {len(emails.messages)} unread emails for inbox {inbox.inbox_id}') - recent_unread_emails = [] - - for i, email_summary in enumerate(emails.messages): - # Get full email details to check timestamp - full_email = await self.email_client.inboxes.messages.get( - inbox_id=inbox.inbox_id, message_id=email_summary.message_id - ) - # Handle timezone comparison properly - email_timestamp = full_email.timestamp - if email_timestamp.tzinfo is None: - # If email timestamp is naive, assume UTC - email_timestamp = email_timestamp.replace(tzinfo=timezone.utc) - - if email_timestamp >= time_cutoff: - recent_unread_emails.append(full_email) - - # If we have recent unread emails, return the latest one - if recent_unread_emails: - # Sort by timestamp and get the most recent - recent_unread_emails.sort(key=lambda x: x.timestamp, reverse=True) - logger.info(f'Found {len(recent_unread_emails)} recent unread emails for inbox {inbox.inbox_id}') - - latest_email = recent_unread_emails[0] - - # Mark as read - await self.email_client.inboxes.messages.update( - inbox_id=inbox.inbox_id, message_id=latest_email.message_id, remove_labels=['unread'] - ) - logger.info(f'Latest email from: {latest_email.from_} with subject: {latest_email.subject}') - return self._serialize_message_for_llm(latest_email) - else: - logger.info('No recent unread emails, waiting for a new one') - # No recent unread emails, wait for new one - try: - latest_message = await self.wait_for_message(inbox_id=inbox.inbox_id) - except TimeoutError: - return f'No email received in the inbox in {self.email_timeout}s' - return self._serialize_message_for_llm(latest_message) + def __init__( + self, + email_client: AsyncAgentMail | None = None, + email_timeout: int = 30, + inbox: Inbox | None = None, + ): + super().__init__() + self.email_client = email_client or AsyncAgentMail() + + self.email_timeout = email_timeout + + self.register_email_tools() + + self.inbox: Inbox | None = inbox + + def _serialize_message_for_llm(self, message: Message) -> str: + """Serialize a message for the LLM""" + # Use text if available, otherwise convert HTML to simple text + body_content = message.text + if not body_content and message.html: + body_content = self._html_to_text(message.html) + + msg = f"From: {message.from_}\nTo: {message.to}\nTimestamp: {message.timestamp.isoformat()}\nSubject: {message.subject}\nBody: {body_content}" + return msg + + def _html_to_text(self, html: str) -> str: + """Simple HTML to text conversion""" + import re + + # Remove script and style elements - handle spaces in closing tags + html = re.sub( + r"]*>.*?", "", html, flags=re.DOTALL | re.IGNORECASE + ) + html = re.sub( + r"]*>.*?", "", html, flags=re.DOTALL | re.IGNORECASE + ) + + # Remove HTML tags + html = re.sub(r"<[^>]+>", "", html) + + # Decode HTML entities + html = html.replace(" ", " ") + html = html.replace("&", "&") + html = html.replace("<", "<") + html = html.replace(">", ">") + html = html.replace(""", '"') + html = html.replace("'", "'") + + # Clean up whitespace + html = re.sub(r"\s+", " ", html) + html = html.strip() + + return html + + async def get_or_create_inbox_client(self) -> Inbox: + """ + Create a default inbox profile for this API key (assume that agent is on free tier) + + If you are not on free tier it is recommended to create 1 inbox per agent. + """ + if self.inbox: + return self.inbox + + return await self.create_inbox_client() + + async def create_inbox_client(self) -> Inbox: + """ + Create a default inbox profile for this API key (assume that agent is on free tier) + + If you are not on free tier it is recommended to create 1 inbox per agent. + """ + inbox = await self.email_client.inboxes.create() + self.inbox = inbox + return inbox + + async def wait_for_message(self, inbox_id: InboxId) -> Message: + """Wait for a message to be received in the inbox""" + async with self.email_client.websockets.connect() as ws: + await ws.send_subscribe(message=Subscribe(inbox_ids=[inbox_id])) + + try: + while True: + data = await asyncio.wait_for(ws.recv(), timeout=self.email_timeout) + if isinstance(data, MessageReceivedEvent): + await self.email_client.inboxes.messages.update( + inbox_id=inbox_id, + message_id=data.message.message_id, + remove_labels=["unread"], + ) + msg = data.message + logger.info( + f"Received new message from: {msg.from_} with subject: {msg.subject}" + ) + return msg + # If not MessageReceived, continue waiting for the next event + except TimeoutError: + raise TimeoutError( + f"No email received in the inbox in {self.email_timeout}s" + ) + + def register_email_tools(self): + """Register all email-related controller actions""" + + @self.action( + "Get email address for login. You can use this email to login to any service with email and password" + ) + async def get_email_address() -> str: + """Get the email address of the inbox""" + inbox = await self.get_or_create_inbox_client() + logger.info(f"Email address: {inbox.inbox_id}") + return inbox.inbox_id + + @self.action( + "Get the latest unread email from the inbox from the last max_age_minutes (default 5 minutes). Waits some seconds for new emails if none found. Use for 2FA codes." + ) + async def get_latest_email(max_age_minutes: int = 5) -> str: + """ + 1. Check for unread emails within the last max_age_minutes + 2. If no recent unread email, wait 30 seconds for new email via websocket + """ + from datetime import datetime, timedelta, timezone + + inbox = await self.get_or_create_inbox_client() + + # Get unread emails + emails = await self.email_client.inboxes.messages.list( + inbox_id=inbox.inbox_id, labels=["unread"] + ) + # Filter unread emails by time window - use UTC timezone to match email timestamps + time_cutoff = datetime.now(timezone.utc) - timedelta( + minutes=max_age_minutes + ) + logger.debug(f"Time cutoff: {time_cutoff}") + logger.info( + f"Found {len(emails.messages)} unread emails for inbox {inbox.inbox_id}" + ) + recent_unread_emails = [] + + for i, email_summary in enumerate(emails.messages): + # Get full email details to check timestamp + full_email = await self.email_client.inboxes.messages.get( + inbox_id=inbox.inbox_id, message_id=email_summary.message_id + ) + # Handle timezone comparison properly + email_timestamp = full_email.timestamp + if email_timestamp.tzinfo is None: + # If email timestamp is naive, assume UTC + email_timestamp = email_timestamp.replace(tzinfo=timezone.utc) + + if email_timestamp >= time_cutoff: + recent_unread_emails.append(full_email) + + # If we have recent unread emails, return the latest one + if recent_unread_emails: + # Sort by timestamp and get the most recent + recent_unread_emails.sort(key=lambda x: x.timestamp, reverse=True) + logger.info( + f"Found {len(recent_unread_emails)} recent unread emails for inbox {inbox.inbox_id}" + ) + + latest_email = recent_unread_emails[0] + + # Mark as read + await self.email_client.inboxes.messages.update( + inbox_id=inbox.inbox_id, + message_id=latest_email.message_id, + remove_labels=["unread"], + ) + logger.info( + f"Latest email from: {latest_email.from_} with subject: {latest_email.subject}" + ) + return self._serialize_message_for_llm(latest_email) + else: + logger.info("No recent unread emails, waiting for a new one") + # No recent unread emails, wait for new one + try: + latest_message = await self.wait_for_message(inbox_id=inbox.inbox_id) + except TimeoutError: + return f"No email received in the inbox in {self.email_timeout}s" + return self._serialize_message_for_llm(latest_message) diff --git a/agentmail/main.py b/agentmail/main.py index 9bf98dd..d174c9d 100644 --- a/agentmail/main.py +++ b/agentmail/main.py @@ -4,7 +4,9 @@ from agentmail import AsyncAgentMail # type: ignore -sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +sys.path.append( + os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +) from dotenv import load_dotenv load_dotenv() @@ -18,24 +20,24 @@ async def main(): - # Create email inbox - # Get an API key from https://agentmail.to/ - email_client = AsyncAgentMail(api_key=os.getenv('AGENTMAIL_API_KEY')) - inbox = await email_client.inboxes.create() - print(f'Your email address is: {inbox.inbox_id}\n\n') + # Create email inbox + # Get an API key from https://agentmail.to/ + email_client = AsyncAgentMail(api_key=os.getenv("AGENTMAIL_API_KEY")) + inbox = await email_client.inboxes.create() + print(f"Your email address is: {inbox.inbox_id}\n\n") - # Initialize the tools for browser-use agent - tools = EmailTools(email_client=email_client, inbox=inbox) + # Initialize the tools for browser-use agent + tools = EmailTools(email_client=email_client, inbox=inbox) - # Initialize the LLM for browser-use agent - llm = ChatBrowserUse() + # Initialize the LLM for browser-use agent + llm = ChatBrowserUse() - browser = Browser() + browser = Browser() - agent = Agent(task=TASK, tools=tools, llm=llm, browser=browser) + agent = Agent(task=TASK, tools=tools, llm=llm, browser=browser) - await agent.run() + await agent.run() -if __name__ == '__main__': - asyncio.run(main()) +if __name__ == "__main__": + asyncio.run(main()) diff --git a/default_template.py b/default_template.py index 5e53725..2b884b5 100644 --- a/default_template.py +++ b/default_template.py @@ -15,16 +15,16 @@ async def main(): - browser = Browser(use_cloud=False) - llm = ChatBrowserUse() - task = 'Find the number of stars of the browser-use repository on GitHub' - agent = Agent( - browser=browser, - task=task, - llm=llm, - ) - await agent.run() - - -if __name__ == '__main__': - asyncio.run(main()) + browser = Browser(use_cloud=False) + llm = ChatBrowserUse() + task = "Find the number of stars of the browser-use repository on GitHub" + agent = Agent( + browser=browser, + task=task, + llm=llm, + ) + await agent.run() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/job-application/main.py b/job-application/main.py index 0736c53..95cb409 100644 --- a/job-application/main.py +++ b/job-application/main.py @@ -30,43 +30,43 @@ async def apply_to_job(applicant_info: dict, resume_path: str): - """ - Apply to Rochester Regional Health job with provided information. - - Expected JSON format in applicant_info: - { - "first_name": "John", - "last_name": "Doe", - "email": "john.doe@example.com", - "phone": "555-555-5555", - "age": "21", - "US_citizen": true, - "sponsorship_needed": false, - "postal_code": "12345", - "country": "USA", - "city": "Rochester", - "address": "123 Main St", - "gender": "Male", - "race": "Asian", - "Veteran_status": "Not a veteran", - "disability_status": "No disability" - } - """ - - # Use o3 model for complex form filling tasks - llm = ChatOpenAI(model='o3') - - tools = Tools() - - @tools.action(description='Upload resume file') - async def upload_resume(browser_session): - params = UploadFileAction(path=resume_path, index=0) - return 'Ready to upload resume' - - # Enable cross-origin iframe support for embedded application forms - browser = Browser(cross_origin_iframes=True) - - task = f""" + """ + Apply to Rochester Regional Health job with provided information. + + Expected JSON format in applicant_info: + { + "first_name": "John", + "last_name": "Doe", + "email": "john.doe@example.com", + "phone": "555-555-5555", + "age": "21", + "US_citizen": true, + "sponsorship_needed": false, + "postal_code": "12345", + "country": "USA", + "city": "Rochester", + "address": "123 Main St", + "gender": "Male", + "race": "Asian", + "Veteran_status": "Not a veteran", + "disability_status": "No disability" + } + """ + + # Use o3 model for complex form filling tasks + llm = ChatOpenAI(model="o3") + + tools = Tools() + + @tools.action(description="Upload resume file") + async def upload_resume(browser_session): + params = UploadFileAction(path=resume_path, index=0) + return "Ready to upload resume" + + # Enable cross-origin iframe support for embedded application forms + browser = Browser(cross_origin_iframes=True) + + task = f""" - Your goal is to fill out and submit a job application form with the provided information. - Navigate to https://apply.appcast.io/jobs/50590620606/applyboard/apply/ - Scroll through the entire application and use extract_structured_data action to extract all the relevant information needed to fill out the job application form. use this information and return a structured output that can be used to fill out the entire form: {applicant_info}. Use the done action to finish the task. Fill out the job application form with the following information. @@ -114,57 +114,59 @@ async def upload_resume(browser_session): - At the end of the task, structure your final_result as 1) a human-readable summary of all detections and actions performed on the page with 2) a list with all questions encountered in the page. Do not say "see above." Include a fully written out, human-readable summary at the very end. """ - # Make resume file available for upload - available_file_paths = [resume_path] + # Make resume file available for upload + available_file_paths = [resume_path] - agent = Agent( - task=task, - llm=llm, - browser=browser, - tools=tools, - available_file_paths=available_file_paths, - ) + agent = Agent( + task=task, + llm=llm, + browser=browser, + tools=tools, + available_file_paths=available_file_paths, + ) - history = await agent.run() + history = await agent.run() - return history.final_result() + return history.final_result() async def main(applicant_data_path: str, resume_path: str): - # Verify files exist before starting - if not os.path.exists(applicant_data_path): - raise FileNotFoundError(f'Applicant data file not found: {applicant_data_path}') - if not os.path.exists(resume_path): - raise FileNotFoundError(f'Resume file not found: {resume_path}') - - # Load applicant information from JSON - with open(applicant_data_path) as f: # noqa: ASYNC230 - applicant_info = json.load(f) - - print(f'\n{"=" * 60}') - print('Starting Job Application') - print(f'{"=" * 60}') - print(f'Applicant: {applicant_info.get("first_name")} {applicant_info.get("last_name")}') - print(f'Email: {applicant_info.get("email")}') - print(f'Resume: {resume_path}') - print(f'{"=" * 60}\n') - - # Submit the application - result = await apply_to_job(applicant_info, resume_path=resume_path) - - # Display results - print(f'\n{"=" * 60}') - print('Application Result') - print(f'{"=" * 60}') - print(result) - print(f'{"=" * 60}\n') - - -if __name__ == '__main__': - parser = argparse.ArgumentParser( - description='Automated job application submission', - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=""" + # Verify files exist before starting + if not os.path.exists(applicant_data_path): + raise FileNotFoundError(f"Applicant data file not found: {applicant_data_path}") + if not os.path.exists(resume_path): + raise FileNotFoundError(f"Resume file not found: {resume_path}") + + # Load applicant information from JSON + with open(applicant_data_path) as f: # noqa: ASYNC230 + applicant_info = json.load(f) + + print(f"\n{'=' * 60}") + print("Starting Job Application") + print(f"{'=' * 60}") + print( + f"Applicant: {applicant_info.get('first_name')} {applicant_info.get('last_name')}" + ) + print(f"Email: {applicant_info.get('email')}") + print(f"Resume: {resume_path}") + print(f"{'=' * 60}\n") + + # Submit the application + result = await apply_to_job(applicant_info, resume_path=resume_path) + + # Display results + print(f"\n{'=' * 60}") + print("Application Result") + print(f"{'=' * 60}") + print(result) + print(f"{'=' * 60}\n") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Automated job application submission", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" Examples: # Use included example data python main.py --resume example_resume.pdf @@ -172,14 +174,16 @@ async def main(applicant_data_path: str, resume_path: str): # Use your own data python main.py --data my_info.json --resume my_resume.pdf """, - ) - parser.add_argument( - '--data', - default='applicant_data.json', - help='Path to applicant data JSON file (default: applicant_data.json)', - ) - parser.add_argument('--resume', required=True, help='Path to resume/CV file (PDF format)') - - args = parser.parse_args() - - asyncio.run(main(args.data, args.resume)) + ) + parser.add_argument( + "--data", + default="applicant_data.json", + help="Path to applicant data JSON file (default: applicant_data.json)", + ) + parser.add_argument( + "--resume", required=True, help="Path to resume/CV file (PDF format)" + ) + + args = parser.parse_args() + + asyncio.run(main(args.data, args.resume)) diff --git a/llm-arena/main.py b/llm-arena/main.py index 72540d2..fd37818 100644 --- a/llm-arena/main.py +++ b/llm-arena/main.py @@ -2,7 +2,15 @@ LLM comparison tool - runs the same task across multiple LLMs in parallel """ -from browser_use import Agent, Browser, ChatBrowserUse, ChatGoogle, ChatAnthropic, ChatOpenAI, sandbox +from browser_use import ( + Agent, + Browser, + ChatBrowserUse, + ChatGoogle, + ChatAnthropic, + ChatOpenAI, + sandbox, +) from browser_use.llm.base import BaseChatModel from dotenv import load_dotenv import asyncio @@ -11,6 +19,7 @@ load_dotenv() + @sandbox() async def execute_task(browser: Browser, task: str, llm: BaseChatModel, llm_name: str): """Execute a task with a fresh browser session.""" @@ -29,11 +38,7 @@ async def execute_task(browser: Browser, task: str, llm: BaseChatModel, llm_name print(f"\n✅ {llm_name} - Completed in {elapsed:.2f}s") print(f"📊 {llm_name} - Result: {result}") - return { - 'llm': llm_name, - 'result': result, - 'time': elapsed - } + return {"llm": llm_name, "result": result, "time": elapsed} async def main(): @@ -58,9 +63,22 @@ async def main(): # Define LLMs to compare llms = [ ("Browser Use (bu-0-1)", ChatBrowserUse()), - ("Google Gemini (gemini-flash-latest)", ChatGoogle(model="gemini-flash-latest", api_key=os.getenv("GOOGLE_API_KEY"))), - ("OpenAI ChatGPT (gpt-4.1-mini)", ChatOpenAI(model="gpt-4.1-mini", api_key=os.getenv("OPENAI_API_KEY"))), - ("Anthropic Claude (claude-sonnet-4-0)", ChatAnthropic(model="claude-sonnet-4-0", api_key=os.getenv("ANTHROPIC_API_KEY"))), + ( + "Google Gemini (gemini-flash-latest)", + ChatGoogle( + model="gemini-flash-latest", api_key=os.getenv("GOOGLE_API_KEY") + ), + ), + ( + "OpenAI ChatGPT (gpt-4.1-mini)", + ChatOpenAI(model="gpt-4.1-mini", api_key=os.getenv("OPENAI_API_KEY")), + ), + ( + "Anthropic Claude (claude-sonnet-4-0)", + ChatAnthropic( + model="claude-sonnet-4-0", api_key=os.getenv("ANTHROPIC_API_KEY") + ), + ), ] print(f"\n🏁 Starting race with {len(llms)} LLMs...") @@ -84,7 +102,7 @@ async def main(): valid_results = [r for r in results if isinstance(r, dict)] if valid_results: - sorted_results = sorted(valid_results, key=lambda x: x['time']) + sorted_results = sorted(valid_results, key=lambda x: x["time"]) for i, result in enumerate(sorted_results, 1): print(f"{i}. {result['llm']} - {result['time']:.2f}s") @@ -93,5 +111,5 @@ async def main(): print("🛑 Shutdown complete") -if __name__ == '__main__': +if __name__ == "__main__": asyncio.run(main()) diff --git a/shopping/launch_chrome_debug.py b/shopping/launch_chrome_debug.py index e607b8c..83dc1d4 100755 --- a/shopping/launch_chrome_debug.py +++ b/shopping/launch_chrome_debug.py @@ -25,174 +25,201 @@ def get_chrome_paths(): - """Get Chrome executable and profile paths based on OS""" - system = platform.system() - - if system == 'Darwin': # macOS - chrome_exe = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' - profile_base = Path.home() / 'Library/Application Support/Google/Chrome' - elif system == 'Windows': - chrome_exe = r'C:\Program Files\Google\Chrome\Application\chrome.exe' - if not Path(chrome_exe).exists(): - chrome_exe = r'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe' - profile_base = Path(os.environ.get('LOCALAPPDATA', '')) / 'Google/Chrome/User Data' - else: # Linux - chrome_exe = '/usr/bin/google-chrome' - if not Path(chrome_exe).exists(): - chrome_exe = '/usr/bin/chromium-browser' - profile_base = Path.home() / '.config/google-chrome' - - return chrome_exe, profile_base + """Get Chrome executable and profile paths based on OS""" + system = platform.system() + + if system == "Darwin": # macOS + chrome_exe = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" + profile_base = Path.home() / "Library/Application Support/Google/Chrome" + elif system == "Windows": + chrome_exe = r"C:\Program Files\Google\Chrome\Application\chrome.exe" + if not Path(chrome_exe).exists(): + chrome_exe = r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" + profile_base = ( + Path(os.environ.get("LOCALAPPDATA", "")) / "Google/Chrome/User Data" + ) + else: # Linux + chrome_exe = "/usr/bin/google-chrome" + if not Path(chrome_exe).exists(): + chrome_exe = "/usr/bin/chromium-browser" + profile_base = Path.home() / ".config/google-chrome" + + return chrome_exe, profile_base def cleanup_port_9222(): - """Kill only the process using port 9222 (previous automation Chrome)""" - system = platform.system() - - try: - if system == 'Darwin': # macOS/Linux - # Use lsof to find process using port 9222 - result = subprocess.run(['lsof', '-i', ':9222', '-t'], capture_output=True, text=True, check=False) - if result.stdout.strip(): - pid = result.stdout.strip().split('\n')[0] # Get first PID if multiple - print(f'⚠️ Found previous automation session (PID {pid}), stopping it...') - subprocess.run(['kill', pid], check=False, capture_output=True) - import time - - time.sleep(2) # Wait for port to be freed - print('✅ Port 9222 is now available') - else: - print('✅ Port 9222 is available') - elif system == 'Windows': - # Use netstat to find process using port 9222 - result = subprocess.run(['netstat', '-ano', '-p', 'TCP'], capture_output=True, text=True, check=False) - for line in result.stdout.split('\n'): - if ':9222' in line and 'LISTENING' in line: - pid = line.strip().split()[-1] - print(f'⚠️ Found previous automation session (PID {pid}), stopping it...') - subprocess.run(['taskkill', '/F', '/PID', pid], check=False, capture_output=True) - import time - - time.sleep(2) - print('✅ Port 9222 is now available') - break - else: - print('✅ Port 9222 is available') - else: # Linux - result = subprocess.run(['lsof', '-i', ':9222', '-t'], capture_output=True, text=True, check=False) - if result.stdout.strip(): - pid = result.stdout.strip().split('\n')[0] - print(f'⚠️ Found previous automation session (PID {pid}), stopping it...') - subprocess.run(['kill', pid], check=False, capture_output=True) - import time - - time.sleep(2) - print('✅ Port 9222 is now available') - else: - print('✅ Port 9222 is available') - except Exception as e: - # If port checking fails, just continue (port is likely free) - print(f'ℹ️ Could not check port 9222 status: {e}') - pass + """Kill only the process using port 9222 (previous automation Chrome)""" + system = platform.system() + + try: + if system == "Darwin": # macOS/Linux + # Use lsof to find process using port 9222 + result = subprocess.run( + ["lsof", "-i", ":9222", "-t"], + capture_output=True, + text=True, + check=False, + ) + if result.stdout.strip(): + pid = result.stdout.strip().split("\n")[0] # Get first PID if multiple + print( + f"⚠️ Found previous automation session (PID {pid}), stopping it..." + ) + subprocess.run(["kill", pid], check=False, capture_output=True) + import time + + time.sleep(2) # Wait for port to be freed + print("✅ Port 9222 is now available") + else: + print("✅ Port 9222 is available") + elif system == "Windows": + # Use netstat to find process using port 9222 + result = subprocess.run( + ["netstat", "-ano", "-p", "TCP"], + capture_output=True, + text=True, + check=False, + ) + for line in result.stdout.split("\n"): + if ":9222" in line and "LISTENING" in line: + pid = line.strip().split()[-1] + print( + f"⚠️ Found previous automation session (PID {pid}), stopping it..." + ) + subprocess.run( + ["taskkill", "/F", "/PID", pid], + check=False, + capture_output=True, + ) + import time + + time.sleep(2) + print("✅ Port 9222 is now available") + break + else: + print("✅ Port 9222 is available") + else: # Linux + result = subprocess.run( + ["lsof", "-i", ":9222", "-t"], + capture_output=True, + text=True, + check=False, + ) + if result.stdout.strip(): + pid = result.stdout.strip().split("\n")[0] + print( + f"⚠️ Found previous automation session (PID {pid}), stopping it..." + ) + subprocess.run(["kill", pid], check=False, capture_output=True) + import time + + time.sleep(2) + print("✅ Port 9222 is now available") + else: + print("✅ Port 9222 is available") + except Exception as e: + # If port checking fails, just continue (port is likely free) + print(f"ℹ️ Could not check port 9222 status: {e}") + pass def main(): - # Parse command line arguments - parser = argparse.ArgumentParser( - description='Launch Chrome with remote debugging for browser-use', - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=""" + # Parse command line arguments + parser = argparse.ArgumentParser( + description="Launch Chrome with remote debugging for browser-use", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" Examples: python launch_chrome_debug.py # Uses Default profile python launch_chrome_debug.py --profile "Profile 6" # Uses Profile 6 """, - ) - parser.add_argument( - '--profile', - '-p', - type=str, - default='Default', - help='Chrome profile name to use (default: Default)', - ) - args = parser.parse_args() - - profile_name = args.profile - - # Check and cleanup port 9222 - print('') - print('🔍 Checking port 9222...') - cleanup_port_9222() - print('') - - # Get Chrome paths - chrome_exe, profile_base = get_chrome_paths() - - # Check if Chrome exists - if not Path(chrome_exe).exists(): - print(f'❌ Chrome not found at: {chrome_exe}') - print(' Please install Google Chrome or update the path in this script.') - sys.exit(1) - - # Create temporary directory for this session - automation_dir = Path(tempfile.mkdtemp(prefix='chrome-automation-')) - source_profile = profile_base / profile_name - dest_profile = automation_dir / profile_name - - # Register cleanup handlers to delete temp directory on exit - def cleanup_temp_dir(): - """Delete the temporary automation directory""" - try: - if automation_dir.exists(): - shutil.rmtree(automation_dir, ignore_errors=True) - except Exception: - pass - - atexit.register(cleanup_temp_dir) - - # Handle Ctrl+C gracefully - def signal_handler(sig, frame): - print('\n👋 Shutting down Chrome...') - cleanup_temp_dir() - sys.exit(0) - - signal.signal(signal.SIGINT, signal_handler) - if hasattr(signal, 'SIGTERM'): - signal.signal(signal.SIGTERM, signal_handler) - - # Copy profile from real Chrome profile to temp directory - print(f'📋 Copying {profile_name} profile to temporary directory...') - print(' This includes all your logged-in sessions (GitHub, Google, etc.)') - - if source_profile.exists(): - shutil.copytree(source_profile, dest_profile) - print('✅ Profile ready') - else: - print(f'⚠️ {profile_name} profile not found at: {source_profile}') - print(' Creating empty profile...') - dest_profile.mkdir(parents=True, exist_ok=True) - - print('') - print('🚀 Launching Chrome with remote debugging on port 9222...') - print('🔗 CDP endpoint: http://localhost:9222') - print('') - print('⚠️ Keep this terminal open - closing it will close Chrome') - print('💡 Open a NEW terminal and run: uv run main.py') - print('') - - # Launch Chrome with remote debugging - cmd = [ - chrome_exe, - '--remote-debugging-port=9222', - f'--user-data-dir={automation_dir}', - f'--profile-directory={profile_name}', - ] - - try: - subprocess.run(cmd) - except KeyboardInterrupt: - print('\n👋 Shutting down Chrome...') - sys.exit(0) - - -if __name__ == '__main__': - main() + ) + parser.add_argument( + "--profile", + "-p", + type=str, + default="Default", + help="Chrome profile name to use (default: Default)", + ) + args = parser.parse_args() + + profile_name = args.profile + + # Check and cleanup port 9222 + print("") + print("🔍 Checking port 9222...") + cleanup_port_9222() + print("") + + # Get Chrome paths + chrome_exe, profile_base = get_chrome_paths() + + # Check if Chrome exists + if not Path(chrome_exe).exists(): + print(f"❌ Chrome not found at: {chrome_exe}") + print(" Please install Google Chrome or update the path in this script.") + sys.exit(1) + + # Create temporary directory for this session + automation_dir = Path(tempfile.mkdtemp(prefix="chrome-automation-")) + source_profile = profile_base / profile_name + dest_profile = automation_dir / profile_name + + # Register cleanup handlers to delete temp directory on exit + def cleanup_temp_dir(): + """Delete the temporary automation directory""" + try: + if automation_dir.exists(): + shutil.rmtree(automation_dir, ignore_errors=True) + except Exception: + pass + + atexit.register(cleanup_temp_dir) + + # Handle Ctrl+C gracefully + def signal_handler(sig, frame): + print("\n👋 Shutting down Chrome...") + cleanup_temp_dir() + sys.exit(0) + + signal.signal(signal.SIGINT, signal_handler) + if hasattr(signal, "SIGTERM"): + signal.signal(signal.SIGTERM, signal_handler) + + # Copy profile from real Chrome profile to temp directory + print(f"📋 Copying {profile_name} profile to temporary directory...") + print(" This includes all your logged-in sessions (GitHub, Google, etc.)") + + if source_profile.exists(): + shutil.copytree(source_profile, dest_profile) + print("✅ Profile ready") + else: + print(f"⚠️ {profile_name} profile not found at: {source_profile}") + print(" Creating empty profile...") + dest_profile.mkdir(parents=True, exist_ok=True) + + print("") + print("🚀 Launching Chrome with remote debugging on port 9222...") + print("🔗 CDP endpoint: http://localhost:9222") + print("") + print("⚠️ Keep this terminal open - closing it will close Chrome") + print("💡 Open a NEW terminal and run: uv run main.py") + print("") + + # Launch Chrome with remote debugging + cmd = [ + chrome_exe, + "--remote-debugging-port=9222", + f"--user-data-dir={automation_dir}", + f"--profile-directory={profile_name}", + ] + + try: + subprocess.run(cmd) + except KeyboardInterrupt: + print("\n👋 Shutting down Chrome...") + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/shopping/main.py b/shopping/main.py index d21fbeb..89044df 100644 --- a/shopping/main.py +++ b/shopping/main.py @@ -6,28 +6,30 @@ class GroceryItem(BaseModel): - """A single grocery item""" + """A single grocery item""" - name: str = Field(..., description='Item name') - price: float = Field(..., description='Price as number') - brand: str | None = Field(None, description='Brand name') - size: str | None = Field(None, description='Size or quantity') - url: str = Field(..., description='Full URL to item') + name: str = Field(..., description="Item name") + price: float = Field(..., description="Price as number") + brand: str | None = Field(None, description="Brand name") + size: str | None = Field(None, description="Size or quantity") + url: str = Field(..., description="Full URL to item") class GroceryCart(BaseModel): - """Grocery cart results""" + """Grocery cart results""" - items: list[GroceryItem] = Field(default_factory=list, description='All grocery items found') + items: list[GroceryItem] = Field( + default_factory=list, description="All grocery items found" + ) -async def add_to_cart(items: list[str] = ['milk', 'eggs', 'bread']): - browser = Browser(cdp_url='http://localhost:9222') +async def add_to_cart(items: list[str] = ["milk", "eggs", "bread"]): + browser = Browser(cdp_url="http://localhost:9222") - llm = ChatBrowserUse() + llm = ChatBrowserUse() - # Task prompt - task = f""" + # Task prompt + task = f""" Search for "{items}" on Instacart at the nearest store. You will buy all of the items at the same store. @@ -40,44 +42,46 @@ async def add_to_cart(items: list[str] = ['milk', 'eggs', 'bread']): - Instacart: https://www.instacart.com/ """ - # Create agent with structured output - agent = Agent( - browser=browser, - llm=llm, - task=task, - output_model_schema=GroceryCart, - ) - - # Run the agent - result = await agent.run() - return result - - -if __name__ == '__main__': - # Get user input - items_input = input('What items would you like to add to cart (comma-separated)? ').strip() - if not items_input: - items = ['milk', 'eggs', 'bread'] - print(f'Using default items: {items}') - else: - items = [item.strip() for item in items_input.split(',')] - - result = asyncio.run(add_to_cart(items)) - - # Access structured output - if result and result.structured_output: - cart = result.structured_output - - print(f'\n{"=" * 60}') - print('Items Added to Cart') - print(f'{"=" * 60}\n') - - for item in cart.items: - print(f'Name: {item.name}') - print(f'Price: ${item.price}') - if item.brand: - print(f'Brand: {item.brand}') - if item.size: - print(f'Size: {item.size}') - print(f'URL: {item.url}') - print(f'{"-" * 60}') + # Create agent with structured output + agent = Agent( + browser=browser, + llm=llm, + task=task, + output_model_schema=GroceryCart, + ) + + # Run the agent + result = await agent.run() + return result + + +if __name__ == "__main__": + # Get user input + items_input = input( + "What items would you like to add to cart (comma-separated)? " + ).strip() + if not items_input: + items = ["milk", "eggs", "bread"] + print(f"Using default items: {items}") + else: + items = [item.strip() for item in items_input.split(",")] + + result = asyncio.run(add_to_cart(items)) + + # Access structured output + if result and result.structured_output: + cart = result.structured_output + + print(f"\n{'=' * 60}") + print("Items Added to Cart") + print(f"{'=' * 60}\n") + + for item in cart.items: + print(f"Name: {item.name}") + print(f"Price: ${item.price}") + if item.brand: + print(f"Brand: {item.brand}") + if item.size: + print(f"Size: {item.size}") + print(f"URL: {item.url}") + print(f"{'-' * 60}") diff --git a/slack/app/main.py b/slack/app/main.py index bd1b95a..e6a4bbc 100644 --- a/slack/app/main.py +++ b/slack/app/main.py @@ -51,6 +51,7 @@ async def slack_events(request: Request): raise except Exception as e: import traceback + logger.error(f"Error in slack_events: {str(e)}") logger.error(f"Traceback: {traceback.format_exc()}") raise HTTPException(status_code=500, detail="Failed to process Slack event") diff --git a/slack/app/service.py b/slack/app/service.py index 5abc4ba..bcac9b7 100644 --- a/slack/app/service.py +++ b/slack/app/service.py @@ -23,9 +23,9 @@ def __init__(self, access_token: str): def format_for_slack(self, text: str) -> str: """Convert markdown-style text to Slack-friendly format""" # Replace escaped newlines with actual newlines - text = text.replace('\\n', '\n') + text = text.replace("\\n", "\n") # Convert markdown bold (**text**) to Slack bold (*text*) - text = text.replace('**', '*') + text = text.replace("**", "*") return text async def send_message( @@ -92,9 +92,7 @@ async def process_agent_task_async(self, task: str, channel_id: str): """Async function to process the agent task""" try: # Send initial "starting" message and capture its timestamp - response = await self.send_message( - channel_id, "Starting browser task..." - ) + response = await self.send_message(channel_id, "Starting browser task...") if not response or not response.get("ok"): logger.error(f"Failed to send initial message: {response}") return @@ -105,7 +103,7 @@ async def process_agent_task_async(self, task: str, channel_id: str): return # Get profile_id from environment (optional) - profile_id = os.getenv('BROWSER_USE_PROFILE_ID') + profile_id = os.getenv("BROWSER_USE_PROFILE_ID") # Callback to capture browser session info def on_browser_created(data: BrowserCreatedData): @@ -115,22 +113,21 @@ def on_browser_created(data: BrowserCreatedData): # Send live URL to Slack immediately asyncio.create_task( - self.send_message( - channel_id, - f"📺 Live session: {data.live_url}" - ) + self.send_message(channel_id, f"📺 Live session: {data.live_url}") ) # Create standalone function for sandbox decorator @sandbox( - log_level='INFO', + log_level="INFO", cloud_timeout=30, cloud_profile_id=profile_id, - on_browser_created=on_browser_created + on_browser_created=on_browser_created, ) async def execute_task(browser: Browser, task_description: str): """Execute browser task in sandbox""" - agent = Agent(browser=browser, task=task_description, llm=ChatBrowserUse()) + agent = Agent( + browser=browser, task=task_description, llm=ChatBrowserUse() + ) result = await agent.run() return result.final_result() @@ -150,8 +147,6 @@ async def execute_task(browser: Browser, task_description: str): # Send error message as a new message try: - await self.send_message( - channel_id, f"❌ Error: {error_message}" - ) + await self.send_message(channel_id, f"❌ Error: {error_message}") except Exception as send_error: logger.error(f"Failed to send error message: {str(send_error)}") diff --git a/tools_template.py b/tools_template.py index 7e86dab..4733d23 100644 --- a/tools_template.py +++ b/tools_template.py @@ -17,30 +17,34 @@ tools = Tools() -@tools.registry.action('Save text content to a file') +@tools.registry.action("Save text content to a file") async def save_to_file(filename: str, content: str): - from pathlib import Path + from pathlib import Path - try: - Path(filename).write_text(content, encoding='utf-8') - return ActionResult(extracted_content=f'Saved to {filename}', include_in_memory=True) - except Exception as e: - return ActionResult(extracted_content=f'Error saving file: {e}', include_in_memory=True) + try: + Path(filename).write_text(content, encoding="utf-8") + return ActionResult( + extracted_content=f"Saved to {filename}", include_in_memory=True + ) + except Exception as e: + return ActionResult( + extracted_content=f"Error saving file: {e}", include_in_memory=True + ) async def main(): - browser = Browser(use_cloud=False) - llm = ChatBrowserUse() - task = 'Go to github.com and find the number of GitHub stars for browser-use and use the save_to_file tool to save the result to stars.txt' - agent = Agent( - task=task, - llm=llm, - browser=browser, - tools=tools, - ) + browser = Browser(use_cloud=False) + llm = ChatBrowserUse() + task = "Go to github.com and find the number of GitHub stars for browser-use and use the save_to_file tool to save the result to stars.txt" + agent = Agent( + task=task, + llm=llm, + browser=browser, + tools=tools, + ) - await agent.run() + await agent.run() -if __name__ == '__main__': - asyncio.run(main()) +if __name__ == "__main__": + asyncio.run(main()) From b2168f5adf70b8f3bae2b0e81d2102c95497cb16 Mon Sep 17 00:00:00 2001 From: ShawnPana Date: Tue, 11 Nov 2025 21:23:28 -0800 Subject: [PATCH 6/7] Fix HTML filtering regex security vulnerability --- agentmail/email_tools.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/agentmail/email_tools.py b/agentmail/email_tools.py index 8f2dce8..6526aaf 100644 --- a/agentmail/email_tools.py +++ b/agentmail/email_tools.py @@ -51,12 +51,12 @@ def _html_to_text(self, html: str) -> str: """Simple HTML to text conversion""" import re - # Remove script and style elements - handle spaces in closing tags + # Remove script and style elements - handle any content in closing tags html = re.sub( - r"]*>.*?", "", html, flags=re.DOTALL | re.IGNORECASE + r"]*>.*?]*>", "", html, flags=re.DOTALL | re.IGNORECASE ) html = re.sub( - r"]*>.*?", "", html, flags=re.DOTALL | re.IGNORECASE + r"]*>.*?]*>", "", html, flags=re.DOTALL | re.IGNORECASE ) # Remove HTML tags From 4702e555f3b66d168fbe4da0c0db7551e447e832 Mon Sep 17 00:00:00 2001 From: ShawnPana Date: Tue, 11 Nov 2025 21:24:08 -0800 Subject: [PATCH 7/7] Format email_tools.py with ruff --- agentmail/email_tools.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/agentmail/email_tools.py b/agentmail/email_tools.py index 6526aaf..80e7d97 100644 --- a/agentmail/email_tools.py +++ b/agentmail/email_tools.py @@ -53,7 +53,10 @@ def _html_to_text(self, html: str) -> str: # Remove script and style elements - handle any content in closing tags html = re.sub( - r"]*>.*?]*>", "", html, flags=re.DOTALL | re.IGNORECASE + r"]*>.*?]*>", + "", + html, + flags=re.DOTALL | re.IGNORECASE, ) html = re.sub( r"]*>.*?]*>", "", html, flags=re.DOTALL | re.IGNORECASE