chore: import upstream snapshot with attribution
Test Browser Use CLI Install / uv pip install (ubuntu-latest) (push) Failing after 1s
Test Browser Use CLI Install / uvx browser-use from local wheel (push) Failing after 1s
Test Browser Use CLI Install / uvx browser-use[cli] from PyPI (push) Failing after 1s
package / pip-install-on-macos-latest-py-3.11 (push) Has been skipped
package / pip-install-on-macos-latest-py-3.13 (push) Has been skipped
package / pip-install-on-ubuntu-latest-py-3.11 (push) Has been skipped
package / pip-install-on-windows-latest-py-3.13 (push) Has been skipped
cloud_evals / trigger_cloud_eval_image_build (push) Failing after 1s
docker / build_publish_image (push) Failing after 1s
Test Browser Use CLI Install / browser-use skill sync (push) Failing after 1s
lint / code-style (push) Failing after 0s
lint / type-checker (push) Failing after 1s
package / pip-build (push) Failing after 1s
lint / syntax-errors (push) Failing after 3s
package / pip-install-on-ubuntu-latest-py-3.13 (push) Has been skipped
package / pip-install-on-windows-latest-py-3.11 (push) Has been skipped
test / ${{ matrix.test_filename }} (push) Has been skipped
test / evaluate-tasks (push) Has been skipped
test / setup-chromium (push) Failing after 2s
test / find_tests (push) Failing after 2s
Test Browser Use CLI Install / uv pip install (windows-latest) (push) Has been cancelled
Test Browser Use CLI Install / uv pip install (macos-latest) (push) Has been cancelled
Test Browser Use CLI Install / uv pip install (ubuntu-latest) (push) Failing after 1s
Test Browser Use CLI Install / uvx browser-use from local wheel (push) Failing after 1s
Test Browser Use CLI Install / uvx browser-use[cli] from PyPI (push) Failing after 1s
package / pip-install-on-macos-latest-py-3.11 (push) Has been skipped
package / pip-install-on-macos-latest-py-3.13 (push) Has been skipped
package / pip-install-on-ubuntu-latest-py-3.11 (push) Has been skipped
package / pip-install-on-windows-latest-py-3.13 (push) Has been skipped
cloud_evals / trigger_cloud_eval_image_build (push) Failing after 1s
docker / build_publish_image (push) Failing after 1s
Test Browser Use CLI Install / browser-use skill sync (push) Failing after 1s
lint / code-style (push) Failing after 0s
lint / type-checker (push) Failing after 1s
package / pip-build (push) Failing after 1s
lint / syntax-errors (push) Failing after 3s
package / pip-install-on-ubuntu-latest-py-3.13 (push) Has been skipped
package / pip-install-on-windows-latest-py-3.11 (push) Has been skipped
test / ${{ matrix.test_filename }} (push) Has been skipped
test / evaluate-tasks (push) Has been skipped
test / setup-chromium (push) Failing after 2s
test / find_tests (push) Failing after 2s
Test Browser Use CLI Install / uv pip install (windows-latest) (push) Has been cancelled
Test Browser Use CLI Install / uv pip install (macos-latest) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
# Ad-Use
|
||||
|
||||
Automatically generate Instagram image ads and TikTok video ads from any landing page using browser agents, Google's Nano Banana 🍌, and Veo3.
|
||||
|
||||
> [!WARNING]
|
||||
> This demo requires browser-use v0.7.7+.
|
||||
|
||||
https://github.com/user-attachments/assets/7fab54a9-b36b-4fba-ab98-a438f2b86b7e
|
||||
|
||||
## Features
|
||||
|
||||
1. Agent visits your target website
|
||||
2. Captures brand name, tagline, and key selling points
|
||||
3. Takes a clean screenshot for design reference
|
||||
4. Creates scroll-stopping Instagram image ads with 🍌
|
||||
5. Generates viral TikTok video ads with Veo3
|
||||
6. Supports parallel generation of multiple ads
|
||||
|
||||
## Setup
|
||||
|
||||
Make sure the newest version of browser-use is installed (with screenshot functionality):
|
||||
```bash
|
||||
pip install -U browser-use
|
||||
```
|
||||
|
||||
Export your Gemini API key, get it from: [Google AI Studio](https://makersuite.google.com/app/apikey)
|
||||
```
|
||||
export GOOGLE_API_KEY='your-google-api-key-here'
|
||||
```
|
||||
|
||||
Clone the repo and cd into the app folder
|
||||
```bash
|
||||
git clone https://github.com/browser-use/browser-use.git
|
||||
cd browser-use/examples/apps/ad-use
|
||||
```
|
||||
|
||||
## Normal Usage
|
||||
|
||||
```bash
|
||||
# Basic - Generate Instagram image ad (default)
|
||||
python ad_generator.py --url https://www.apple.com/iphone-17-pro/
|
||||
|
||||
# Generate TikTok video ad with Veo3
|
||||
python ad_generator.py --tiktok --url https://www.apple.com/iphone-17-pro/
|
||||
|
||||
# Generate multiple ads in parallel
|
||||
python ad_generator.py --instagram --count 3 --url https://www.apple.com/iphone-17-pro/
|
||||
python ad_generator.py --tiktok --count 2 --url https://www.apple.com/iphone-17-pro/
|
||||
|
||||
# Debug Mode - See the browser in action
|
||||
python ad_generator.py --url https://www.apple.com/iphone-17-pro/ --debug
|
||||
```
|
||||
|
||||
## Command Line Options
|
||||
|
||||
- `--url`: Landing page URL to analyze
|
||||
- `--instagram`: Generate Instagram image ad (default if no flag specified)
|
||||
- `--tiktok`: Generate TikTok video ad using Veo3
|
||||
- `--count N`: Generate N ads in parallel (default: 1)
|
||||
- `--debug`: Show browser window and enable verbose logging
|
||||
|
||||
## Programmatic Usage
|
||||
```python
|
||||
import asyncio
|
||||
from ad_generator import create_ad_from_landing_page
|
||||
|
||||
async def main():
|
||||
results = await create_ad_from_landing_page(
|
||||
url="https://your-landing-page.com",
|
||||
debug=False
|
||||
)
|
||||
print(f"Generated ads: {results}")
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
Generated ads are saved in the `output/` directory with:
|
||||
- **PNG image files** (ad_timestamp.png) - Instagram ads generated with Gemini 2.5 Flash Image
|
||||
- **MP4 video files** (ad_timestamp.mp4) - TikTok ads generated with Veo3
|
||||
- **Analysis files** (analysis_timestamp.txt) - Browser agent analysis and prompts used
|
||||
- **Landing page screenshots** (landing_page_timestamp.png) - Reference screenshots
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,417 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from browser_use.utils import create_task_with_error_handling
|
||||
|
||||
|
||||
def setup_environment(debug: bool):
|
||||
if not debug:
|
||||
os.environ['BROWSER_USE_SETUP_LOGGING'] = 'false'
|
||||
os.environ['BROWSER_USE_LOGGING_LEVEL'] = 'critical'
|
||||
logging.getLogger().setLevel(logging.CRITICAL)
|
||||
else:
|
||||
os.environ['BROWSER_USE_SETUP_LOGGING'] = 'true'
|
||||
os.environ['BROWSER_USE_LOGGING_LEVEL'] = 'info'
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(description='Generate ads from landing pages using browser-use + 🍌')
|
||||
parser.add_argument('--url', nargs='?', help='Landing page URL to analyze')
|
||||
parser.add_argument('--debug', action='store_true', default=False, help='Enable debug mode (show browser, verbose logs)')
|
||||
parser.add_argument('--count', type=int, default=1, help='Number of ads to generate in parallel (default: 1)')
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument('--instagram', action='store_true', default=False, help='Generate Instagram image ad (default)')
|
||||
group.add_argument('--tiktok', action='store_true', default=False, help='Generate TikTok video ad using Veo3')
|
||||
args = parser.parse_args()
|
||||
if not args.instagram and not args.tiktok:
|
||||
args.instagram = True
|
||||
setup_environment(args.debug)
|
||||
|
||||
from typing import Any, cast
|
||||
|
||||
import aiofiles
|
||||
from google import genai
|
||||
from PIL import Image
|
||||
|
||||
from browser_use import Agent, BrowserSession
|
||||
from browser_use.llm.google import ChatGoogle
|
||||
|
||||
GOOGLE_API_KEY = os.getenv('GOOGLE_API_KEY')
|
||||
|
||||
|
||||
class LandingPageAnalyzer:
|
||||
def __init__(self, debug: bool = False):
|
||||
self.debug = debug
|
||||
self.llm = ChatGoogle(model='gemini-2.0-flash-exp', api_key=GOOGLE_API_KEY)
|
||||
self.output_dir = Path('output')
|
||||
self.output_dir.mkdir(exist_ok=True)
|
||||
|
||||
async def analyze_landing_page(self, url: str, mode: str = 'instagram') -> dict:
|
||||
browser_session = BrowserSession(
|
||||
headless=not self.debug,
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
task=f"""Go to {url} and quickly extract key brand information for Instagram ad creation.
|
||||
|
||||
Steps:
|
||||
1. Navigate to the website
|
||||
2. From the initial view, extract ONLY these essentials:
|
||||
- Brand/Product name
|
||||
- Main tagline or value proposition (one sentence)
|
||||
- Primary call-to-action text
|
||||
- Any visible pricing or special offer
|
||||
3. Scroll down half a page, twice (0.5 pages each) to check for any key info
|
||||
4. Done - keep it simple and focused on the brand
|
||||
|
||||
Return ONLY the key brand info, not page structure details.""",
|
||||
llm=self.llm,
|
||||
browser_session=browser_session,
|
||||
max_actions_per_step=2,
|
||||
step_timeout=30,
|
||||
use_thinking=False,
|
||||
vision_detail_level='high',
|
||||
)
|
||||
|
||||
screenshot_path = None
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
|
||||
async def screenshot_callback(agent_instance):
|
||||
nonlocal screenshot_path
|
||||
await asyncio.sleep(4)
|
||||
screenshot_path = self.output_dir / f'landing_page_{timestamp}.png'
|
||||
await agent_instance.browser_session.take_screenshot(path=str(screenshot_path), full_page=False)
|
||||
|
||||
screenshot_task = create_task_with_error_handling(
|
||||
screenshot_callback(agent), name='screenshot_callback', suppress_exceptions=True
|
||||
)
|
||||
history = await agent.run()
|
||||
try:
|
||||
await screenshot_task
|
||||
except Exception as e:
|
||||
print(f'Screenshot task failed: {e}')
|
||||
|
||||
analysis = history.final_result() or 'No analysis content extracted'
|
||||
return {'url': url, 'analysis': analysis, 'screenshot_path': screenshot_path, 'timestamp': timestamp}
|
||||
|
||||
|
||||
class AdGenerator:
|
||||
def __init__(self, api_key: str | None = GOOGLE_API_KEY, mode: str = 'instagram'):
|
||||
if not api_key:
|
||||
raise ValueError('GOOGLE_API_KEY is missing or empty – set the environment variable or pass api_key explicitly')
|
||||
|
||||
self.client = genai.Client(api_key=api_key)
|
||||
self.output_dir = Path('output')
|
||||
self.output_dir.mkdir(exist_ok=True)
|
||||
self.mode = mode
|
||||
|
||||
async def create_video_concept(self, browser_analysis: str, ad_id: int) -> str:
|
||||
"""Generate a unique creative concept for each video ad"""
|
||||
if self.mode != 'tiktok':
|
||||
return ''
|
||||
|
||||
concept_prompt = f"""Based on this brand analysis:
|
||||
{browser_analysis}
|
||||
|
||||
Create a UNIQUE and SPECIFIC TikTok video concept #{ad_id}.
|
||||
|
||||
Be creative and different! Consider various approaches like:
|
||||
- Different visual metaphors and storytelling angles
|
||||
- Various trending TikTok formats (transitions, reveals, transformations)
|
||||
- Different emotional appeals (funny, inspiring, surprising, relatable)
|
||||
- Unique visual styles (neon, retro, minimalist, maximalist, surreal)
|
||||
- Different perspectives (first-person, aerial, macro, time-lapse)
|
||||
|
||||
Return a 2-3 sentence description of a specific, unique video concept that would work for this brand.
|
||||
Make it visually interesting and different from typical ads. Be specific about visual elements, transitions, and mood."""
|
||||
|
||||
response = self.client.models.generate_content(model='gemini-2.0-flash-exp', contents=concept_prompt)
|
||||
return response.text if response and response.text else ''
|
||||
|
||||
def create_ad_prompt(self, browser_analysis: str, video_concept: str = '') -> str:
|
||||
if self.mode == 'instagram':
|
||||
prompt = f"""Create an Instagram ad for this brand:
|
||||
|
||||
{browser_analysis}
|
||||
|
||||
Create a vibrant, eye-catching Instagram ad image with:
|
||||
- Try to use the colors and style of the logo or brand, else:
|
||||
- Bold, modern gradient background with bright colors
|
||||
- Large, playful sans-serif text with the product/service name from the analysis
|
||||
- Trendy design elements: geometric shapes, sparkles, emojis
|
||||
- Fun bubbles or badges for any pricing or special offers mentioned
|
||||
- Call-to-action button with text from the analysis
|
||||
- Emphasizes the key value proposition from the analysis
|
||||
- Uses visual elements that match the brand personality
|
||||
- Square format (1:1 ratio)
|
||||
- Use color psychology to drive action
|
||||
|
||||
Style: Modern Instagram advertisement, (1:1), scroll-stopping, professional but playful, conversion-focused"""
|
||||
else: # tiktok
|
||||
if video_concept:
|
||||
prompt = f"""Create a TikTok video ad based on this specific concept:
|
||||
|
||||
{video_concept}
|
||||
|
||||
Brand context: {browser_analysis}
|
||||
|
||||
Requirements:
|
||||
- Vertical 9:16 format
|
||||
- High quality, professional execution
|
||||
- Bring the concept to life exactly as described
|
||||
- No text overlays, pure visual storytelling"""
|
||||
else:
|
||||
prompt = f"""Create a viral TikTok video ad for this brand:
|
||||
|
||||
{browser_analysis}
|
||||
|
||||
Create a dynamic, engaging vertical video with:
|
||||
- Quick hook opening that grabs attention immediately
|
||||
- Minimal text overlays (focus on visual storytelling)
|
||||
- Fast-paced but not overwhelming editing
|
||||
- Authentic, relatable energy that appeals to Gen Z
|
||||
- Vertical 9:16 format optimized for mobile
|
||||
- High energy but professional execution
|
||||
|
||||
Style: Modern TikTok advertisement, viral potential, authentic energy, minimal text, maximum visual impact"""
|
||||
return prompt
|
||||
|
||||
async def generate_ad_image(self, prompt: str, screenshot_path: Path | None = None) -> bytes | None:
|
||||
"""Generate ad image bytes using Gemini. Returns None on failure."""
|
||||
try:
|
||||
from typing import Any
|
||||
|
||||
contents: list[Any] = [prompt]
|
||||
|
||||
if screenshot_path and screenshot_path.exists():
|
||||
img = Image.open(screenshot_path)
|
||||
w, h = img.size
|
||||
side = min(w, h)
|
||||
img = img.crop(((w - side) // 2, (h - side) // 2, (w + side) // 2, (h + side) // 2))
|
||||
contents = [prompt + '\n\nHere is the actual landing page screenshot to reference for design inspiration:', img]
|
||||
|
||||
response = await self.client.aio.models.generate_content(
|
||||
model='gemini-2.5-flash-image-preview',
|
||||
contents=contents,
|
||||
)
|
||||
|
||||
cand = getattr(response, 'candidates', None)
|
||||
if cand:
|
||||
for part in getattr(cand[0].content, 'parts', []):
|
||||
inline = getattr(part, 'inline_data', None)
|
||||
if inline:
|
||||
return inline.data
|
||||
except Exception as e:
|
||||
print(f'❌ Image generation failed: {e}')
|
||||
return None
|
||||
|
||||
async def generate_ad_video(self, prompt: str, screenshot_path: Path | None = None, ad_id: int = 1) -> bytes:
|
||||
"""Generate ad video using Veo3."""
|
||||
sync_client = genai.Client(api_key=GOOGLE_API_KEY)
|
||||
|
||||
# Commented out image input for now - it was using the screenshot as first frame
|
||||
# if screenshot_path and screenshot_path.exists():
|
||||
# import base64
|
||||
# import io
|
||||
|
||||
# img = Image.open(screenshot_path)
|
||||
# img_buffer = io.BytesIO()
|
||||
# img.save(img_buffer, format='PNG')
|
||||
# img_bytes = img_buffer.getvalue()
|
||||
|
||||
# operation = sync_client.models.generate_videos(
|
||||
# model='veo-3.0-generate-001',
|
||||
# prompt=prompt,
|
||||
# image=cast(Any, {
|
||||
# 'imageBytes': base64.b64encode(img_bytes).decode('utf-8'),
|
||||
# 'mimeType': 'image/png'
|
||||
# }),
|
||||
# config=cast(Any, {'aspectRatio': '9:16', 'resolution': '720p'}),
|
||||
# )
|
||||
# else:
|
||||
operation = sync_client.models.generate_videos(
|
||||
model='veo-3.0-generate-001',
|
||||
prompt=prompt,
|
||||
config=cast(Any, {'aspectRatio': '9:16', 'resolution': '720p'}),
|
||||
)
|
||||
|
||||
while not operation.done:
|
||||
await asyncio.sleep(10)
|
||||
operation = sync_client.operations.get(operation)
|
||||
|
||||
if not operation.response or not operation.response.generated_videos:
|
||||
raise RuntimeError('No videos generated')
|
||||
videos = operation.response.generated_videos
|
||||
video = videos[0]
|
||||
video_file = getattr(video, 'video', None)
|
||||
if not video_file:
|
||||
raise RuntimeError('No video file in response')
|
||||
sync_client.files.download(file=video_file)
|
||||
video_bytes = getattr(video_file, 'video_bytes', None)
|
||||
if not video_bytes:
|
||||
raise RuntimeError('No video bytes in response')
|
||||
return video_bytes
|
||||
|
||||
async def save_results(self, ad_content: bytes, prompt: str, analysis: str, url: str, timestamp: str) -> str:
|
||||
if self.mode == 'instagram':
|
||||
content_path = self.output_dir / f'ad_{timestamp}.png'
|
||||
else: # tiktok
|
||||
content_path = self.output_dir / f'ad_{timestamp}.mp4'
|
||||
|
||||
async with aiofiles.open(content_path, 'wb') as f:
|
||||
await f.write(ad_content)
|
||||
|
||||
analysis_path = self.output_dir / f'analysis_{timestamp}.txt'
|
||||
async with aiofiles.open(analysis_path, 'w', encoding='utf-8') as f:
|
||||
await f.write(f'URL: {url}\n\n')
|
||||
await f.write('BROWSER-USE ANALYSIS:\n')
|
||||
await f.write(analysis)
|
||||
await f.write('\n\nGENERATED PROMPT:\n')
|
||||
await f.write(prompt)
|
||||
|
||||
return str(content_path)
|
||||
|
||||
|
||||
def open_file(file_path: str):
|
||||
"""Open file with default system viewer"""
|
||||
try:
|
||||
if sys.platform.startswith('darwin'):
|
||||
subprocess.run(['open', file_path], check=True)
|
||||
elif sys.platform.startswith('win'):
|
||||
subprocess.run(['cmd', '/c', 'start', '', file_path], check=True)
|
||||
else:
|
||||
subprocess.run(['xdg-open', file_path], check=True)
|
||||
except Exception as e:
|
||||
print(f'❌ Could not open file: {e}')
|
||||
|
||||
|
||||
async def create_ad_from_landing_page(url: str, debug: bool = False, mode: str = 'instagram', ad_id: int = 1):
|
||||
analyzer = LandingPageAnalyzer(debug=debug)
|
||||
|
||||
try:
|
||||
if ad_id == 1:
|
||||
print(f'🚀 Analyzing {url} for {mode.capitalize()} ad...')
|
||||
page_data = await analyzer.analyze_landing_page(url, mode=mode)
|
||||
else:
|
||||
analyzer_temp = LandingPageAnalyzer(debug=debug)
|
||||
page_data = await analyzer_temp.analyze_landing_page(url, mode=mode)
|
||||
|
||||
generator = AdGenerator(mode=mode)
|
||||
|
||||
if mode == 'instagram':
|
||||
prompt = generator.create_ad_prompt(page_data['analysis'])
|
||||
ad_content = await generator.generate_ad_image(prompt, page_data.get('screenshot_path'))
|
||||
if ad_content is None:
|
||||
raise RuntimeError(f'Ad image generation failed for ad #{ad_id}')
|
||||
else: # tiktok
|
||||
video_concept = await generator.create_video_concept(page_data['analysis'], ad_id)
|
||||
prompt = generator.create_ad_prompt(page_data['analysis'], video_concept)
|
||||
ad_content = await generator.generate_ad_video(prompt, page_data.get('screenshot_path'), ad_id)
|
||||
|
||||
result_path = await generator.save_results(ad_content, prompt, page_data['analysis'], url, page_data['timestamp'])
|
||||
|
||||
if mode == 'instagram':
|
||||
print(f'🎨 Generated image ad #{ad_id}: {result_path}')
|
||||
else:
|
||||
print(f'🎬 Generated video ad #{ad_id}: {result_path}')
|
||||
|
||||
open_file(result_path)
|
||||
|
||||
return result_path
|
||||
|
||||
except Exception as e:
|
||||
print(f'❌ Error for ad #{ad_id}: {e}')
|
||||
raise
|
||||
finally:
|
||||
if ad_id == 1 and page_data.get('screenshot_path'):
|
||||
print(f'📸 Page screenshot: {page_data["screenshot_path"]}')
|
||||
|
||||
|
||||
async def generate_single_ad(page_data: dict, mode: str, ad_id: int):
|
||||
"""Generate a single ad using pre-analyzed page data"""
|
||||
generator = AdGenerator(mode=mode)
|
||||
|
||||
try:
|
||||
if mode == 'instagram':
|
||||
prompt = generator.create_ad_prompt(page_data['analysis'])
|
||||
ad_content = await generator.generate_ad_image(prompt, page_data.get('screenshot_path'))
|
||||
if ad_content is None:
|
||||
raise RuntimeError(f'Ad image generation failed for ad #{ad_id}')
|
||||
else: # tiktok
|
||||
video_concept = await generator.create_video_concept(page_data['analysis'], ad_id)
|
||||
prompt = generator.create_ad_prompt(page_data['analysis'], video_concept)
|
||||
ad_content = await generator.generate_ad_video(prompt, page_data.get('screenshot_path'), ad_id)
|
||||
|
||||
# Create unique timestamp for each ad
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + f'_{ad_id}'
|
||||
result_path = await generator.save_results(ad_content, prompt, page_data['analysis'], page_data['url'], timestamp)
|
||||
|
||||
if mode == 'instagram':
|
||||
print(f'🎨 Generated image ad #{ad_id}: {result_path}')
|
||||
else:
|
||||
print(f'🎬 Generated video ad #{ad_id}: {result_path}')
|
||||
|
||||
return result_path
|
||||
|
||||
except Exception as e:
|
||||
print(f'❌ Error for ad #{ad_id}: {e}')
|
||||
raise
|
||||
|
||||
|
||||
async def create_multiple_ads(url: str, debug: bool = False, mode: str = 'instagram', count: int = 1):
|
||||
"""Generate multiple ads in parallel using asyncio concurrency"""
|
||||
if count == 1:
|
||||
return await create_ad_from_landing_page(url, debug, mode, 1)
|
||||
|
||||
print(f'🚀 Analyzing {url} for {count} {mode} ads...')
|
||||
|
||||
analyzer = LandingPageAnalyzer(debug=debug)
|
||||
page_data = await analyzer.analyze_landing_page(url, mode=mode)
|
||||
|
||||
print(f'🎯 Generating {count} {mode} ads in parallel...')
|
||||
|
||||
tasks = []
|
||||
for i in range(count):
|
||||
task = create_task_with_error_handling(generate_single_ad(page_data, mode, i + 1), name=f'generate_ad_{i + 1}')
|
||||
tasks.append(task)
|
||||
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
successful = []
|
||||
failed = []
|
||||
|
||||
for i, result in enumerate(results):
|
||||
if isinstance(result, Exception):
|
||||
failed.append(i + 1)
|
||||
else:
|
||||
successful.append(result)
|
||||
|
||||
print(f'\n✅ Successfully generated {len(successful)}/{count} ads')
|
||||
if failed:
|
||||
print(f'❌ Failed ads: {failed}')
|
||||
|
||||
if page_data.get('screenshot_path'):
|
||||
print(f'📸 Page screenshot: {page_data["screenshot_path"]}')
|
||||
|
||||
for ad_path in successful:
|
||||
open_file(ad_path)
|
||||
|
||||
return successful
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
url = args.url
|
||||
if not url:
|
||||
url = input('🔗 Enter URL: ').strip() or 'https://www.apple.com/iphone-17-pro/'
|
||||
|
||||
if args.tiktok:
|
||||
mode = 'tiktok'
|
||||
else:
|
||||
mode = 'instagram'
|
||||
|
||||
asyncio.run(create_multiple_ads(url, debug=args.debug, mode=mode, count=args.count))
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
@@ -0,0 +1,114 @@
|
||||
# Msg-Use
|
||||
|
||||
AI-powered message scheduler using browser agents and Gemini. Schedule personalized messages in natural language and let AI compose them intelligently.
|
||||
|
||||
[!WARNING]
|
||||
This demo requires browser-use v0.7.7+.
|
||||
|
||||
https://browser-use.github.io/media/demos/msg_use.mp4
|
||||
|
||||
## Features
|
||||
|
||||
1. Agent logs into WhatsApp Web automatically
|
||||
2. Parses natural language scheduling instructions
|
||||
3. Composes personalized messages using AI
|
||||
4. Schedules messages for future delivery or sends immediately
|
||||
5. Persistent session (no repeated QR scanning)
|
||||
|
||||
## Setup
|
||||
|
||||
Make sure the newest version of browser-use is installed:
|
||||
```bash
|
||||
pip install -U browser-use
|
||||
```
|
||||
|
||||
Export your Gemini API key, get it from: [Google AI Studio](https://makersuite.google.com/app/apikey)
|
||||
```
|
||||
export GOOGLE_API_KEY='your-gemini-api-key-here'
|
||||
```
|
||||
|
||||
Clone the repo and cd into the app folder
|
||||
```bash
|
||||
git clone https://github.com/browser-use/browser-use.git
|
||||
cd browser-use/examples/apps/msg-use
|
||||
```
|
||||
|
||||
## Initial Login
|
||||
|
||||
First-time setup requires QR code scanning:
|
||||
```bash
|
||||
python login.py
|
||||
```
|
||||
- Scan QR code when browser opens
|
||||
- Session will be saved for future use
|
||||
|
||||
## Normal Usage
|
||||
|
||||
1. **Edit your schedule** in `messages.txt`:
|
||||
```
|
||||
- Send "Hi" to Magnus on the 09.09 at 18:15
|
||||
- Tell hinge date (Camila) at 20:00 that I miss her
|
||||
- Remind mom to pick up the car next tuesday
|
||||
```
|
||||
|
||||
2. **Test mode** - See what will be sent:
|
||||
```bash
|
||||
python scheduler.py --test
|
||||
```
|
||||
|
||||
3. **Run scheduler**:
|
||||
```bash
|
||||
python scheduler.py
|
||||
|
||||
# Debug Mode - See the browser in action
|
||||
python scheduler.py --debug
|
||||
|
||||
# Auto Mode - Respond to unread messages every ~30 minutes
|
||||
python scheduler.py --auto
|
||||
```
|
||||
|
||||
## Programmatic Usage
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from scheduler import schedule_messages
|
||||
|
||||
async def main():
|
||||
messages = [
|
||||
"Send hello to John at 15:30",
|
||||
"Remind Sarah about meeting tomorrow at 9am"
|
||||
]
|
||||
await schedule_messages(messages, debug=False)
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
Example scheduling output:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"contact": "Magnus",
|
||||
"original_message": "Hi",
|
||||
"composed_message": "Hi",
|
||||
"scheduled_time": "2025-06-13 18:15"
|
||||
},
|
||||
{
|
||||
"contact": "Camila",
|
||||
"original_message": "I miss her",
|
||||
"composed_message": "I miss you ❤️",
|
||||
"scheduled_time": "2025-06-14 20:00"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Files
|
||||
|
||||
- `scheduler.py` - Main scheduler script
|
||||
- `login.py` - One-time login setup
|
||||
- `messages.txt` - Your message schedule in natural language
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,71 @@
|
||||
import asyncio
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from browser_use import Agent, BrowserSession
|
||||
from browser_use.llm.google import ChatGoogle
|
||||
|
||||
GOOGLE_API_KEY = os.getenv('GOOGLE_API_KEY')
|
||||
|
||||
# Browser profile directory for persistence (same as main script)
|
||||
USER_DATA_DIR = Path.home() / '.config' / 'whatsapp_scheduler' / 'browser_profile'
|
||||
USER_DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Storage state file for cookies
|
||||
STORAGE_STATE_FILE = USER_DATA_DIR / 'storage_state.json'
|
||||
|
||||
|
||||
async def login_to_whatsapp():
|
||||
"""Open WhatsApp Web and wait for user to scan QR code"""
|
||||
if not GOOGLE_API_KEY:
|
||||
print('❌ Error: GOOGLE_API_KEY environment variable is required')
|
||||
print("Please set it with: export GOOGLE_API_KEY='your-api-key-here'")
|
||||
return
|
||||
|
||||
print('WhatsApp Login Setup')
|
||||
print('=' * 50)
|
||||
print(f'Browser profile directory: {USER_DATA_DIR}')
|
||||
print(f'Storage state file: {STORAGE_STATE_FILE}')
|
||||
print('=' * 50)
|
||||
|
||||
try:
|
||||
llm = ChatGoogle(model='gemini-2.0-flash-exp', temperature=0.3, api_key=GOOGLE_API_KEY)
|
||||
|
||||
task = """
|
||||
You are helping a user log into WhatsApp Web. Follow these steps:
|
||||
|
||||
1. Navigate to https://web.whatsapp.com
|
||||
2. Wait for the page to load completely
|
||||
3. If you see a QR code, tell the user to scan it with their phone
|
||||
4. Wait patiently for the login to complete
|
||||
5. Once you see the WhatsApp chat interface, confirm successful login
|
||||
|
||||
Take your time and be patient with page loads.
|
||||
"""
|
||||
|
||||
print('\nOpening WhatsApp Web...')
|
||||
print('Please scan the QR code when it appears.\n')
|
||||
|
||||
browser_session = BrowserSession(
|
||||
headless=False, # Show browser
|
||||
user_data_dir=str(USER_DATA_DIR), # Use persistent profile directory
|
||||
storage_state=str(STORAGE_STATE_FILE) if STORAGE_STATE_FILE.exists() else None, # Use saved cookies/session
|
||||
)
|
||||
|
||||
agent = Agent(task=task, llm=llm, browser_session=browser_session)
|
||||
|
||||
result = await agent.run()
|
||||
|
||||
print('\n✅ Login completed!')
|
||||
print("Note: For now, you'll need to scan the QR code each time.")
|
||||
print("We'll improve session persistence in a future update.")
|
||||
print('\nPress Enter to close the browser...')
|
||||
input()
|
||||
|
||||
except Exception as e:
|
||||
print(f'\n❌ Error during login: {str(e)}')
|
||||
print('Please try again.')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(login_to_whatsapp())
|
||||
Executable
+286
@@ -0,0 +1,286 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
WhatsApp Message Scheduler - Send scheduled messages via WhatsApp Web
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def setup_environment(debug: bool):
|
||||
if not debug:
|
||||
os.environ['BROWSER_USE_SETUP_LOGGING'] = 'false'
|
||||
os.environ['BROWSER_USE_LOGGING_LEVEL'] = 'critical'
|
||||
logging.getLogger().setLevel(logging.CRITICAL)
|
||||
else:
|
||||
os.environ['BROWSER_USE_SETUP_LOGGING'] = 'true'
|
||||
os.environ['BROWSER_USE_LOGGING_LEVEL'] = 'info'
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(description='WhatsApp Scheduler - Send scheduled messages via WhatsApp Web')
|
||||
parser.add_argument('--debug', action='store_true', help='Debug mode: show browser and verbose logs')
|
||||
parser.add_argument('--test', action='store_true', help='Test mode: show what messages would be sent without sending them')
|
||||
parser.add_argument('--auto', action='store_true', help='Auto mode: respond to unread messages every 30 minutes')
|
||||
args = parser.parse_args()
|
||||
setup_environment(args.debug)
|
||||
|
||||
from browser_use import Agent, BrowserSession
|
||||
from browser_use.llm.google import ChatGoogle
|
||||
|
||||
GOOGLE_API_KEY = os.getenv('GOOGLE_API_KEY') or os.getenv('GEMINI_API_KEY')
|
||||
|
||||
USER_DATA_DIR = Path.home() / '.config' / 'whatsapp_scheduler' / 'browser_profile'
|
||||
USER_DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
STORAGE_STATE_FILE = USER_DATA_DIR / 'storage_state.json'
|
||||
|
||||
|
||||
async def parse_messages():
|
||||
"""Parse messages.txt and extract scheduling info"""
|
||||
messages_file = Path('messages.txt')
|
||||
if not messages_file.exists():
|
||||
print('❌ messages.txt not found!')
|
||||
return []
|
||||
|
||||
import aiofiles
|
||||
|
||||
async with aiofiles.open(messages_file) as f:
|
||||
content = await f.read()
|
||||
|
||||
llm = ChatGoogle(model='gemini-2.0-flash-exp', temperature=0.1, api_key=GOOGLE_API_KEY)
|
||||
|
||||
now = datetime.now()
|
||||
prompt = f"""
|
||||
Parse these WhatsApp message instructions and extract:
|
||||
1. Contact name (extract just the name, not descriptions)
|
||||
2. Message content (what to send)
|
||||
3. Date and time (when to send)
|
||||
|
||||
Current date/time: {now.strftime('%Y-%m-%d %H:%M')}
|
||||
Today is: {now.strftime('%Y-%m-%d')}
|
||||
Current time is: {now.strftime('%H:%M')}
|
||||
|
||||
Instructions:
|
||||
{content}
|
||||
|
||||
Return ONLY a JSON array with format:
|
||||
[{{"contact": "name", "message": "text", "datetime": "YYYY-MM-DD HH:MM"}}]
|
||||
|
||||
CRITICAL: Transform instructions into actual messages:
|
||||
|
||||
QUOTED TEXT → Use exactly as-is:
|
||||
- Text in "quotes" becomes the exact message
|
||||
|
||||
UNQUOTED INSTRUCTIONS → Generate actual content:
|
||||
- If it's an instruction to write something → write the actual thing
|
||||
- If it's an instruction to tell someone something → write what to tell them
|
||||
- If it's an instruction to remind someone → write the actual reminder
|
||||
- For multi-line content like poems: use single line with spacing, not line breaks
|
||||
|
||||
DO NOT copy the instruction - create the actual message content!
|
||||
|
||||
Time Rules:
|
||||
- If only time given (like "at 15:30"), use TODAY
|
||||
- If no date specified, assume TODAY
|
||||
- If no year given, use current year
|
||||
- Default time is 9:00 if not specified
|
||||
- Extract names from parentheses: "hinge date (Camila)" → "Camila"
|
||||
- "tomorrow" means {(now + timedelta(days=1)).strftime('%Y-%m-%d')}
|
||||
- "next tuesday" or similar means the next occurrence of that day
|
||||
"""
|
||||
|
||||
from browser_use.llm.messages import UserMessage
|
||||
|
||||
response = await llm.ainvoke([UserMessage(content=prompt)])
|
||||
response_text = response.completion if hasattr(response, 'completion') else str(response)
|
||||
|
||||
# Extract JSON
|
||||
json_match = re.search(r'\[.*?\]', response_text, re.DOTALL)
|
||||
if json_match:
|
||||
try:
|
||||
messages = json.loads(json_match.group())
|
||||
for msg in messages:
|
||||
if 'message' in msg:
|
||||
msg['message'] = re.sub(r'\n+', ' • ', msg['message'])
|
||||
msg['message'] = re.sub(r'\s+', ' ', msg['message']).strip()
|
||||
return messages
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
return []
|
||||
|
||||
|
||||
async def send_message(contact, message):
|
||||
"""Send a WhatsApp message"""
|
||||
print(f'\n📱 Sending to {contact}: {message}')
|
||||
|
||||
llm = ChatGoogle(model='gemini-2.0-flash-exp', temperature=0.3, api_key=GOOGLE_API_KEY)
|
||||
|
||||
task = f"""
|
||||
Send WhatsApp message:
|
||||
1. Go to https://web.whatsapp.com
|
||||
2. Search for contact: {contact}
|
||||
3. Click on the contact
|
||||
4. Type message: {message}
|
||||
5. Press Enter to send
|
||||
6. Confirm sent
|
||||
"""
|
||||
|
||||
browser = BrowserSession(
|
||||
headless=not args.debug, # headless=False only when debug=True
|
||||
user_data_dir=str(USER_DATA_DIR),
|
||||
storage_state=str(STORAGE_STATE_FILE) if STORAGE_STATE_FILE.exists() else None,
|
||||
)
|
||||
|
||||
agent = Agent(task=task, llm=llm, browser_session=browser)
|
||||
await agent.run()
|
||||
print(f'✅ Sent to {contact}')
|
||||
|
||||
|
||||
async def auto_respond_to_unread():
|
||||
"""Click unread tab and respond to messages"""
|
||||
print('\nAuto-responding to unread messages...')
|
||||
|
||||
llm = ChatGoogle(model='gemini-2.0-flash-exp', temperature=0.3, api_key=GOOGLE_API_KEY)
|
||||
|
||||
task = """
|
||||
1. Go to https://web.whatsapp.com
|
||||
2. Wait for page to load
|
||||
3. Click on the "Unread" filter tab
|
||||
4. If there are unread messages:
|
||||
- Click on each unread chat
|
||||
- Read the last message
|
||||
- Generate and send a friendly, contextual response
|
||||
- Move to next unread chat
|
||||
5. Report how many messages were responded to
|
||||
"""
|
||||
|
||||
browser = BrowserSession(
|
||||
headless=not args.debug,
|
||||
user_data_dir=str(USER_DATA_DIR),
|
||||
storage_state=str(STORAGE_STATE_FILE) if STORAGE_STATE_FILE.exists() else None,
|
||||
)
|
||||
|
||||
agent = Agent(task=task, llm=llm, browser_session=browser)
|
||||
result = await agent.run()
|
||||
print('✅ Auto-response complete')
|
||||
return result
|
||||
|
||||
|
||||
async def main():
|
||||
if not GOOGLE_API_KEY:
|
||||
print('❌ Set GOOGLE_API_KEY or GEMINI_API_KEY environment variable')
|
||||
return
|
||||
|
||||
print('WhatsApp Scheduler')
|
||||
print(f'Profile: {USER_DATA_DIR}')
|
||||
print()
|
||||
|
||||
# Auto mode - respond to unread messages periodically
|
||||
if args.auto:
|
||||
print('AUTO MODE - Responding to unread messages every ~30 minutes')
|
||||
print('Press Ctrl+C to stop.\n')
|
||||
|
||||
while True:
|
||||
try:
|
||||
await auto_respond_to_unread()
|
||||
|
||||
# Wait 30 minutes +/- 5 minutes randomly
|
||||
wait_minutes = 30 + random.randint(-5, 5)
|
||||
print(f'\n⏰ Next check in {wait_minutes} minutes...')
|
||||
await asyncio.sleep(wait_minutes * 60)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print('\n\nAuto mode stopped by user')
|
||||
break
|
||||
except Exception as e:
|
||||
print(f'\n❌ Error in auto mode: {e}')
|
||||
print('Waiting 5 minutes before retry...')
|
||||
await asyncio.sleep(300)
|
||||
return
|
||||
|
||||
# Parse messages
|
||||
print('Parsing messages.txt...')
|
||||
messages = await parse_messages()
|
||||
|
||||
if not messages:
|
||||
print('No messages found')
|
||||
return
|
||||
|
||||
print(f'\nFound {len(messages)} messages:')
|
||||
for msg in messages:
|
||||
print(f' • {msg["datetime"]}: {msg["message"][:30]}... to {msg["contact"]}')
|
||||
|
||||
now = datetime.now()
|
||||
immediate = []
|
||||
future = []
|
||||
|
||||
for msg in messages:
|
||||
msg_time = datetime.strptime(msg['datetime'], '%Y-%m-%d %H:%M')
|
||||
if msg_time <= now:
|
||||
immediate.append(msg)
|
||||
else:
|
||||
future.append(msg)
|
||||
|
||||
if args.test:
|
||||
print('\n=== TEST MODE - Preview ===')
|
||||
if immediate:
|
||||
print(f'\nWould send {len(immediate)} past-due messages NOW:')
|
||||
for msg in immediate:
|
||||
print(f' 📱 To {msg["contact"]}: {msg["message"]}')
|
||||
if future:
|
||||
print(f'\nWould monitor {len(future)} future messages:')
|
||||
for msg in future:
|
||||
print(f' ⏰ {msg["datetime"]}: To {msg["contact"]}: {msg["message"]}')
|
||||
print('\nTest mode complete. No messages sent.')
|
||||
return
|
||||
|
||||
if immediate:
|
||||
print(f'\nSending {len(immediate)} past-due messages NOW...')
|
||||
for msg in immediate:
|
||||
await send_message(msg['contact'], msg['message'])
|
||||
|
||||
if future:
|
||||
print(f'\n⏰ Monitoring {len(future)} future messages...')
|
||||
print('Press Ctrl+C to stop.\n')
|
||||
|
||||
last_status = None
|
||||
|
||||
while future:
|
||||
now = datetime.now()
|
||||
due = []
|
||||
remaining = []
|
||||
|
||||
for msg in future:
|
||||
msg_time = datetime.strptime(msg['datetime'], '%Y-%m-%d %H:%M')
|
||||
if msg_time <= now:
|
||||
due.append(msg)
|
||||
else:
|
||||
remaining.append(msg)
|
||||
|
||||
for msg in due:
|
||||
print(f'\n⏰ Time reached for {msg["contact"]}')
|
||||
await send_message(msg['contact'], msg['message'])
|
||||
|
||||
future = remaining
|
||||
|
||||
if future:
|
||||
next_msg = min(future, key=lambda x: datetime.strptime(x['datetime'], '%Y-%m-%d %H:%M'))
|
||||
current_status = f'Next: {next_msg["datetime"]} to {next_msg["contact"]}'
|
||||
|
||||
if current_status != last_status:
|
||||
print(current_status)
|
||||
last_status = current_status
|
||||
|
||||
await asyncio.sleep(30) # Check every 30 seconds
|
||||
|
||||
print('\n✅ All messages processed!')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,87 @@
|
||||
# News-Use
|
||||
|
||||
Automatically monitor news websites and extract the latest articles with sentiment analysis using browser agents and Google Gemini.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> This demo requires browser-use v0.7.7+.
|
||||
|
||||
https://github.com/user-attachments/assets/698757ca-8827-41f3-98e5-c235d6eef69f
|
||||
|
||||
## Features
|
||||
|
||||
1. Agent visits any news website
|
||||
2. Finds and clicks the most recent headline article
|
||||
3. Extracts title, URL, posting time, and content
|
||||
4. Generates short/long summaries with sentiment analysis
|
||||
5. Persistent deduplication across restarts
|
||||
|
||||
## Setup
|
||||
|
||||
Make sure the newest version of browser-use is installed:
|
||||
```bash
|
||||
pip install -U browser-use
|
||||
```
|
||||
|
||||
Export your Gemini API key, get it from: [Google AI Studio](https://makersuite.google.com/app/apikey)
|
||||
```
|
||||
export GEMINI_API_KEY='your-google-api-key-here'
|
||||
```
|
||||
|
||||
Clone the repo and cd into the app folder
|
||||
```bash
|
||||
git clone https://github.com/browser-use/browser-use.git
|
||||
cd browser-use/examples/apps/news-use
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# One-time extraction - Get the latest article and exit
|
||||
python news_monitor.py --once
|
||||
|
||||
# Continuous monitoring - Check every 5 minutes (default)
|
||||
python news_monitor.py
|
||||
|
||||
# Custom interval - Check every 60 seconds
|
||||
python news_monitor.py --interval 60
|
||||
|
||||
# Different news site
|
||||
python news_monitor.py --url https://techcrunch.com
|
||||
|
||||
# Debug mode - See browser in action with verbose output
|
||||
python news_monitor.py --once --debug
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
Articles are displayed with timestamp, sentiment emoji, and summary:
|
||||
```
|
||||
[2025-09-11 02:49:21] - 🟢 - Klarna's IPO raises $1.4B, benefiting existing investors
|
||||
```
|
||||
|
||||
Sentiment indicators:
|
||||
- 🟢 Positive
|
||||
- 🟡 Neutral
|
||||
- 🔴 Negative
|
||||
|
||||
## Programmatic Usage
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from news_monitor import extract_latest_article
|
||||
|
||||
async def main():
|
||||
result = await extract_latest_article(
|
||||
site_url="https://techcrunch.com",
|
||||
debug=False
|
||||
)
|
||||
if result["status"] == "success":
|
||||
article = result["data"]
|
||||
print(f"Latest: {article['title']}")
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
Executable
+303
@@ -0,0 +1,303 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
News monitoring agent with browser-use + Gemini Flash.
|
||||
Automatically extracts and analyzes the latest articles from any news website.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
from dateutil import parser as dtparser
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
def setup_environment(debug: bool):
|
||||
if not debug:
|
||||
os.environ['BROWSER_USE_SETUP_LOGGING'] = 'false'
|
||||
os.environ['BROWSER_USE_LOGGING_LEVEL'] = 'critical'
|
||||
logging.getLogger().setLevel(logging.CRITICAL)
|
||||
else:
|
||||
os.environ['BROWSER_USE_SETUP_LOGGING'] = 'true'
|
||||
os.environ['BROWSER_USE_LOGGING_LEVEL'] = 'info'
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(description='News extractor using Browser-Use + Gemini')
|
||||
parser.add_argument('--url', default='https://www.techcrunch.com', help='News site root URL')
|
||||
parser.add_argument('--interval', type=int, default=300, help='Seconds between checks in monitor mode')
|
||||
parser.add_argument('--once', action='store_true', help='Run a single extraction and exit')
|
||||
parser.add_argument('--output', default='news_data.json', help='Path to JSON file where articles are stored')
|
||||
parser.add_argument('--debug', action='store_true', help='Verbose console output and non-headless browser')
|
||||
args = parser.parse_args()
|
||||
|
||||
setup_environment(args.debug)
|
||||
|
||||
from browser_use import Agent, BrowserSession, ChatGoogle
|
||||
|
||||
GEMINI_API_KEY = os.getenv('GOOGLE_API_KEY') or 'xxxx'
|
||||
|
||||
if GEMINI_API_KEY == 'xxxx':
|
||||
print('⚠️ WARNING: Please set GOOGLE_API_KEY environment variable')
|
||||
print(' You can get an API key at: https://makersuite.google.com/app/apikey')
|
||||
print(" Then run: export GEMINI_API_KEY='your-api-key-here'")
|
||||
print()
|
||||
|
||||
|
||||
class NewsArticle(BaseModel):
|
||||
title: str
|
||||
url: str
|
||||
posting_time: str
|
||||
short_summary: str
|
||||
long_summary: str
|
||||
sentiment: Literal['positive', 'neutral', 'negative']
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Core extractor
|
||||
# ---------------------------------------------------------
|
||||
|
||||
|
||||
async def extract_latest_article(site_url: str, debug: bool = False) -> dict:
|
||||
"""Open site_url, navigate to the newest article and return structured JSON."""
|
||||
|
||||
prompt = (
|
||||
f'Navigate to {site_url} and find the most recent headline article (usually at the top). '
|
||||
f'Click on it to open the full article page. Once loaded, scroll & extract ALL required information: '
|
||||
f'1. title: The article headline '
|
||||
f'2. url: The full URL of the article page '
|
||||
f'3. posting_time: The publication date/time as shown on the page '
|
||||
f"4. short_summary: A 10-word overview of the article's content "
|
||||
f'5. long_summary: A 100-word detailed summary of the article '
|
||||
f"6. sentiment: Classify as 'positive', 'neutral', or 'negative' based on the article tone. "
|
||||
f'When done, call the done action with success=True and put ALL extracted data in the text field '
|
||||
f'as valid JSON in this exact format: '
|
||||
f'{{"title": "...", "url": "...", "posting_time": "...", "short_summary": "...", "long_summary": "...", "sentiment": "positive|neutral|negative"}}'
|
||||
)
|
||||
|
||||
llm = ChatGoogle(model='gemini-2.0-flash', temperature=0.1, api_key=GEMINI_API_KEY)
|
||||
browser_session = BrowserSession(headless=not debug)
|
||||
|
||||
agent = Agent(task=prompt, llm=llm, browser_session=browser_session, use_vision=False)
|
||||
|
||||
if debug:
|
||||
print(f'[DEBUG] Starting extraction from {site_url}')
|
||||
start = time.time()
|
||||
|
||||
result = await agent.run(max_steps=25)
|
||||
|
||||
raw = result.final_result() if result else None
|
||||
if debug:
|
||||
print(f'[DEBUG] Raw result type: {type(raw)}')
|
||||
print(f'[DEBUG] Raw result: {raw[:500] if isinstance(raw, str) else raw}')
|
||||
print(f'[DEBUG] Extraction time: {time.time() - start:.2f}s')
|
||||
|
||||
if isinstance(raw, dict):
|
||||
return {'status': 'success', 'data': raw}
|
||||
|
||||
text = str(raw).strip() if raw else ''
|
||||
|
||||
if '<json>' in text and '</json>' in text:
|
||||
text = text.split('<json>', 1)[1].split('</json>', 1)[0].strip()
|
||||
|
||||
if text.lower().startswith('here is'):
|
||||
brace = text.find('{')
|
||||
if brace != -1:
|
||||
text = text[brace:]
|
||||
|
||||
if text.startswith('```'):
|
||||
text = text.lstrip('`\n ')
|
||||
if text.lower().startswith('json'):
|
||||
text = text[4:].lstrip()
|
||||
|
||||
def _escape_newlines(src: str) -> str:
|
||||
out, in_str, esc = [], False, False
|
||||
for ch in src:
|
||||
if in_str:
|
||||
if esc:
|
||||
esc = False
|
||||
elif ch == '\\':
|
||||
esc = True
|
||||
elif ch == '"':
|
||||
in_str = False
|
||||
elif ch == '\n':
|
||||
out.append('\\n')
|
||||
continue
|
||||
elif ch == '\r':
|
||||
continue
|
||||
else:
|
||||
if ch == '"':
|
||||
in_str = True
|
||||
out.append(ch)
|
||||
return ''.join(out)
|
||||
|
||||
cleaned = _escape_newlines(text)
|
||||
|
||||
def _try_parse(txt: str):
|
||||
try:
|
||||
return json.loads(txt)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
data = _try_parse(cleaned)
|
||||
|
||||
# Fallback: grab first balanced JSON object
|
||||
if data is None:
|
||||
brace = 0
|
||||
start = None
|
||||
for i, ch in enumerate(text):
|
||||
if ch == '{':
|
||||
if brace == 0:
|
||||
start = i
|
||||
brace += 1
|
||||
elif ch == '}':
|
||||
brace -= 1
|
||||
if brace == 0 and start is not None:
|
||||
candidate = _escape_newlines(text[start : i + 1])
|
||||
data = _try_parse(candidate)
|
||||
if data is not None:
|
||||
break
|
||||
|
||||
if isinstance(data, dict):
|
||||
return {'status': 'success', 'data': data}
|
||||
return {'status': 'error', 'error': f'JSON parse failed. Raw head: {text[:200]}'}
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Persistence helpers
|
||||
# ---------------------------------------------------------
|
||||
|
||||
|
||||
def load_seen_hashes(file_path: str = 'news_data.json') -> set:
|
||||
"""Load already-saved article URL hashes from disk for dedup across restarts."""
|
||||
if not os.path.exists(file_path):
|
||||
return set()
|
||||
try:
|
||||
with open(file_path) as f:
|
||||
items = json.load(f)
|
||||
return {entry['hash'] for entry in items if 'hash' in entry}
|
||||
except Exception:
|
||||
return set()
|
||||
|
||||
|
||||
def save_article(article: dict, file_path: str = 'news_data.json'):
|
||||
"""Append article to disk with a hash for future dedup."""
|
||||
payload = {
|
||||
'hash': hashlib.md5(article['url'].encode()).hexdigest(),
|
||||
'pulled_at': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
|
||||
'data': article,
|
||||
}
|
||||
|
||||
existing = []
|
||||
if os.path.exists(file_path):
|
||||
try:
|
||||
with open(file_path) as f:
|
||||
existing = json.load(f)
|
||||
except Exception:
|
||||
existing = []
|
||||
|
||||
existing.append(payload)
|
||||
# Keep last 100
|
||||
existing = existing[-100:]
|
||||
|
||||
with open(file_path, 'w') as f:
|
||||
json.dump(existing, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# CLI functions
|
||||
# ---------------------------------------------------------
|
||||
|
||||
|
||||
def _fmt(ts_raw: str) -> str:
|
||||
"""Format timestamp string"""
|
||||
try:
|
||||
return dtparser.parse(ts_raw).strftime('%Y-%m-%d %H:%M:%S')
|
||||
except Exception:
|
||||
return datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
|
||||
async def run_once(url: str, output_path: str, debug: bool):
|
||||
"""Run a single extraction and exit"""
|
||||
res = await extract_latest_article(url, debug)
|
||||
|
||||
if res['status'] == 'success':
|
||||
art = res['data']
|
||||
url_val = art.get('url', '')
|
||||
hash_ = hashlib.md5(url_val.encode()).hexdigest() if url_val else None
|
||||
if url_val:
|
||||
save_article(art, output_path)
|
||||
ts = _fmt(art.get('posting_time', ''))
|
||||
sentiment = art.get('sentiment', 'neutral')
|
||||
emoji = {'positive': '🟢', 'negative': '🔴', 'neutral': '🟡'}.get(sentiment, '🟡')
|
||||
summary = art.get('short_summary', art.get('summary', art.get('title', '')))
|
||||
if debug:
|
||||
print(json.dumps(art, ensure_ascii=False, indent=2))
|
||||
print()
|
||||
print(f'[{ts}] - {emoji} - {summary}')
|
||||
if not debug:
|
||||
print() # Only add spacing in non-debug mode
|
||||
return hash_
|
||||
else:
|
||||
print(f'Error: {res["error"]}')
|
||||
return None
|
||||
|
||||
|
||||
async def monitor(url: str, interval: int, output_path: str, debug: bool):
|
||||
"""Continuous monitoring mode"""
|
||||
seen = load_seen_hashes(output_path)
|
||||
print(f'Monitoring {url} every {interval}s')
|
||||
print()
|
||||
|
||||
while True:
|
||||
try:
|
||||
res = await extract_latest_article(url, debug)
|
||||
|
||||
if res['status'] == 'success':
|
||||
art = res['data']
|
||||
url_val = art.get('url', '')
|
||||
hash_ = hashlib.md5(url_val.encode()).hexdigest() if url_val else None
|
||||
if hash_ and hash_ not in seen:
|
||||
seen.add(hash_)
|
||||
ts = _fmt(art.get('posting_time', ''))
|
||||
sentiment = art.get('sentiment', 'neutral')
|
||||
emoji = {'positive': '🟢', 'negative': '🔴', 'neutral': '🟡'}.get(sentiment, '🟡')
|
||||
summary = art.get('short_summary', art.get('title', ''))
|
||||
save_article(art, output_path)
|
||||
if debug:
|
||||
print(json.dumps(art, ensure_ascii=False, indent=2))
|
||||
print(f'[{ts}] - {emoji} - {summary}')
|
||||
if not debug:
|
||||
print() # Add spacing between articles in non-debug mode
|
||||
elif debug:
|
||||
print(f'Error: {res["error"]}')
|
||||
|
||||
except Exception as e:
|
||||
if debug:
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
else:
|
||||
print(f'Unhandled error: {e}')
|
||||
|
||||
await asyncio.sleep(interval)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
if args.once:
|
||||
asyncio.run(run_once(args.url, args.output, args.debug))
|
||||
else:
|
||||
try:
|
||||
asyncio.run(monitor(args.url, args.interval, args.output, args.debug))
|
||||
except KeyboardInterrupt:
|
||||
print('\nStopped by user')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Run the Rust-backed Browser Use Agent.
|
||||
|
||||
Set BROWSER_USE_TERMINAL_BINARY when the terminal binary is not on PATH.
|
||||
Set BU_CDP_URL or BROWSER_USE_CDP_URL to attach to a remote Browser Use cloud browser.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from browser_use.beta import Agent, BrowserSession, ChatBrowserUse
|
||||
|
||||
# from browser_use.beta import ChatOpenAI # ChatOpenAI(model='gpt-5.5')
|
||||
# from browser_use.beta import ChatGoogle # ChatGoogle(model='gemini-3.1-pro-preview')
|
||||
# from browser_use.beta import ChatAnthropic # ChatAnthropic(model='claude-opus-4-8')
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
cdp_url = os.environ.get('BU_CDP_URL') or os.environ.get('BROWSER_USE_CDP_URL')
|
||||
browser_session = BrowserSession(cdp_url=cdp_url) if cdp_url else None
|
||||
task = os.environ.get('BU_TASK', 'Open https://example.com and report the page title.')
|
||||
max_steps = int(os.environ.get('BU_MAX_STEPS', '20'))
|
||||
|
||||
agent = Agent(
|
||||
task=task,
|
||||
llm=ChatBrowserUse(model='openai/gpt-5.5'),
|
||||
# llm=ChatBrowserUse(), # Browser Use's own optimized model (bu-2-0)
|
||||
# llm=ChatOpenAI(model='gpt-5.5'),
|
||||
# llm=ChatGoogle(model='gemini-3.1-pro-preview'),
|
||||
# llm=ChatAnthropic(model='claude-opus-4-8'), # Sonnet also works well.
|
||||
browser_session=browser_session,
|
||||
)
|
||||
history = await agent.run(max_steps=max_steps)
|
||||
print(history.final_result() or '(no final result)')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
Examples of using Browser-Use cloud browser service.
|
||||
|
||||
Prerequisites:
|
||||
1. Set BROWSER_USE_API_KEY environment variable
|
||||
2. Active subscription at https://cloud.browser-use.com
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from browser_use import Agent, Browser, ChatBrowserUse
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
async def basic():
|
||||
"""Simplest usage - just pass cloud params directly."""
|
||||
browser = Browser(use_cloud=True)
|
||||
|
||||
agent = Agent(
|
||||
task='Go to github.com/browser-use/browser-use and tell me the star count',
|
||||
llm=ChatBrowserUse(model='bu-2-0'),
|
||||
browser=browser,
|
||||
)
|
||||
|
||||
result = await agent.run()
|
||||
print(f'Result: {result}')
|
||||
|
||||
|
||||
async def full_config():
|
||||
"""Full cloud configuration with specific profile."""
|
||||
browser = Browser(
|
||||
# cloud_profile_id='21182245-590f-4712-8888-9611651a024c',
|
||||
cloud_proxy_country_code='jp',
|
||||
cloud_timeout=60,
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
task='go and check my ip address and the location',
|
||||
llm=ChatBrowserUse(model='bu-2-0'),
|
||||
browser=browser,
|
||||
)
|
||||
|
||||
result = await agent.run()
|
||||
print(f'Result: {result}')
|
||||
|
||||
|
||||
async def main():
|
||||
try:
|
||||
# await basic()
|
||||
await full_config()
|
||||
except Exception as e:
|
||||
print(f'Error: {e}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
Custom HTTP Headers via a custom Watchdog.
|
||||
|
||||
Creates a custom watchdog that listens to TabCreatedEvent and injects
|
||||
custom HTTP headers into every new tab using Network.setExtraHTTPHeaders.
|
||||
|
||||
Note: The CDP EventRegistry only supports one handler per event method,
|
||||
so registering directly on Target.attachedToTarget would replace the
|
||||
internal SessionManager handler. Using the browser-use event system
|
||||
(TabCreatedEvent) avoids this and fires after the target is fully set up.
|
||||
|
||||
Note: Network.setExtraHTTPHeaders is a full replacement (not additive).
|
||||
|
||||
Verified by navigating to https://httpbin.org/headers in a new tab.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from typing import ClassVar
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from bubus import BaseEvent
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from browser_use import Agent, Browser, ChatBrowserUse
|
||||
from browser_use.browser.events import AgentFocusChangedEvent, TabCreatedEvent
|
||||
from browser_use.browser.watchdog_base import BaseWatchdog
|
||||
|
||||
CUSTOM_HEADERS = {
|
||||
'X-Custom-Auth': 'Bearer my-secret-token',
|
||||
'X-Request-Source': 'browser-use-agent',
|
||||
'X-Trace-Id': 'example-trace-12345',
|
||||
}
|
||||
|
||||
|
||||
class CustomHeadersWatchdog(BaseWatchdog):
|
||||
"""Injects custom HTTP headers on every new tab and focus change.
|
||||
|
||||
Listens to both TabCreatedEvent (new tabs) and AgentFocusChangedEvent
|
||||
(tab switches) because headers are bound to a CDP session, and sessions
|
||||
can be recreated on cross-origin navigations or tab switches.
|
||||
"""
|
||||
|
||||
LISTENS_TO: ClassVar[list[type[BaseEvent]]] = [TabCreatedEvent, AgentFocusChangedEvent]
|
||||
EMITS: ClassVar[list[type[BaseEvent]]] = []
|
||||
|
||||
async def on_TabCreatedEvent(self, event: TabCreatedEvent) -> None:
|
||||
"""Set extra headers when a new tab is created."""
|
||||
try:
|
||||
await self.browser_session.set_extra_headers(CUSTOM_HEADERS, target_id=event.target_id)
|
||||
except Exception as e:
|
||||
self.logger.debug(f'Could not set headers on {event.target_id[:8]}: {e}')
|
||||
|
||||
async def on_AgentFocusChangedEvent(self, event: AgentFocusChangedEvent) -> None:
|
||||
"""Re-apply headers when the agent switches to a different tab."""
|
||||
try:
|
||||
await self.browser_session.set_extra_headers(CUSTOM_HEADERS, target_id=event.target_id)
|
||||
except Exception as e:
|
||||
self.logger.debug(f'Could not set headers on {event.target_id[:8]}: {e}')
|
||||
|
||||
|
||||
async def main():
|
||||
browser = Browser(headless=False)
|
||||
|
||||
# Start the browser so watchdogs are initialized
|
||||
await browser.start()
|
||||
|
||||
# Attach our custom watchdog to the browser session
|
||||
CustomHeadersWatchdog.model_rebuild()
|
||||
headers_watchdog = CustomHeadersWatchdog(event_bus=browser.event_bus, browser_session=browser)
|
||||
headers_watchdog.attach_to_session()
|
||||
|
||||
# The watchdog only fires for tabs created AFTER registration.
|
||||
# To apply headers to an already-existing tab, call set_extra_headers():
|
||||
#
|
||||
# await browser.set_extra_headers(CUSTOM_HEADERS)
|
||||
# await browser.set_extra_headers(CUSTOM_HEADERS, target_id=some_target_id)
|
||||
#
|
||||
# Keep in mind that setExtraHTTPHeaders is a full replacement – each
|
||||
# call overwrites all previously set extra headers on that target.
|
||||
|
||||
# Run the agent – open httpbin.org/headers in a new tab so the
|
||||
# watchdog fires and injects the custom headers.
|
||||
agent = Agent(
|
||||
task=(
|
||||
'Open https://httpbin.org/headers in two different tabs and extract the full JSON response. '
|
||||
'Look for the custom headers X-Custom-Auth, X-Request-Source, and X-Trace-Id in the output and compare the results.'
|
||||
),
|
||||
llm=ChatBrowserUse(model='bu-2-0'),
|
||||
browser=browser,
|
||||
)
|
||||
|
||||
result = await agent.run()
|
||||
print(result.final_result())
|
||||
|
||||
await browser.kill()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,45 @@
|
||||
import asyncio
|
||||
|
||||
from browser_use import Agent, Browser, ChatOpenAI
|
||||
|
||||
# NOTE: This is still experimental, and agents might conflict each other.
|
||||
|
||||
|
||||
async def main():
|
||||
# Create 3 separate browser instances
|
||||
browsers = [
|
||||
Browser(
|
||||
user_data_dir=f'./temp-profile-{i}',
|
||||
headless=False,
|
||||
)
|
||||
for i in range(3)
|
||||
]
|
||||
|
||||
# Create 3 agents with different tasks
|
||||
agents = [
|
||||
Agent(
|
||||
task='Search for "browser automation" on Google',
|
||||
browser=browsers[0],
|
||||
llm=ChatOpenAI(model='gpt-4.1-mini'),
|
||||
),
|
||||
Agent(
|
||||
task='Search for "AI agents" on DuckDuckGo',
|
||||
browser=browsers[1],
|
||||
llm=ChatOpenAI(model='gpt-4.1-mini'),
|
||||
),
|
||||
Agent(
|
||||
task='Visit Wikipedia and search for "web scraping"',
|
||||
browser=browsers[2],
|
||||
llm=ChatOpenAI(model='gpt-4.1-mini'),
|
||||
),
|
||||
]
|
||||
|
||||
# Run all agents in parallel
|
||||
tasks = [agent.run() for agent in agents]
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
print('🎉 All agents completed!')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,376 @@
|
||||
"""
|
||||
Key features:
|
||||
1. Browser-Use and Playwright sharing the same Chrome instance via CDP
|
||||
2. Take actions with Playwright and continue with Browser-Use actions
|
||||
3. Let the agent call Playwright functions like screenshot or click on selectors
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
# Check for required dependencies first - before other imports
|
||||
try:
|
||||
import aiohttp # type: ignore
|
||||
from playwright.async_api import Browser, Page, async_playwright # type: ignore
|
||||
except ImportError as e:
|
||||
print(f'❌ Missing dependencies for this example: {e}')
|
||||
print('This example requires: playwright aiohttp')
|
||||
print('Install with: uv add playwright aiohttp')
|
||||
print('Also run: playwright install chromium')
|
||||
sys.exit(1)
|
||||
|
||||
from browser_use import Agent, BrowserSession, ChatOpenAI, Tools
|
||||
from browser_use.agent.views import ActionResult
|
||||
|
||||
# Global Playwright browser instance - shared between custom actions
|
||||
playwright_browser: Browser | None = None
|
||||
playwright_page: Page | None = None
|
||||
|
||||
|
||||
# Custom action parameter models
|
||||
class PlaywrightFillFormAction(BaseModel):
|
||||
"""Parameters for Playwright form filling action."""
|
||||
|
||||
customer_name: str = Field(..., description='Customer name to fill')
|
||||
phone_number: str = Field(..., description='Phone number to fill')
|
||||
email: str = Field(..., description='Email address to fill')
|
||||
size_option: str = Field(..., description='Size option (small/medium/large)')
|
||||
|
||||
|
||||
class PlaywrightScreenshotAction(BaseModel):
|
||||
"""Parameters for Playwright screenshot action."""
|
||||
|
||||
filename: str = Field(default='playwright_screenshot.png', description='Filename for screenshot')
|
||||
quality: int | None = Field(default=None, description='JPEG quality (1-100), only for .jpg/.jpeg files')
|
||||
|
||||
|
||||
class PlaywrightGetTextAction(BaseModel):
|
||||
"""Parameters for getting text using Playwright selectors."""
|
||||
|
||||
selector: str = Field(..., description='CSS selector to get text from. Use "title" for page title.')
|
||||
|
||||
|
||||
async def start_chrome_with_debug_port(port: int = 9222):
|
||||
"""
|
||||
Start Chrome with remote debugging enabled.
|
||||
Returns the Chrome process.
|
||||
"""
|
||||
# Create temporary directory for Chrome user data
|
||||
user_data_dir = tempfile.mkdtemp(prefix='chrome_cdp_')
|
||||
|
||||
# Chrome launch command
|
||||
chrome_paths = [
|
||||
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', # macOS
|
||||
'/usr/bin/google-chrome', # Linux
|
||||
'/usr/bin/chromium-browser', # Linux Chromium
|
||||
'chrome', # Windows/PATH
|
||||
'chromium', # Generic
|
||||
]
|
||||
|
||||
chrome_exe = None
|
||||
for path in chrome_paths:
|
||||
if os.path.exists(path) or path in ['chrome', 'chromium']:
|
||||
try:
|
||||
# Test if executable works
|
||||
test_proc = await asyncio.create_subprocess_exec(
|
||||
path, '--version', stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
||||
)
|
||||
await test_proc.wait()
|
||||
chrome_exe = path
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if not chrome_exe:
|
||||
raise RuntimeError('❌ Chrome not found. Please install Chrome or Chromium.')
|
||||
|
||||
# Chrome command arguments
|
||||
cmd = [
|
||||
chrome_exe,
|
||||
f'--remote-debugging-port={port}',
|
||||
f'--user-data-dir={user_data_dir}',
|
||||
'--no-first-run',
|
||||
'--no-default-browser-check',
|
||||
'--disable-extensions',
|
||||
'about:blank', # Start with blank page
|
||||
]
|
||||
|
||||
# Start Chrome process
|
||||
process = await asyncio.create_subprocess_exec(*cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
|
||||
# Wait for Chrome to start and CDP to be ready
|
||||
cdp_ready = False
|
||||
for _ in range(20): # 20 second timeout
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(
|
||||
f'http://localhost:{port}/json/version', timeout=aiohttp.ClientTimeout(total=1)
|
||||
) as response:
|
||||
if response.status == 200:
|
||||
cdp_ready = True
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
await asyncio.sleep(1)
|
||||
|
||||
if not cdp_ready:
|
||||
process.terminate()
|
||||
raise RuntimeError('❌ Chrome failed to start with CDP')
|
||||
|
||||
return process
|
||||
|
||||
|
||||
async def connect_playwright_to_cdp(cdp_url: str):
|
||||
"""
|
||||
Connect Playwright to the same Chrome instance Browser-Use is using.
|
||||
This enables custom actions to use Playwright functions.
|
||||
"""
|
||||
global playwright_browser, playwright_page
|
||||
|
||||
playwright = await async_playwright().start()
|
||||
playwright_browser = await playwright.chromium.connect_over_cdp(cdp_url)
|
||||
|
||||
# Get or create a page
|
||||
if playwright_browser and playwright_browser.contexts and playwright_browser.contexts[0].pages:
|
||||
playwright_page = playwright_browser.contexts[0].pages[0]
|
||||
elif playwright_browser:
|
||||
context = await playwright_browser.new_context()
|
||||
playwright_page = await context.new_page()
|
||||
|
||||
|
||||
# Create custom tools that use Playwright functions
|
||||
tools = Tools()
|
||||
|
||||
|
||||
@tools.registry.action(
|
||||
"Fill out a form using Playwright's precise form filling capabilities. This uses Playwright selectors for reliable form interaction.",
|
||||
param_model=PlaywrightFillFormAction,
|
||||
)
|
||||
async def playwright_fill_form(params: PlaywrightFillFormAction, browser_session: BrowserSession):
|
||||
"""
|
||||
Custom action that uses Playwright to fill forms with high precision.
|
||||
This demonstrates how to create Browser-Use actions that leverage Playwright's capabilities.
|
||||
"""
|
||||
try:
|
||||
if not playwright_page:
|
||||
return ActionResult(error='Playwright not connected. Run setup first.')
|
||||
|
||||
# Filling form with Playwright's precise selectors
|
||||
|
||||
# Wait for form to be ready and fill basic fields
|
||||
await playwright_page.wait_for_selector('input[name="custname"]', timeout=10000)
|
||||
await playwright_page.fill('input[name="custname"]', params.customer_name)
|
||||
await playwright_page.fill('input[name="custtel"]', params.phone_number)
|
||||
await playwright_page.fill('input[name="custemail"]', params.email)
|
||||
|
||||
# Handle size selection - check if it's a select dropdown or radio buttons
|
||||
size_select = playwright_page.locator('select[name="size"]')
|
||||
size_radio = playwright_page.locator(f'input[name="size"][value="{params.size_option}"]')
|
||||
|
||||
if await size_select.count() > 0:
|
||||
# It's a select dropdown
|
||||
await playwright_page.select_option('select[name="size"]', params.size_option)
|
||||
elif await size_radio.count() > 0:
|
||||
# It's radio buttons
|
||||
await playwright_page.check(f'input[name="size"][value="{params.size_option}"]')
|
||||
else:
|
||||
raise ValueError(f'Could not find size input field for value: {params.size_option}')
|
||||
|
||||
# Get form data to verify it was filled
|
||||
form_data = {}
|
||||
form_data['name'] = await playwright_page.input_value('input[name="custname"]')
|
||||
form_data['phone'] = await playwright_page.input_value('input[name="custtel"]')
|
||||
form_data['email'] = await playwright_page.input_value('input[name="custemail"]')
|
||||
|
||||
# Get size value based on input type
|
||||
if await size_select.count() > 0:
|
||||
form_data['size'] = await playwright_page.input_value('select[name="size"]')
|
||||
else:
|
||||
# For radio buttons, find the checked one
|
||||
checked_radio = playwright_page.locator('input[name="size"]:checked')
|
||||
if await checked_radio.count() > 0:
|
||||
form_data['size'] = await checked_radio.get_attribute('value')
|
||||
else:
|
||||
form_data['size'] = 'none selected'
|
||||
|
||||
success_msg = f'✅ Form filled successfully with Playwright: {form_data}'
|
||||
|
||||
return ActionResult(
|
||||
extracted_content=success_msg, include_in_memory=True, long_term_memory=f'Filled form with: {form_data}'
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f'❌ Playwright form filling failed: {str(e)}'
|
||||
return ActionResult(error=error_msg)
|
||||
|
||||
|
||||
@tools.registry.action(
|
||||
"Take a screenshot using Playwright's screenshot capabilities with high quality and precision.",
|
||||
param_model=PlaywrightScreenshotAction,
|
||||
)
|
||||
async def playwright_screenshot(params: PlaywrightScreenshotAction, browser_session: BrowserSession):
|
||||
"""
|
||||
Custom action that uses Playwright's advanced screenshot features.
|
||||
"""
|
||||
try:
|
||||
if not playwright_page:
|
||||
return ActionResult(error='Playwright not connected. Run setup first.')
|
||||
|
||||
# Taking screenshot with Playwright
|
||||
|
||||
# Use Playwright's screenshot with full page capture
|
||||
screenshot_kwargs = {'path': params.filename, 'full_page': True}
|
||||
|
||||
# Add quality parameter only for JPEG files
|
||||
if params.quality is not None and params.filename.lower().endswith(('.jpg', '.jpeg')):
|
||||
screenshot_kwargs['quality'] = params.quality
|
||||
|
||||
await playwright_page.screenshot(**screenshot_kwargs)
|
||||
|
||||
success_msg = f'✅ Screenshot saved as {params.filename} using Playwright'
|
||||
|
||||
return ActionResult(
|
||||
extracted_content=success_msg, include_in_memory=True, long_term_memory=f'Screenshot saved: {params.filename}'
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f'❌ Playwright screenshot failed: {str(e)}'
|
||||
return ActionResult(error=error_msg)
|
||||
|
||||
|
||||
@tools.registry.action(
|
||||
"Extract text from elements using Playwright's powerful CSS selectors and XPath support.", param_model=PlaywrightGetTextAction
|
||||
)
|
||||
async def playwright_get_text(params: PlaywrightGetTextAction, browser_session: BrowserSession):
|
||||
"""
|
||||
Custom action that uses Playwright's advanced text extraction with CSS selectors and XPath.
|
||||
"""
|
||||
try:
|
||||
if not playwright_page:
|
||||
return ActionResult(error='Playwright not connected. Run setup first.')
|
||||
|
||||
# Extracting text with Playwright selectors
|
||||
|
||||
# Handle special selectors
|
||||
if params.selector.lower() == 'title':
|
||||
# Use page.title() for title element
|
||||
text_content = await playwright_page.title()
|
||||
result_data = {
|
||||
'selector': 'title',
|
||||
'text_content': text_content,
|
||||
'inner_text': text_content,
|
||||
'tag_name': 'TITLE',
|
||||
'is_visible': True,
|
||||
}
|
||||
else:
|
||||
# Use Playwright's robust element selection and text extraction
|
||||
element = playwright_page.locator(params.selector).first
|
||||
|
||||
if await element.count() == 0:
|
||||
error_msg = f'❌ No element found with selector: {params.selector}'
|
||||
return ActionResult(error=error_msg)
|
||||
|
||||
text_content = await element.text_content()
|
||||
inner_text = await element.inner_text()
|
||||
|
||||
# Get additional element info
|
||||
tag_name = await element.evaluate('el => el.tagName')
|
||||
is_visible = await element.is_visible()
|
||||
|
||||
result_data = {
|
||||
'selector': params.selector,
|
||||
'text_content': text_content,
|
||||
'inner_text': inner_text,
|
||||
'tag_name': tag_name,
|
||||
'is_visible': is_visible,
|
||||
}
|
||||
|
||||
success_msg = f'✅ Extracted text using Playwright: {result_data}'
|
||||
|
||||
return ActionResult(
|
||||
extracted_content=str(result_data),
|
||||
include_in_memory=True,
|
||||
long_term_memory=f'Extracted from {params.selector}: {result_data["text_content"]}',
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f'❌ Playwright text extraction failed: {str(e)}'
|
||||
return ActionResult(error=error_msg)
|
||||
|
||||
|
||||
async def main():
|
||||
"""
|
||||
Main function demonstrating Browser-Use + Playwright integration with custom actions.
|
||||
"""
|
||||
print('🚀 Advanced Playwright + Browser-Use Integration with Custom Actions')
|
||||
|
||||
chrome_process = None
|
||||
try:
|
||||
# Step 1: Start Chrome with CDP debugging
|
||||
chrome_process = await start_chrome_with_debug_port()
|
||||
cdp_url = 'http://localhost:9222'
|
||||
|
||||
# Step 2: Connect Playwright to the same Chrome instance
|
||||
await connect_playwright_to_cdp(cdp_url)
|
||||
|
||||
# Step 3: Create Browser-Use session connected to same Chrome
|
||||
browser_session = BrowserSession(cdp_url=cdp_url)
|
||||
|
||||
# Step 4: Create AI agent with our custom Playwright-powered tools
|
||||
agent = Agent(
|
||||
task="""
|
||||
Please help me demonstrate the integration between Browser-Use and Playwright:
|
||||
|
||||
1. First, navigate to https://httpbin.org/forms/post
|
||||
2. Use the 'playwright_fill_form' action to fill the form with these details:
|
||||
- Customer name: "Alice Johnson"
|
||||
- Phone: "555-9876"
|
||||
- Email: "alice@demo.com"
|
||||
- Size: "large"
|
||||
3. Take a screenshot using the 'playwright_screenshot' action and save it as "form_demo.png"
|
||||
4. Extract the title of the page using 'playwright_get_text' action with selector "title"
|
||||
5. Finally, submit the form and tell me what happened
|
||||
|
||||
This demonstrates how Browser-Use AI can orchestrate tasks while using Playwright's precise capabilities for specific operations.
|
||||
""",
|
||||
llm=ChatOpenAI(model='gpt-4.1-mini'),
|
||||
tools=tools, # Our custom tools with Playwright actions
|
||||
browser_session=browser_session,
|
||||
)
|
||||
|
||||
print('🎯 Starting AI agent with custom Playwright actions...')
|
||||
|
||||
# Step 5: Run the agent - it will use both Browser-Use actions and our custom Playwright actions
|
||||
result = await agent.run()
|
||||
|
||||
# Keep browser open briefly to see results
|
||||
print(f'✅ Integration demo completed! Result: {result}')
|
||||
await asyncio.sleep(2) # Brief pause to see results
|
||||
|
||||
except Exception as e:
|
||||
print(f'❌ Error: {e}')
|
||||
raise
|
||||
|
||||
finally:
|
||||
# Clean up resources
|
||||
if playwright_browser:
|
||||
await playwright_browser.close()
|
||||
|
||||
if chrome_process:
|
||||
chrome_process.terminate()
|
||||
try:
|
||||
await asyncio.wait_for(chrome_process.wait(), 5)
|
||||
except TimeoutError:
|
||||
chrome_process.kill()
|
||||
|
||||
print('✅ Cleanup complete')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Run the advanced integration demo
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,48 @@
|
||||
"""
|
||||
Connect to your existing Chrome browser so it's logged into your websites
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from browser_use import Agent, Browser, ChatGoogle
|
||||
|
||||
|
||||
def select_chrome_profile() -> str | None:
|
||||
"""Prompt user to select a Chrome profile."""
|
||||
profiles = Browser.list_chrome_profiles()
|
||||
if not profiles:
|
||||
return None
|
||||
|
||||
print('Available Chrome profiles:')
|
||||
for i, p in enumerate(profiles, 1):
|
||||
print(f' {i}. {p["name"]}')
|
||||
|
||||
while True:
|
||||
choice = input(f'\nSelect profile (1-{len(profiles)}): ').strip()
|
||||
if choice.isdigit() and 1 <= int(choice) <= len(profiles):
|
||||
return profiles[int(choice) - 1]['directory']
|
||||
print('Invalid choice, try again.')
|
||||
|
||||
|
||||
async def main():
|
||||
profile = select_chrome_profile()
|
||||
browser = Browser.from_system_chrome(profile_directory=profile)
|
||||
|
||||
agent = Agent(
|
||||
llm=ChatGoogle(model='gemini-3-flash-preview'),
|
||||
task='go to amazon.com and search for pens to draw on whiteboards',
|
||||
browser=browser,
|
||||
)
|
||||
await agent.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
Export cookies and storage state from your real Chrome browser
|
||||
|
||||
This allows you to save your authenticated sessions for later use
|
||||
without needing to connect to the Chrome profile every time
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from browser_use import Browser
|
||||
|
||||
|
||||
def select_chrome_profile() -> str | None:
|
||||
"""Prompt user to select a Chrome profile."""
|
||||
profiles = Browser.list_chrome_profiles()
|
||||
if not profiles:
|
||||
return None
|
||||
|
||||
print('Available Chrome profiles:')
|
||||
for i, p in enumerate(profiles, 1):
|
||||
print(f' {i}. {p["name"]}')
|
||||
|
||||
while True:
|
||||
choice = input(f'\nSelect profile (1-{len(profiles)}): ').strip()
|
||||
if choice.isdigit() and 1 <= int(choice) <= len(profiles):
|
||||
return profiles[int(choice) - 1]['directory']
|
||||
print('Invalid choice, try again.')
|
||||
|
||||
|
||||
async def main():
|
||||
profile = select_chrome_profile()
|
||||
browser = Browser.from_system_chrome(profile_directory=profile)
|
||||
|
||||
await browser.start()
|
||||
await browser.export_storage_state('storage_state.json')
|
||||
await browser.stop()
|
||||
print('Storage state exported to storage_state.json')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,51 @@
|
||||
"""
|
||||
Simple demonstration of the CDP feature.
|
||||
|
||||
To test this locally, follow these steps:
|
||||
1. Find the chrome executable file.
|
||||
2. On mac by default, the chrome is in `/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`
|
||||
3. Add the following argument to the shortcut:
|
||||
`--remote-debugging-port=9222`
|
||||
4. Open a web browser and navigate to `http://localhost:9222/json/version` to verify that the Remote Debugging Protocol (CDP) is running.
|
||||
5. Launch this example.
|
||||
|
||||
Full command Mac:
|
||||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --remote-debugging-port=9222
|
||||
|
||||
@dev You need to set the `OPENAI_API_KEY` environment variable before proceeding.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from browser_use import Agent, Tools
|
||||
from browser_use.browser import BrowserProfile, BrowserSession
|
||||
from browser_use.llm import ChatOpenAI
|
||||
|
||||
browser_session = BrowserSession(browser_profile=BrowserProfile(cdp_url='http://localhost:9222', is_local=True))
|
||||
tools = Tools()
|
||||
|
||||
|
||||
async def main():
|
||||
agent = Agent(
|
||||
task='Visit https://duckduckgo.com and search for "browser-use founders"',
|
||||
llm=ChatOpenAI(model='gpt-4.1-mini'),
|
||||
tools=tools,
|
||||
browser_session=browser_session,
|
||||
)
|
||||
|
||||
await agent.run()
|
||||
await browser_session.kill()
|
||||
|
||||
input('Press Enter to close...')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,188 @@
|
||||
"""
|
||||
Cloud Example 1: Your First Browser Use Cloud Task
|
||||
==================================================
|
||||
|
||||
This example demonstrates the most basic Browser Use Cloud functionality:
|
||||
- Create a simple automation task
|
||||
- Get the task ID
|
||||
- Monitor completion
|
||||
- Retrieve results
|
||||
|
||||
Perfect for first-time cloud users to understand the API basics.
|
||||
|
||||
Cost: ~$0.04 (1 task + 3 steps with GPT-4.1 mini)
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
# Configuration
|
||||
API_KEY = os.getenv('BROWSER_USE_API_KEY')
|
||||
if not API_KEY:
|
||||
raise ValueError(
|
||||
'Please set BROWSER_USE_API_KEY environment variable. You can also create an API key at https://cloud.browser-use.com/new-api-key'
|
||||
)
|
||||
|
||||
BASE_URL = os.getenv('BROWSER_USE_BASE_URL', 'https://api.browser-use.com/api/v1')
|
||||
TIMEOUT = int(os.getenv('BROWSER_USE_TIMEOUT', '30'))
|
||||
HEADERS = {'Authorization': f'Bearer {API_KEY}', 'Content-Type': 'application/json'}
|
||||
|
||||
|
||||
def _request_with_retry(method: str, url: str, **kwargs) -> requests.Response:
|
||||
"""Make HTTP request with timeout and retry logic."""
|
||||
kwargs.setdefault('timeout', TIMEOUT)
|
||||
|
||||
for attempt in range(3):
|
||||
try:
|
||||
response = requests.request(method, url, **kwargs)
|
||||
response.raise_for_status()
|
||||
return response
|
||||
except RequestException as e:
|
||||
if attempt == 2: # Last attempt
|
||||
raise
|
||||
sleep_time = 2**attempt
|
||||
print(f'⚠️ Request failed (attempt {attempt + 1}/3), retrying in {sleep_time}s: {e}')
|
||||
time.sleep(sleep_time)
|
||||
|
||||
# This line should never be reached, but satisfies type checker
|
||||
raise RuntimeError('Unexpected error in retry logic')
|
||||
|
||||
|
||||
def create_task(instructions: str) -> str:
|
||||
"""
|
||||
Create a new browser automation task.
|
||||
|
||||
Args:
|
||||
instructions: Natural language description of what the agent should do
|
||||
|
||||
Returns:
|
||||
task_id: Unique identifier for the created task
|
||||
"""
|
||||
print(f'📝 Creating task: {instructions}')
|
||||
|
||||
payload = {
|
||||
'task': instructions,
|
||||
'llm_model': 'gpt-4.1-mini', # Cost-effective model
|
||||
'max_agent_steps': 10, # Prevent runaway costs
|
||||
'enable_public_share': True, # Enable shareable execution URLs
|
||||
}
|
||||
|
||||
response = _request_with_retry('post', f'{BASE_URL}/run-task', headers=HEADERS, json=payload)
|
||||
|
||||
task_id = response.json()['id']
|
||||
print(f'✅ Task created with ID: {task_id}')
|
||||
return task_id
|
||||
|
||||
|
||||
def get_task_status(task_id: str) -> dict[str, Any]:
|
||||
"""Get the current status of a task."""
|
||||
response = _request_with_retry('get', f'{BASE_URL}/task/{task_id}/status', headers=HEADERS)
|
||||
return response.json()
|
||||
|
||||
|
||||
def get_task_details(task_id: str) -> dict[str, Any]:
|
||||
"""Get full task details including steps and output."""
|
||||
response = _request_with_retry('get', f'{BASE_URL}/task/{task_id}', headers=HEADERS)
|
||||
return response.json()
|
||||
|
||||
|
||||
def wait_for_completion(task_id: str, poll_interval: int = 3) -> dict[str, Any]:
|
||||
"""
|
||||
Wait for task completion and show progress.
|
||||
|
||||
Args:
|
||||
task_id: The task to monitor
|
||||
poll_interval: How often to check status (seconds)
|
||||
|
||||
Returns:
|
||||
Complete task details with output
|
||||
"""
|
||||
print(f'⏳ Monitoring task {task_id}...')
|
||||
|
||||
step_count = 0
|
||||
start_time = time.time()
|
||||
|
||||
while True:
|
||||
details = get_task_details(task_id)
|
||||
status = details['status']
|
||||
current_steps = len(details.get('steps', []))
|
||||
elapsed = time.time() - start_time
|
||||
|
||||
# Clear line and show current progress
|
||||
if current_steps > step_count:
|
||||
step_count = current_steps
|
||||
|
||||
# Build status message
|
||||
if status == 'running':
|
||||
if current_steps > 0:
|
||||
status_msg = f'🔄 Step {current_steps} | ⏱️ {elapsed:.0f}s | 🤖 Agent working...'
|
||||
else:
|
||||
status_msg = f'🤖 Agent starting... | ⏱️ {elapsed:.0f}s'
|
||||
else:
|
||||
status_msg = f'🔄 Step {current_steps} | ⏱️ {elapsed:.0f}s | Status: {status}'
|
||||
|
||||
# Clear line and print status
|
||||
print(f'\r{status_msg:<80}', end='', flush=True)
|
||||
|
||||
# Check if finished
|
||||
if status == 'finished':
|
||||
print(f'\r✅ Task completed successfully! ({current_steps} steps in {elapsed:.1f}s)' + ' ' * 20)
|
||||
return details
|
||||
elif status in ['failed', 'stopped']:
|
||||
print(f'\r❌ Task {status} after {current_steps} steps' + ' ' * 30)
|
||||
return details
|
||||
|
||||
time.sleep(poll_interval)
|
||||
|
||||
|
||||
def main():
|
||||
"""Run a basic cloud automation task."""
|
||||
print('🚀 Browser Use Cloud - Basic Task Example')
|
||||
print('=' * 50)
|
||||
|
||||
# Define a simple search task (using DuckDuckGo to avoid captchas)
|
||||
task_description = (
|
||||
"Go to DuckDuckGo and search for 'browser automation tools'. Tell me the top 3 results with their titles and URLs."
|
||||
)
|
||||
|
||||
try:
|
||||
# Step 1: Create the task
|
||||
task_id = create_task(task_description)
|
||||
|
||||
# Step 2: Wait for completion
|
||||
result = wait_for_completion(task_id)
|
||||
|
||||
# Step 3: Display results
|
||||
print('\n📊 Results:')
|
||||
print('-' * 30)
|
||||
print(f'Status: {result["status"]}')
|
||||
print(f'Steps taken: {len(result.get("steps", []))}')
|
||||
|
||||
if result.get('output'):
|
||||
print(f'Output: {result["output"]}')
|
||||
else:
|
||||
print('No output available')
|
||||
|
||||
# Show share URLs for viewing execution
|
||||
if result.get('live_url'):
|
||||
print(f'\n🔗 Live Preview: {result["live_url"]}')
|
||||
if result.get('public_share_url'):
|
||||
print(f'🌐 Share URL: {result["public_share_url"]}')
|
||||
elif result.get('share_url'):
|
||||
print(f'🌐 Share URL: {result["share_url"]}')
|
||||
|
||||
if not result.get('live_url') and not result.get('public_share_url') and not result.get('share_url'):
|
||||
print("\n💡 Tip: Add 'enable_public_share': True to task payload to get shareable URLs")
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f'❌ API Error: {e}')
|
||||
except Exception as e:
|
||||
print(f'❌ Error: {e}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,265 @@
|
||||
"""
|
||||
Cloud Example 2: Ultra-Fast Mode with Gemini Flash ⚡
|
||||
====================================================
|
||||
|
||||
This example demonstrates the fastest and most cost-effective configuration:
|
||||
- Gemini 2.5 Flash model ($0.01 per step)
|
||||
- No proxy (faster execution, but no captcha solving)
|
||||
- No element highlighting (better performance)
|
||||
- Optimized viewport size
|
||||
- Maximum speed configuration
|
||||
|
||||
Perfect for: Quick content generation, humor tasks, fast web scraping
|
||||
|
||||
Cost: ~$0.03 (1 task + 2-3 steps with Gemini Flash)
|
||||
Speed: 2-3x faster than default configuration
|
||||
Fun Factor: 💯 (Creates hilarious tech commentary)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
# Configuration
|
||||
API_KEY = os.getenv('BROWSER_USE_API_KEY')
|
||||
if not API_KEY:
|
||||
raise ValueError(
|
||||
'Please set BROWSER_USE_API_KEY environment variable. You can also create an API key at https://cloud.browser-use.com/new-api-key'
|
||||
)
|
||||
|
||||
BASE_URL = os.getenv('BROWSER_USE_BASE_URL', 'https://api.browser-use.com/api/v1')
|
||||
TIMEOUT = int(os.getenv('BROWSER_USE_TIMEOUT', '30'))
|
||||
HEADERS = {'Authorization': f'Bearer {API_KEY}', 'Content-Type': 'application/json'}
|
||||
|
||||
|
||||
def _request_with_retry(method: str, url: str, **kwargs) -> requests.Response:
|
||||
"""Make HTTP request with timeout and retry logic."""
|
||||
kwargs.setdefault('timeout', TIMEOUT)
|
||||
|
||||
for attempt in range(3):
|
||||
try:
|
||||
response = requests.request(method, url, **kwargs)
|
||||
response.raise_for_status()
|
||||
return response
|
||||
except RequestException as e:
|
||||
if attempt == 2: # Last attempt
|
||||
raise
|
||||
sleep_time = 2**attempt
|
||||
print(f'⚠️ Request failed (attempt {attempt + 1}/3), retrying in {sleep_time}s: {e}')
|
||||
time.sleep(sleep_time)
|
||||
|
||||
raise RuntimeError('Unexpected error in retry logic')
|
||||
|
||||
|
||||
def create_fast_task(instructions: str) -> str:
|
||||
"""
|
||||
Create a browser automation task optimized for speed and cost.
|
||||
|
||||
Args:
|
||||
instructions: Natural language description of what the agent should do
|
||||
|
||||
Returns:
|
||||
task_id: Unique identifier for the created task
|
||||
"""
|
||||
print(f'⚡ Creating FAST task: {instructions}')
|
||||
|
||||
# Ultra-fast configuration
|
||||
payload = {
|
||||
'task': instructions,
|
||||
# Model: Fastest and cheapest
|
||||
'llm_model': 'gemini-2.5-flash',
|
||||
# Performance optimizations
|
||||
'use_proxy': False, # No proxy = faster execution
|
||||
'highlight_elements': False, # No highlighting = better performance
|
||||
'use_adblock': True, # Block ads for faster loading
|
||||
# Viewport optimization (smaller = faster)
|
||||
'browser_viewport_width': 1024,
|
||||
'browser_viewport_height': 768,
|
||||
# Cost control
|
||||
'max_agent_steps': 25, # Reasonable limit for fast tasks
|
||||
# Enable sharing for viewing execution
|
||||
'enable_public_share': True, # Get shareable URLs
|
||||
# Optional: Speed up with domain restrictions
|
||||
# "allowed_domains": ["google.com", "*.google.com"]
|
||||
}
|
||||
|
||||
response = _request_with_retry('post', f'{BASE_URL}/run-task', headers=HEADERS, json=payload)
|
||||
|
||||
task_id = response.json()['id']
|
||||
print(f'✅ Fast task created with ID: {task_id}')
|
||||
print('⚡ Configuration: Gemini Flash + No Proxy + No Highlighting')
|
||||
return task_id
|
||||
|
||||
|
||||
def monitor_fast_task(task_id: str) -> dict[str, Any]:
|
||||
"""
|
||||
Monitor task with optimized polling for fast execution.
|
||||
|
||||
Args:
|
||||
task_id: The task to monitor
|
||||
|
||||
Returns:
|
||||
Complete task details with output
|
||||
"""
|
||||
print(f'🚀 Fast monitoring task {task_id}...')
|
||||
|
||||
start_time = time.time()
|
||||
step_count = 0
|
||||
last_step_time = start_time
|
||||
|
||||
# Faster polling for quick tasks
|
||||
poll_interval = 1 # Check every second for fast tasks
|
||||
|
||||
while True:
|
||||
response = _request_with_retry('get', f'{BASE_URL}/task/{task_id}', headers=HEADERS)
|
||||
details = response.json()
|
||||
status = details['status']
|
||||
|
||||
# Show progress with timing
|
||||
current_steps = len(details.get('steps', []))
|
||||
elapsed = time.time() - start_time
|
||||
|
||||
# Build status message
|
||||
if current_steps > step_count:
|
||||
step_time = time.time() - last_step_time
|
||||
last_step_time = time.time()
|
||||
step_count = current_steps
|
||||
step_msg = f'🔥 Step {current_steps} | ⚡ {step_time:.1f}s | Total: {elapsed:.1f}s'
|
||||
else:
|
||||
if status == 'running':
|
||||
step_msg = f'🚀 Step {current_steps} | ⏱️ {elapsed:.1f}s | Fast processing...'
|
||||
else:
|
||||
step_msg = f'🚀 Step {current_steps} | ⏱️ {elapsed:.1f}s | Status: {status}'
|
||||
|
||||
# Clear line and show progress
|
||||
print(f'\r{step_msg:<80}', end='', flush=True)
|
||||
|
||||
# Check completion
|
||||
if status == 'finished':
|
||||
total_time = time.time() - start_time
|
||||
if current_steps > 0:
|
||||
avg_msg = f'⚡ Average: {total_time / current_steps:.1f}s per step'
|
||||
else:
|
||||
avg_msg = '⚡ No steps recorded'
|
||||
print(f'\r🏁 Task completed in {total_time:.1f}s! {avg_msg}' + ' ' * 20)
|
||||
return details
|
||||
|
||||
elif status in ['failed', 'stopped']:
|
||||
print(f'\r❌ Task {status} after {elapsed:.1f}s' + ' ' * 30)
|
||||
return details
|
||||
|
||||
time.sleep(poll_interval)
|
||||
|
||||
|
||||
def run_speed_comparison():
|
||||
"""Run multiple tasks to compare speed vs accuracy."""
|
||||
print('\n🏃♂️ Speed Comparison Demo')
|
||||
print('=' * 40)
|
||||
|
||||
tasks = [
|
||||
'Go to ProductHunt and roast the top product like a sarcastic tech reviewer',
|
||||
'Visit Reddit r/ProgrammerHumor and summarize the top post as a dramatic news story',
|
||||
"Check GitHub trending and write a conspiracy theory about why everyone's switching to Rust",
|
||||
]
|
||||
|
||||
results = []
|
||||
|
||||
for i, task in enumerate(tasks, 1):
|
||||
print(f'\n📝 Fast Task {i}/{len(tasks)}')
|
||||
print(f'Task: {task}')
|
||||
|
||||
start = time.time()
|
||||
task_id = create_fast_task(task)
|
||||
result = monitor_fast_task(task_id)
|
||||
end = time.time()
|
||||
|
||||
results.append(
|
||||
{
|
||||
'task': task,
|
||||
'duration': end - start,
|
||||
'steps': len(result.get('steps', [])),
|
||||
'status': result['status'],
|
||||
'output': result.get('output', '')[:100] + '...' if result.get('output') else 'No output',
|
||||
}
|
||||
)
|
||||
|
||||
# Summary
|
||||
print('\n📊 Speed Summary')
|
||||
print('=' * 50)
|
||||
total_time = sum(r['duration'] for r in results)
|
||||
total_steps = sum(r['steps'] for r in results)
|
||||
|
||||
for i, result in enumerate(results, 1):
|
||||
print(f'Task {i}: {result["duration"]:.1f}s ({result["steps"]} steps) - {result["status"]}')
|
||||
|
||||
print(f'\n⚡ Total time: {total_time:.1f}s')
|
||||
print(f'🔥 Average per task: {total_time / len(results):.1f}s')
|
||||
if total_steps > 0:
|
||||
print(f'💨 Average per step: {total_time / total_steps:.1f}s')
|
||||
else:
|
||||
print('💨 Average per step: N/A (no steps recorded)')
|
||||
|
||||
|
||||
def main():
|
||||
"""Demonstrate ultra-fast cloud automation."""
|
||||
print('⚡ Browser Use Cloud - Ultra-Fast Mode with Gemini Flash')
|
||||
print('=' * 60)
|
||||
|
||||
print('🎯 Configuration Benefits:')
|
||||
print('• Gemini Flash: $0.01 per step (cheapest)')
|
||||
print('• No proxy: 30% faster execution')
|
||||
print('• No highlighting: Better performance')
|
||||
print('• Optimized viewport: Faster rendering')
|
||||
|
||||
try:
|
||||
# Single fast task
|
||||
print('\n🚀 Single Fast Task Demo')
|
||||
print('-' * 30)
|
||||
|
||||
task = """
|
||||
Go to Hacker News (news.ycombinator.com) and get the top 3 articles from the front page.
|
||||
|
||||
Then, write a funny tech news segment in the style of Fireship YouTube channel:
|
||||
- Be sarcastic and witty about tech trends
|
||||
- Use developer humor and memes
|
||||
- Make fun of common programming struggles
|
||||
- Include phrases like "And yes, it runs on JavaScript" or "Plot twist: it's written in Rust"
|
||||
- Keep it under 250 words but make it entertaining
|
||||
- Structure it like a news anchor delivering breaking tech news
|
||||
|
||||
Make each story sound dramatic but also hilarious, like you're reporting on the most important events in human history.
|
||||
"""
|
||||
task_id = create_fast_task(task)
|
||||
result = monitor_fast_task(task_id)
|
||||
|
||||
print(f'\n📊 Result: {result.get("output", "No output")}')
|
||||
|
||||
# Show execution URLs
|
||||
if result.get('live_url'):
|
||||
print(f'\n🔗 Live Preview: {result["live_url"]}')
|
||||
if result.get('public_share_url'):
|
||||
print(f'🌐 Share URL: {result["public_share_url"]}')
|
||||
elif result.get('share_url'):
|
||||
print(f'🌐 Share URL: {result["share_url"]}')
|
||||
|
||||
# Optional: Run speed comparison with --compare flag
|
||||
parser = argparse.ArgumentParser(description='Fast mode demo with Gemini Flash')
|
||||
parser.add_argument('--compare', action='store_true', help='Run speed comparison with 3 tasks')
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.compare:
|
||||
print('\n🏃♂️ Running speed comparison...')
|
||||
run_speed_comparison()
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f'❌ API Error: {e}')
|
||||
except Exception as e:
|
||||
print(f'❌ Error: {e}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,362 @@
|
||||
"""
|
||||
Cloud Example 3: Structured JSON Output 📋
|
||||
==========================================
|
||||
|
||||
This example demonstrates how to get structured, validated JSON output:
|
||||
- Define Pydantic schemas for type safety
|
||||
- Extract structured data from websites
|
||||
- Validate and parse JSON responses
|
||||
- Handle different data types and nested structures
|
||||
|
||||
Perfect for: Data extraction, API integration, structured analysis
|
||||
|
||||
Cost: ~$0.06 (1 task + 5-6 steps with GPT-4.1 mini)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
from pydantic import BaseModel, Field, ValidationError
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
# Configuration
|
||||
API_KEY = os.getenv('BROWSER_USE_API_KEY')
|
||||
if not API_KEY:
|
||||
raise ValueError(
|
||||
'Please set BROWSER_USE_API_KEY environment variable. You can also create an API key at https://cloud.browser-use.com/new-api-key'
|
||||
)
|
||||
|
||||
BASE_URL = os.getenv('BROWSER_USE_BASE_URL', 'https://api.browser-use.com/api/v1')
|
||||
TIMEOUT = int(os.getenv('BROWSER_USE_TIMEOUT', '30'))
|
||||
HEADERS = {'Authorization': f'Bearer {API_KEY}', 'Content-Type': 'application/json'}
|
||||
|
||||
|
||||
def _request_with_retry(method: str, url: str, **kwargs) -> requests.Response:
|
||||
"""Make HTTP request with timeout and retry logic."""
|
||||
kwargs.setdefault('timeout', TIMEOUT)
|
||||
|
||||
for attempt in range(3):
|
||||
try:
|
||||
response = requests.request(method, url, **kwargs)
|
||||
response.raise_for_status()
|
||||
return response
|
||||
except RequestException as e:
|
||||
if attempt == 2: # Last attempt
|
||||
raise
|
||||
sleep_time = 2**attempt
|
||||
print(f'⚠️ Request failed (attempt {attempt + 1}/3), retrying in {sleep_time}s: {e}')
|
||||
time.sleep(sleep_time)
|
||||
|
||||
raise RuntimeError('Unexpected error in retry logic')
|
||||
|
||||
|
||||
# Define structured output schemas using Pydantic
|
||||
class NewsArticle(BaseModel):
|
||||
"""Schema for a news article."""
|
||||
|
||||
title: str = Field(description='The headline of the article')
|
||||
summary: str = Field(description='Brief summary of the article')
|
||||
url: str = Field(description='Direct link to the article')
|
||||
published_date: str | None = Field(description='Publication date if available')
|
||||
category: str | None = Field(description='Article category/section')
|
||||
|
||||
|
||||
class NewsResponse(BaseModel):
|
||||
"""Schema for multiple news articles."""
|
||||
|
||||
articles: list[NewsArticle] = Field(description='List of news articles')
|
||||
source_website: str = Field(description='The website where articles were found')
|
||||
extracted_at: str = Field(description='When the data was extracted')
|
||||
|
||||
|
||||
class ProductInfo(BaseModel):
|
||||
"""Schema for product information."""
|
||||
|
||||
name: str = Field(description='Product name')
|
||||
price: float = Field(description='Product price in USD')
|
||||
rating: float | None = Field(description='Average rating (0-5 scale)')
|
||||
availability: str = Field(description='Stock status (in stock, out of stock, etc.)')
|
||||
description: str = Field(description='Product description')
|
||||
|
||||
|
||||
class CompanyInfo(BaseModel):
|
||||
"""Schema for company information."""
|
||||
|
||||
name: str = Field(description='Company name')
|
||||
stock_symbol: str | None = Field(description='Stock ticker symbol')
|
||||
market_cap: str | None = Field(description='Market capitalization')
|
||||
industry: str = Field(description='Primary industry')
|
||||
headquarters: str = Field(description='Headquarters location')
|
||||
founded_year: int | None = Field(description='Year founded')
|
||||
|
||||
|
||||
def create_structured_task(instructions: str, schema_model: type[BaseModel], **kwargs) -> str:
|
||||
"""
|
||||
Create a task that returns structured JSON output.
|
||||
|
||||
Args:
|
||||
instructions: Task description
|
||||
schema_model: Pydantic model defining the expected output structure
|
||||
**kwargs: Additional task parameters
|
||||
|
||||
Returns:
|
||||
task_id: Unique identifier for the created task
|
||||
"""
|
||||
print(f'📝 Creating structured task: {instructions}')
|
||||
print(f'🏗️ Expected schema: {schema_model.__name__}')
|
||||
|
||||
# Generate JSON schema from Pydantic model
|
||||
json_schema = schema_model.model_json_schema()
|
||||
|
||||
payload = {
|
||||
'task': instructions,
|
||||
'structured_output_json': json.dumps(json_schema),
|
||||
'llm_model': 'gpt-4.1-mini',
|
||||
'max_agent_steps': 15,
|
||||
'enable_public_share': True, # Enable shareable execution URLs
|
||||
**kwargs,
|
||||
}
|
||||
|
||||
response = _request_with_retry('post', f'{BASE_URL}/run-task', headers=HEADERS, json=payload)
|
||||
|
||||
task_id = response.json()['id']
|
||||
print(f'✅ Structured task created: {task_id}')
|
||||
return task_id
|
||||
|
||||
|
||||
def wait_for_structured_completion(task_id: str, max_wait_time: int = 300) -> dict[str, Any]:
|
||||
"""Wait for task completion and return the result."""
|
||||
print(f'⏳ Waiting for structured output (max {max_wait_time}s)...')
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
while True:
|
||||
response = _request_with_retry('get', f'{BASE_URL}/task/{task_id}/status', headers=HEADERS)
|
||||
status = response.json()
|
||||
elapsed = time.time() - start_time
|
||||
|
||||
# Check for timeout
|
||||
if elapsed > max_wait_time:
|
||||
print(f'\r⏰ Task timeout after {max_wait_time}s - stopping wait' + ' ' * 30)
|
||||
# Get final details before timeout
|
||||
details_response = _request_with_retry('get', f'{BASE_URL}/task/{task_id}', headers=HEADERS)
|
||||
details = details_response.json()
|
||||
return details
|
||||
|
||||
# Get step count from full details for better progress tracking
|
||||
details_response = _request_with_retry('get', f'{BASE_URL}/task/{task_id}', headers=HEADERS)
|
||||
details = details_response.json()
|
||||
steps = len(details.get('steps', []))
|
||||
|
||||
# Build status message
|
||||
if status == 'running':
|
||||
status_msg = f'📋 Structured task | Step {steps} | ⏱️ {elapsed:.0f}s | 🔄 Extracting...'
|
||||
else:
|
||||
status_msg = f'📋 Structured task | Step {steps} | ⏱️ {elapsed:.0f}s | Status: {status}'
|
||||
|
||||
# Clear line and show status
|
||||
print(f'\r{status_msg:<80}', end='', flush=True)
|
||||
|
||||
if status == 'finished':
|
||||
print(f'\r✅ Structured data extracted! ({steps} steps in {elapsed:.1f}s)' + ' ' * 20)
|
||||
return details
|
||||
|
||||
elif status in ['failed', 'stopped']:
|
||||
print(f'\r❌ Task {status} after {steps} steps' + ' ' * 30)
|
||||
return details
|
||||
|
||||
time.sleep(3)
|
||||
|
||||
|
||||
def validate_and_display_output(output: str, schema_model: type[BaseModel]):
|
||||
"""
|
||||
Validate the JSON output against the schema and display results.
|
||||
|
||||
Args:
|
||||
output: Raw JSON string from the task
|
||||
schema_model: Pydantic model for validation
|
||||
"""
|
||||
print('\n📊 Structured Output Analysis')
|
||||
print('=' * 40)
|
||||
|
||||
try:
|
||||
# Parse and validate the JSON
|
||||
parsed_data = schema_model.model_validate_json(output)
|
||||
print('✅ JSON validation successful!')
|
||||
|
||||
# Pretty print the structured data
|
||||
print('\n📋 Parsed Data:')
|
||||
print('-' * 20)
|
||||
print(parsed_data.model_dump_json(indent=2))
|
||||
|
||||
# Display specific fields based on model type
|
||||
if isinstance(parsed_data, NewsResponse):
|
||||
print(f'\n📰 Found {len(parsed_data.articles)} articles from {parsed_data.source_website}')
|
||||
for i, article in enumerate(parsed_data.articles[:3], 1):
|
||||
print(f'\n{i}. {article.title}')
|
||||
print(f' Summary: {article.summary[:100]}...')
|
||||
print(f' URL: {article.url}')
|
||||
|
||||
elif isinstance(parsed_data, ProductInfo):
|
||||
print(f'\n🛍️ Product: {parsed_data.name}')
|
||||
print(f' Price: ${parsed_data.price}')
|
||||
print(f' Rating: {parsed_data.rating}/5' if parsed_data.rating else ' Rating: N/A')
|
||||
print(f' Status: {parsed_data.availability}')
|
||||
|
||||
elif isinstance(parsed_data, CompanyInfo):
|
||||
print(f'\n🏢 Company: {parsed_data.name}')
|
||||
print(f' Industry: {parsed_data.industry}')
|
||||
print(f' Headquarters: {parsed_data.headquarters}')
|
||||
if parsed_data.founded_year:
|
||||
print(f' Founded: {parsed_data.founded_year}')
|
||||
|
||||
return parsed_data
|
||||
|
||||
except ValidationError as e:
|
||||
print('❌ JSON validation failed!')
|
||||
print(f'Errors: {e}')
|
||||
print(f'\nRaw output: {output[:500]}...')
|
||||
return None
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
print('❌ Invalid JSON format!')
|
||||
print(f'Error: {e}')
|
||||
print(f'\nRaw output: {output[:500]}...')
|
||||
return None
|
||||
|
||||
|
||||
def demo_news_extraction():
|
||||
"""Demo: Extract structured news data."""
|
||||
print('\n📰 Demo 1: News Article Extraction')
|
||||
print('-' * 40)
|
||||
|
||||
task = """
|
||||
Go to a major news website (like BBC, CNN, or Reuters) and extract information
|
||||
about the top 3 news articles. For each article, get the title, summary, URL,
|
||||
and any other available metadata.
|
||||
"""
|
||||
|
||||
task_id = create_structured_task(task, NewsResponse)
|
||||
result = wait_for_structured_completion(task_id)
|
||||
|
||||
if result.get('output'):
|
||||
parsed_result = validate_and_display_output(result['output'], NewsResponse)
|
||||
|
||||
# Show execution URLs
|
||||
if result.get('live_url'):
|
||||
print(f'\n🔗 Live Preview: {result["live_url"]}')
|
||||
if result.get('public_share_url'):
|
||||
print(f'🌐 Share URL: {result["public_share_url"]}')
|
||||
elif result.get('share_url'):
|
||||
print(f'🌐 Share URL: {result["share_url"]}')
|
||||
|
||||
return parsed_result
|
||||
else:
|
||||
print('❌ No structured output received')
|
||||
return None
|
||||
|
||||
|
||||
def demo_product_extraction():
|
||||
"""Demo: Extract structured product data."""
|
||||
print('\n🛍️ Demo 2: Product Information Extraction')
|
||||
print('-' * 40)
|
||||
|
||||
task = """
|
||||
Go to Amazon and search for 'wireless headphones'. Find the first product result
|
||||
and extract detailed information including name, price, rating, availability,
|
||||
and description.
|
||||
"""
|
||||
|
||||
task_id = create_structured_task(task, ProductInfo)
|
||||
result = wait_for_structured_completion(task_id)
|
||||
|
||||
if result.get('output'):
|
||||
parsed_result = validate_and_display_output(result['output'], ProductInfo)
|
||||
|
||||
# Show execution URLs
|
||||
if result.get('live_url'):
|
||||
print(f'\n🔗 Live Preview: {result["live_url"]}')
|
||||
if result.get('public_share_url'):
|
||||
print(f'🌐 Share URL: {result["public_share_url"]}')
|
||||
elif result.get('share_url'):
|
||||
print(f'🌐 Share URL: {result["share_url"]}')
|
||||
|
||||
return parsed_result
|
||||
else:
|
||||
print('❌ No structured output received')
|
||||
return None
|
||||
|
||||
|
||||
def demo_company_extraction():
|
||||
"""Demo: Extract structured company data."""
|
||||
print('\n🏢 Demo 3: Company Information Extraction')
|
||||
print('-' * 40)
|
||||
|
||||
task = """
|
||||
Go to a financial website and look up information about Apple Inc.
|
||||
Extract company details including name, stock symbol, market cap,
|
||||
industry, headquarters, and founding year.
|
||||
"""
|
||||
|
||||
task_id = create_structured_task(task, CompanyInfo)
|
||||
result = wait_for_structured_completion(task_id)
|
||||
|
||||
if result.get('output'):
|
||||
parsed_result = validate_and_display_output(result['output'], CompanyInfo)
|
||||
|
||||
# Show execution URLs
|
||||
if result.get('live_url'):
|
||||
print(f'\n🔗 Live Preview: {result["live_url"]}')
|
||||
if result.get('public_share_url'):
|
||||
print(f'🌐 Share URL: {result["public_share_url"]}')
|
||||
elif result.get('share_url'):
|
||||
print(f'🌐 Share URL: {result["share_url"]}')
|
||||
|
||||
return parsed_result
|
||||
else:
|
||||
print('❌ No structured output received')
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
"""Demonstrate structured output extraction."""
|
||||
print('📋 Browser Use Cloud - Structured JSON Output')
|
||||
print('=' * 50)
|
||||
|
||||
print('🎯 Features:')
|
||||
print('• Type-safe Pydantic schemas')
|
||||
print('• Automatic JSON validation')
|
||||
print('• Structured data extraction')
|
||||
print('• Multiple output formats')
|
||||
|
||||
try:
|
||||
# Parse command line arguments
|
||||
parser = argparse.ArgumentParser(description='Structured output extraction demo')
|
||||
parser.add_argument('--demo', choices=['news', 'product', 'company', 'all'], default='news', help='Which demo to run')
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f'\n🔍 Running {args.demo} demo(s)...')
|
||||
|
||||
if args.demo == 'news':
|
||||
demo_news_extraction()
|
||||
elif args.demo == 'product':
|
||||
demo_product_extraction()
|
||||
elif args.demo == 'company':
|
||||
demo_company_extraction()
|
||||
elif args.demo == 'all':
|
||||
demo_news_extraction()
|
||||
demo_product_extraction()
|
||||
demo_company_extraction()
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f'❌ API Error: {e}')
|
||||
except Exception as e:
|
||||
print(f'❌ Error: {e}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,331 @@
|
||||
"""
|
||||
Cloud Example 4: Proxy Usage 🌍
|
||||
===============================
|
||||
|
||||
This example demonstrates reliable proxy usage scenarios:
|
||||
- Different country proxies for geo-restrictions
|
||||
- IP address and location verification
|
||||
- Region-specific content access (streaming, news)
|
||||
- Search result localization by country
|
||||
- Mobile/residential proxy benefits
|
||||
|
||||
Perfect for: Geo-restricted content, location testing, regional analysis
|
||||
|
||||
Cost: ~$0.08 (1 task + 6-8 steps with proxy enabled)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
# Configuration
|
||||
API_KEY = os.getenv('BROWSER_USE_API_KEY')
|
||||
if not API_KEY:
|
||||
raise ValueError(
|
||||
'Please set BROWSER_USE_API_KEY environment variable. You can also create an API key at https://cloud.browser-use.com/new-api-key'
|
||||
)
|
||||
|
||||
BASE_URL = os.getenv('BROWSER_USE_BASE_URL', 'https://api.browser-use.com/api/v1')
|
||||
TIMEOUT = int(os.getenv('BROWSER_USE_TIMEOUT', '30'))
|
||||
HEADERS = {'Authorization': f'Bearer {API_KEY}', 'Content-Type': 'application/json'}
|
||||
|
||||
|
||||
def _request_with_retry(method: str, url: str, **kwargs) -> requests.Response:
|
||||
"""Make HTTP request with timeout and retry logic."""
|
||||
kwargs.setdefault('timeout', TIMEOUT)
|
||||
|
||||
for attempt in range(3):
|
||||
try:
|
||||
response = requests.request(method, url, **kwargs)
|
||||
response.raise_for_status()
|
||||
return response
|
||||
except RequestException as e:
|
||||
if attempt == 2: # Last attempt
|
||||
raise
|
||||
sleep_time = 2**attempt
|
||||
print(f'⚠️ Request failed (attempt {attempt + 1}/3), retrying in {sleep_time}s: {e}')
|
||||
time.sleep(sleep_time)
|
||||
|
||||
raise RuntimeError('Unexpected error in retry logic')
|
||||
|
||||
|
||||
def create_task_with_proxy(instructions: str, country_code: str = 'us') -> str:
|
||||
"""
|
||||
Create a task with proxy enabled from a specific country.
|
||||
|
||||
Args:
|
||||
instructions: Task description
|
||||
country_code: Proxy country ('us', 'fr', 'it', 'jp', 'au', 'de', 'fi', 'ca')
|
||||
|
||||
Returns:
|
||||
task_id: Unique identifier for the created task
|
||||
"""
|
||||
print(f'🌍 Creating task with {country_code.upper()} proxy')
|
||||
print(f'📝 Task: {instructions}')
|
||||
|
||||
payload = {
|
||||
'task': instructions,
|
||||
'llm_model': 'gpt-4.1-mini',
|
||||
# Proxy configuration
|
||||
'use_proxy': True, # Required for captcha solving
|
||||
'proxy_country_code': country_code, # Choose proxy location
|
||||
# Standard settings
|
||||
'use_adblock': True, # Block ads for faster loading
|
||||
'highlight_elements': True, # Keep highlighting for visibility
|
||||
'max_agent_steps': 15,
|
||||
# Enable sharing for viewing execution
|
||||
'enable_public_share': True, # Get shareable URLs
|
||||
}
|
||||
|
||||
response = _request_with_retry('post', f'{BASE_URL}/run-task', headers=HEADERS, json=payload)
|
||||
|
||||
task_id = response.json()['id']
|
||||
print(f'✅ Task created with {country_code.upper()} proxy: {task_id}')
|
||||
return task_id
|
||||
|
||||
|
||||
def test_ip_location(country_code: str) -> dict[str, Any]:
|
||||
"""Test IP address and location detection with proxy."""
|
||||
task = """
|
||||
Go to whatismyipaddress.com and tell me:
|
||||
1. The detected IP address
|
||||
2. The detected country/location
|
||||
3. The ISP/organization
|
||||
4. Any other location details shown
|
||||
|
||||
Please be specific about what you see on the page.
|
||||
"""
|
||||
|
||||
task_id = create_task_with_proxy(task, country_code)
|
||||
return wait_for_completion(task_id)
|
||||
|
||||
|
||||
def test_geo_restricted_content(country_code: str) -> dict[str, Any]:
|
||||
"""Test access to geo-restricted content."""
|
||||
task = """
|
||||
Go to a major news website (like BBC, CNN, or local news) and check:
|
||||
1. What content is available
|
||||
2. Any geo-restriction messages
|
||||
3. Local/regional content differences
|
||||
4. Language or currency preferences shown
|
||||
|
||||
Note any differences from what you might expect.
|
||||
"""
|
||||
|
||||
task_id = create_task_with_proxy(task, country_code)
|
||||
return wait_for_completion(task_id)
|
||||
|
||||
|
||||
def test_streaming_service_access(country_code: str) -> dict[str, Any]:
|
||||
"""Test access to region-specific streaming content."""
|
||||
task = """
|
||||
Go to a major streaming service website (like Netflix, YouTube, or BBC iPlayer)
|
||||
and check what content or messaging appears.
|
||||
|
||||
Report:
|
||||
1. What homepage content is shown
|
||||
2. Any geo-restriction messages or content differences
|
||||
3. Available content regions or language options
|
||||
4. Any pricing or availability differences
|
||||
|
||||
Note: Don't try to log in, just observe the publicly available content.
|
||||
"""
|
||||
|
||||
task_id = create_task_with_proxy(task, country_code)
|
||||
return wait_for_completion(task_id)
|
||||
|
||||
|
||||
def test_search_results_by_location(country_code: str) -> dict[str, Any]:
|
||||
"""Test how search results vary by location."""
|
||||
task = """
|
||||
Go to Google and search for "best restaurants near me" or "local news".
|
||||
|
||||
Report:
|
||||
1. What local results appear
|
||||
2. The detected location in search results
|
||||
3. Any location-specific content or ads
|
||||
4. Language preferences
|
||||
|
||||
This will show how search results change based on proxy location.
|
||||
"""
|
||||
|
||||
task_id = create_task_with_proxy(task, country_code)
|
||||
return wait_for_completion(task_id)
|
||||
|
||||
|
||||
def wait_for_completion(task_id: str) -> dict[str, Any]:
|
||||
"""Wait for task completion and return results."""
|
||||
print(f'⏳ Waiting for task {task_id} to complete...')
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
while True:
|
||||
response = _request_with_retry('get', f'{BASE_URL}/task/{task_id}', headers=HEADERS)
|
||||
details = response.json()
|
||||
|
||||
status = details['status']
|
||||
steps = len(details.get('steps', []))
|
||||
elapsed = time.time() - start_time
|
||||
|
||||
# Build status message
|
||||
if status == 'running':
|
||||
status_msg = f'🌍 Proxy task | Step {steps} | ⏱️ {elapsed:.0f}s | 🤖 Processing...'
|
||||
else:
|
||||
status_msg = f'🌍 Proxy task | Step {steps} | ⏱️ {elapsed:.0f}s | Status: {status}'
|
||||
|
||||
# Clear line and show status
|
||||
print(f'\r{status_msg:<80}', end='', flush=True)
|
||||
|
||||
if status == 'finished':
|
||||
print(f'\r✅ Task completed in {steps} steps! ({elapsed:.1f}s total)' + ' ' * 20)
|
||||
return details
|
||||
|
||||
elif status in ['failed', 'stopped']:
|
||||
print(f'\r❌ Task {status} after {steps} steps' + ' ' * 30)
|
||||
return details
|
||||
|
||||
time.sleep(3)
|
||||
|
||||
|
||||
def demo_proxy_countries():
|
||||
"""Demonstrate proxy usage across different countries."""
|
||||
print('\n🌍 Demo 1: Proxy Countries Comparison')
|
||||
print('-' * 45)
|
||||
|
||||
countries = [('us', 'United States'), ('de', 'Germany'), ('jp', 'Japan'), ('au', 'Australia')]
|
||||
|
||||
results = {}
|
||||
|
||||
for code, name in countries:
|
||||
print(f'\n🌍 Testing {name} ({code.upper()}) proxy:')
|
||||
print('=' * 40)
|
||||
|
||||
result = test_ip_location(code)
|
||||
results[code] = result
|
||||
|
||||
if result.get('output'):
|
||||
print(f'📍 Location Result: {result["output"][:200]}...')
|
||||
|
||||
# Show execution URLs
|
||||
if result.get('live_url'):
|
||||
print(f'🔗 Live Preview: {result["live_url"]}')
|
||||
if result.get('public_share_url'):
|
||||
print(f'🌐 Share URL: {result["public_share_url"]}')
|
||||
elif result.get('share_url'):
|
||||
print(f'🌐 Share URL: {result["share_url"]}')
|
||||
|
||||
print('-' * 40)
|
||||
time.sleep(2) # Brief pause between tests
|
||||
|
||||
# Summary comparison
|
||||
print('\n📊 Proxy Location Summary:')
|
||||
print('=' * 30)
|
||||
for code, result in results.items():
|
||||
status = result.get('status', 'unknown')
|
||||
print(f'{code.upper()}: {status}')
|
||||
|
||||
|
||||
def demo_geo_restrictions():
|
||||
"""Demonstrate geo-restriction bypass."""
|
||||
print('\n🚫 Demo 2: Geo-Restriction Testing')
|
||||
print('-' * 40)
|
||||
|
||||
# Test from different locations
|
||||
locations = [('us', 'US content'), ('de', 'European content')]
|
||||
|
||||
for code, description in locations:
|
||||
print(f'\n🌍 Testing {description} with {code.upper()} proxy:')
|
||||
result = test_geo_restricted_content(code)
|
||||
|
||||
if result.get('output'):
|
||||
print(f'📰 Content Access: {result["output"][:200]}...')
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
|
||||
def demo_streaming_access():
|
||||
"""Demonstrate streaming service access with different proxies."""
|
||||
print('\n📺 Demo 3: Streaming Service Access')
|
||||
print('-' * 40)
|
||||
|
||||
locations = [('us', 'US'), ('de', 'Germany')]
|
||||
|
||||
for code, name in locations:
|
||||
print(f'\n🌍 Testing streaming access from {name}:')
|
||||
result = test_streaming_service_access(code)
|
||||
|
||||
if result.get('output'):
|
||||
print(f'📺 Access Result: {result["output"][:200]}...')
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
|
||||
def demo_search_localization():
|
||||
"""Demonstrate search result localization."""
|
||||
print('\n🔍 Demo 4: Search Localization')
|
||||
print('-' * 35)
|
||||
|
||||
locations = [('us', 'US'), ('de', 'Germany')]
|
||||
|
||||
for code, name in locations:
|
||||
print(f'\n🌍 Testing search results from {name}:')
|
||||
result = test_search_results_by_location(code)
|
||||
|
||||
if result.get('output'):
|
||||
print(f'🔍 Search Results: {result["output"][:200]}...')
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
|
||||
def main():
|
||||
"""Demonstrate comprehensive proxy usage."""
|
||||
print('🌍 Browser Use Cloud - Proxy Usage Examples')
|
||||
print('=' * 50)
|
||||
|
||||
print('🎯 Proxy Benefits:')
|
||||
print('• Bypass geo-restrictions')
|
||||
print('• Test location-specific content')
|
||||
print('• Access region-locked websites')
|
||||
print('• Mobile/residential IP addresses')
|
||||
print('• Verify IP geolocation')
|
||||
|
||||
print('\n🌐 Available Countries:')
|
||||
countries = ['🇺🇸 US', '🇫🇷 France', '🇮🇹 Italy', '🇯🇵 Japan', '🇦🇺 Australia', '🇩🇪 Germany', '🇫🇮 Finland', '🇨🇦 Canada']
|
||||
print(' • '.join(countries))
|
||||
|
||||
try:
|
||||
# Parse command line arguments
|
||||
parser = argparse.ArgumentParser(description='Proxy usage examples')
|
||||
parser.add_argument(
|
||||
'--demo', choices=['countries', 'geo', 'streaming', 'search', 'all'], default='countries', help='Which demo to run'
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f'\n🔍 Running {args.demo} demo(s)...')
|
||||
|
||||
if args.demo == 'countries':
|
||||
demo_proxy_countries()
|
||||
elif args.demo == 'geo':
|
||||
demo_geo_restrictions()
|
||||
elif args.demo == 'streaming':
|
||||
demo_streaming_access()
|
||||
elif args.demo == 'search':
|
||||
demo_search_localization()
|
||||
elif args.demo == 'all':
|
||||
demo_proxy_countries()
|
||||
demo_geo_restrictions()
|
||||
demo_streaming_access()
|
||||
demo_search_localization()
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f'❌ API Error: {e}')
|
||||
except Exception as e:
|
||||
print(f'❌ Error: {e}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,355 @@
|
||||
"""
|
||||
Cloud Example 5: Search API (Beta) 🔍
|
||||
=====================================
|
||||
|
||||
This example demonstrates the Browser Use Search API (BETA):
|
||||
- Simple search: Search Google and extract from multiple results
|
||||
- URL search: Extract specific content from a target URL
|
||||
- Deep navigation through websites (depth parameter)
|
||||
- Real-time content extraction vs cached results
|
||||
|
||||
Perfect for: Content extraction, research, competitive analysis
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
|
||||
# Configuration
|
||||
API_KEY = os.getenv('BROWSER_USE_API_KEY')
|
||||
if not API_KEY:
|
||||
raise ValueError(
|
||||
'Please set BROWSER_USE_API_KEY environment variable. You can also create an API key at https://cloud.browser-use.com/new-api-key'
|
||||
)
|
||||
|
||||
BASE_URL = os.getenv('BROWSER_USE_BASE_URL', 'https://api.browser-use.com/api/v1')
|
||||
TIMEOUT = int(os.getenv('BROWSER_USE_TIMEOUT', '30'))
|
||||
HEADERS = {'Authorization': f'Bearer {API_KEY}', 'Content-Type': 'application/json'}
|
||||
|
||||
|
||||
async def simple_search(query: str, max_websites: int = 5, depth: int = 2) -> dict[str, Any]:
|
||||
"""
|
||||
Search Google and extract content from multiple top results.
|
||||
|
||||
Args:
|
||||
query: Search query to process
|
||||
max_websites: Number of websites to process (1-10)
|
||||
depth: How deep to navigate (2-5)
|
||||
|
||||
Returns:
|
||||
Dictionary with results from multiple websites
|
||||
"""
|
||||
# Validate input parameters
|
||||
max_websites = max(1, min(max_websites, 10)) # Clamp to 1-10
|
||||
depth = max(2, min(depth, 5)) # Clamp to 2-5
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
print(f"🔍 Simple Search: '{query}'")
|
||||
print(f'📊 Processing {max_websites} websites at depth {depth}')
|
||||
print(f'💰 Estimated cost: {depth * max_websites}¢')
|
||||
|
||||
payload = {'query': query, 'max_websites': max_websites, 'depth': depth}
|
||||
|
||||
timeout = aiohttp.ClientTimeout(total=TIMEOUT)
|
||||
connector = aiohttp.TCPConnector(limit=10) # Limit concurrent connections
|
||||
|
||||
async with aiohttp.ClientSession(timeout=timeout, connector=connector) as session:
|
||||
async with session.post(f'{BASE_URL}/simple-search', json=payload, headers=HEADERS) as response:
|
||||
elapsed = time.time() - start_time
|
||||
if response.status == 200:
|
||||
try:
|
||||
result = await response.json()
|
||||
print(f'✅ Found results from {len(result.get("results", []))} websites in {elapsed:.1f}s')
|
||||
return result
|
||||
except (aiohttp.ContentTypeError, json.JSONDecodeError) as e:
|
||||
error_text = await response.text()
|
||||
print(f'❌ Invalid JSON response: {e} (after {elapsed:.1f}s)')
|
||||
return {'error': 'Invalid JSON', 'details': error_text}
|
||||
else:
|
||||
error_text = await response.text()
|
||||
print(f'❌ Search failed: {response.status} - {error_text} (after {elapsed:.1f}s)')
|
||||
return {'error': f'HTTP {response.status}', 'details': error_text}
|
||||
|
||||
|
||||
async def search_url(url: str, query: str, depth: int = 2) -> dict[str, Any]:
|
||||
"""
|
||||
Extract specific content from a target URL.
|
||||
|
||||
Args:
|
||||
url: Target URL to extract from
|
||||
query: What specific content to look for
|
||||
depth: How deep to navigate (2-5)
|
||||
|
||||
Returns:
|
||||
Dictionary with extracted content
|
||||
"""
|
||||
# Validate input parameters
|
||||
depth = max(2, min(depth, 5)) # Clamp to 2-5
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
print(f'🎯 URL Search: {url}')
|
||||
print(f"🔍 Looking for: '{query}'")
|
||||
print(f'📊 Navigation depth: {depth}')
|
||||
print(f'💰 Estimated cost: {depth}¢')
|
||||
|
||||
payload = {'url': url, 'query': query, 'depth': depth}
|
||||
|
||||
timeout = aiohttp.ClientTimeout(total=TIMEOUT)
|
||||
connector = aiohttp.TCPConnector(limit=10) # Limit concurrent connections
|
||||
|
||||
async with aiohttp.ClientSession(timeout=timeout, connector=connector) as session:
|
||||
async with session.post(f'{BASE_URL}/search-url', json=payload, headers=HEADERS) as response:
|
||||
elapsed = time.time() - start_time
|
||||
if response.status == 200:
|
||||
try:
|
||||
result = await response.json()
|
||||
print(f'✅ Extracted content from {result.get("url", "website")} in {elapsed:.1f}s')
|
||||
return result
|
||||
except (aiohttp.ContentTypeError, json.JSONDecodeError) as e:
|
||||
error_text = await response.text()
|
||||
print(f'❌ Invalid JSON response: {e} (after {elapsed:.1f}s)')
|
||||
return {'error': 'Invalid JSON', 'details': error_text}
|
||||
else:
|
||||
error_text = await response.text()
|
||||
print(f'❌ URL search failed: {response.status} - {error_text} (after {elapsed:.1f}s)')
|
||||
return {'error': f'HTTP {response.status}', 'details': error_text}
|
||||
|
||||
|
||||
def display_simple_search_results(results: dict[str, Any]):
|
||||
"""Display simple search results in a readable format."""
|
||||
if 'error' in results:
|
||||
print(f'❌ Error: {results["error"]}')
|
||||
return
|
||||
|
||||
websites = results.get('results', [])
|
||||
|
||||
print(f'\n📋 Search Results ({len(websites)} websites)')
|
||||
print('=' * 50)
|
||||
|
||||
for i, site in enumerate(websites, 1):
|
||||
url = site.get('url', 'Unknown URL')
|
||||
content = site.get('content', 'No content')
|
||||
|
||||
print(f'\n{i}. 🌐 {url}')
|
||||
print('-' * 40)
|
||||
|
||||
# Show first 300 chars of content
|
||||
if len(content) > 300:
|
||||
print(f'{content[:300]}...')
|
||||
print(f'[Content truncated - {len(content)} total characters]')
|
||||
else:
|
||||
print(content)
|
||||
|
||||
# Show execution URLs if available
|
||||
if results.get('live_url'):
|
||||
print(f'\n🔗 Live Preview: {results["live_url"]}')
|
||||
if results.get('public_share_url'):
|
||||
print(f'🌐 Share URL: {results["public_share_url"]}')
|
||||
elif results.get('share_url'):
|
||||
print(f'🌐 Share URL: {results["share_url"]}')
|
||||
|
||||
|
||||
def display_url_search_results(results: dict[str, Any]):
|
||||
"""Display URL search results in a readable format."""
|
||||
if 'error' in results:
|
||||
print(f'❌ Error: {results["error"]}')
|
||||
return
|
||||
|
||||
url = results.get('url', 'Unknown URL')
|
||||
content = results.get('content', 'No content')
|
||||
|
||||
print(f'\n📄 Extracted Content from: {url}')
|
||||
print('=' * 60)
|
||||
print(content)
|
||||
|
||||
# Show execution URLs if available
|
||||
if results.get('live_url'):
|
||||
print(f'\n🔗 Live Preview: {results["live_url"]}')
|
||||
if results.get('public_share_url'):
|
||||
print(f'🌐 Share URL: {results["public_share_url"]}')
|
||||
elif results.get('share_url'):
|
||||
print(f'🌐 Share URL: {results["share_url"]}')
|
||||
|
||||
|
||||
async def demo_news_search():
|
||||
"""Demo: Search for latest news across multiple sources."""
|
||||
print('\n📰 Demo 1: Latest News Search')
|
||||
print('-' * 35)
|
||||
|
||||
demo_start = time.time()
|
||||
query = 'latest developments in artificial intelligence 2024'
|
||||
results = await simple_search(query, max_websites=4, depth=2)
|
||||
demo_elapsed = time.time() - demo_start
|
||||
|
||||
display_simple_search_results(results)
|
||||
print(f'\n⏱️ Total demo time: {demo_elapsed:.1f}s')
|
||||
|
||||
return results
|
||||
|
||||
|
||||
async def demo_competitive_analysis():
|
||||
"""Demo: Analyze competitor websites."""
|
||||
print('\n🏢 Demo 2: Competitive Analysis')
|
||||
print('-' * 35)
|
||||
|
||||
query = 'browser automation tools comparison features pricing'
|
||||
results = await simple_search(query, max_websites=3, depth=3)
|
||||
display_simple_search_results(results)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
async def demo_deep_website_analysis():
|
||||
"""Demo: Deep analysis of a specific website."""
|
||||
print('\n🎯 Demo 3: Deep Website Analysis')
|
||||
print('-' * 35)
|
||||
|
||||
demo_start = time.time()
|
||||
url = 'https://docs.browser-use.com'
|
||||
query = 'Browser Use features, pricing, and API capabilities'
|
||||
results = await search_url(url, query, depth=3)
|
||||
demo_elapsed = time.time() - demo_start
|
||||
|
||||
display_url_search_results(results)
|
||||
print(f'\n⏱️ Total demo time: {demo_elapsed:.1f}s')
|
||||
|
||||
return results
|
||||
|
||||
|
||||
async def demo_product_research():
|
||||
"""Demo: Product research and comparison."""
|
||||
print('\n🛍️ Demo 4: Product Research')
|
||||
print('-' * 30)
|
||||
|
||||
query = 'best wireless headphones 2024 reviews comparison'
|
||||
results = await simple_search(query, max_websites=5, depth=2)
|
||||
display_simple_search_results(results)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
async def demo_real_time_vs_cached():
|
||||
"""Demo: Show difference between real-time and cached results."""
|
||||
print('\n⚡ Demo 5: Real-time vs Cached Data')
|
||||
print('-' * 40)
|
||||
|
||||
print('🔄 Browser Use Search API benefits:')
|
||||
print('• Actually browses websites like a human')
|
||||
print('• Gets live, current data (not cached)')
|
||||
print('• Navigates deep into sites via clicks')
|
||||
print('• Handles JavaScript and dynamic content')
|
||||
print('• Accesses pages requiring navigation')
|
||||
|
||||
# Example with live data
|
||||
query = 'current Bitcoin price USD live'
|
||||
results = await simple_search(query, max_websites=3, depth=2)
|
||||
|
||||
print('\n💰 Live Bitcoin Price Search Results:')
|
||||
display_simple_search_results(results)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
async def demo_search_depth_comparison():
|
||||
"""Demo: Compare different search depths."""
|
||||
print('\n📊 Demo 6: Search Depth Comparison')
|
||||
print('-' * 40)
|
||||
|
||||
url = 'https://news.ycombinator.com'
|
||||
query = 'trending technology discussions'
|
||||
|
||||
depths = [2, 3, 4]
|
||||
results = {}
|
||||
|
||||
for depth in depths:
|
||||
print(f'\n🔍 Testing depth {depth}:')
|
||||
result = await search_url(url, query, depth)
|
||||
results[depth] = result
|
||||
|
||||
if 'content' in result:
|
||||
content_length = len(result['content'])
|
||||
print(f'📏 Content length: {content_length} characters')
|
||||
|
||||
# Brief pause between requests
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Summary
|
||||
print('\n📊 Depth Comparison Summary:')
|
||||
print('-' * 30)
|
||||
for depth, result in results.items():
|
||||
if 'content' in result:
|
||||
length = len(result['content'])
|
||||
print(f'Depth {depth}: {length} characters')
|
||||
else:
|
||||
print(f'Depth {depth}: Error or no content')
|
||||
|
||||
return results
|
||||
|
||||
|
||||
async def main():
|
||||
"""Demonstrate comprehensive Search API usage."""
|
||||
print('🔍 Browser Use Cloud - Search API (BETA)')
|
||||
print('=' * 45)
|
||||
|
||||
print('⚠️ Note: This API is in BETA and may change')
|
||||
print()
|
||||
print('🎯 Search API Features:')
|
||||
print('• Real-time website browsing (not cached)')
|
||||
print('• Deep navigation through multiple pages')
|
||||
print('• Dynamic content and JavaScript handling')
|
||||
print('• Multiple result aggregation')
|
||||
print('• Cost-effective content extraction')
|
||||
|
||||
print('\n💰 Pricing:')
|
||||
print('• Simple Search: 1¢ × depth × websites')
|
||||
print('• URL Search: 1¢ × depth')
|
||||
print('• Example: depth=2, 5 websites = 10¢')
|
||||
|
||||
try:
|
||||
# Parse command line arguments
|
||||
parser = argparse.ArgumentParser(description='Search API (BETA) examples')
|
||||
parser.add_argument(
|
||||
'--demo',
|
||||
choices=['news', 'competitive', 'deep', 'product', 'realtime', 'depth', 'all'],
|
||||
default='news',
|
||||
help='Which demo to run',
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f'\n🔍 Running {args.demo} demo(s)...')
|
||||
|
||||
if args.demo == 'news':
|
||||
await demo_news_search()
|
||||
elif args.demo == 'competitive':
|
||||
await demo_competitive_analysis()
|
||||
elif args.demo == 'deep':
|
||||
await demo_deep_website_analysis()
|
||||
elif args.demo == 'product':
|
||||
await demo_product_research()
|
||||
elif args.demo == 'realtime':
|
||||
await demo_real_time_vs_cached()
|
||||
elif args.demo == 'depth':
|
||||
await demo_search_depth_comparison()
|
||||
elif args.demo == 'all':
|
||||
await demo_news_search()
|
||||
await demo_competitive_analysis()
|
||||
await demo_deep_website_analysis()
|
||||
await demo_product_research()
|
||||
await demo_real_time_vs_cached()
|
||||
await demo_search_depth_comparison()
|
||||
|
||||
except aiohttp.ClientError as e:
|
||||
print(f'❌ Network Error: {e}')
|
||||
except Exception as e:
|
||||
print(f'❌ Error: {e}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,137 @@
|
||||
# Browser Use Cloud Examples 🚀
|
||||
|
||||
Welcome to the Browser Use Cloud examples! This folder contains progressively complex examples to help you get started with the Browser Use Cloud API quickly and efficiently.
|
||||
|
||||
## 📋 Prerequisites
|
||||
|
||||
1. **API Key**: Get your API key from [cloud.browser-use.com](https://cloud.browser-use.com/new-api-key)
|
||||
2. **Python Environment**: Python 3.11+ with dependencies
|
||||
3. **Environment Variables**: Configure your API settings
|
||||
|
||||
### Quick Setup
|
||||
|
||||
```bash
|
||||
# Create virtual environment and install dependencies (from project root)
|
||||
uv venv --python 3.11
|
||||
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
||||
uv sync
|
||||
|
||||
# Set environment variables
|
||||
export BROWSER_USE_API_KEY="your_api_key_here"
|
||||
export BROWSER_USE_BASE_URL="https://api.browser-use.com/api/v1" # Optional
|
||||
export BROWSER_USE_TIMEOUT="30" # Optional: request timeout in seconds
|
||||
|
||||
# Or use .env file (recommended)
|
||||
cp examples/cloud/env.example .env
|
||||
# Edit .env with your values
|
||||
|
||||
# Run examples from project root
|
||||
python examples/cloud/01_basic_task.py
|
||||
```
|
||||
|
||||
## 🎯 Examples Overview
|
||||
|
||||
### 🚀 Easy Cloud Setup Examples
|
||||
|
||||
- **[01_basic_task.py](./01_basic_task.py)** - Your first cloud task (start here!)
|
||||
- **[02_fast_mode_gemini.py](./02_fast_mode_gemini.py)** - ⚡ Ultra-fast mode with Gemini Flash & Fireship humor
|
||||
- **[03_structured_output.py](./03_structured_output.py)** - Get structured JSON responses
|
||||
- **[04_proxy_usage.py](./04_proxy_usage.py)** - 🌍 Proxy for geo-restrictions & captcha solving
|
||||
- **[05_search_api.py](./05_search_api.py)** - 🔍 Search API for content extraction (BETA)
|
||||
|
||||
## 💰 Cost Optimization Tips
|
||||
|
||||
1. **Use Gemini Flash** for fastest/cheapest execution ($0.01/step)
|
||||
2. **Disable proxy** when not needed for captcha solving
|
||||
3. **Disable element highlighting** for better performance
|
||||
4. **Set max_agent_steps** to prevent runaway costs
|
||||
5. **Use structured output** to reduce parsing overhead
|
||||
6. **Add timeouts and retries** for reliability in production
|
||||
7. **Use domain restrictions** when working with secrets
|
||||
|
||||
## 🎨 Fast Mode Configuration
|
||||
|
||||
For maximum speed and cost efficiency:
|
||||
|
||||
```python
|
||||
{
|
||||
"llm_model": "gemini-2.5-flash",
|
||||
"use_proxy": False,
|
||||
"highlight_elements": False,
|
||||
"use_adblock": True,
|
||||
"max_agent_steps": 50
|
||||
}
|
||||
```
|
||||
|
||||
## 🔐 Security & Advanced Features
|
||||
|
||||
### Using Proxy
|
||||
```python
|
||||
{
|
||||
"use_proxy": True,
|
||||
"proxy_country_code": "us", # 'us', 'fr', 'it', 'jp', 'au', 'de', 'fi', 'ca'
|
||||
}
|
||||
```
|
||||
|
||||
### Passing Secrets Securely
|
||||
```python
|
||||
{
|
||||
"secrets": {
|
||||
"username": "your_username",
|
||||
"password": "your_password",
|
||||
"api_key": "your_api_key"
|
||||
},
|
||||
"allowed_domains": ["*.yoursite.com"] # Recommended with secrets
|
||||
}
|
||||
```
|
||||
|
||||
## 🔍 Search API (BETA)
|
||||
|
||||
The Search API extracts content by actually browsing websites (not cached results):
|
||||
|
||||
### Simple Search (Multi-site)
|
||||
```python
|
||||
# Cost: 1¢ × depth × websites
|
||||
{
|
||||
"query": "latest AI news",
|
||||
"max_websites": 5,
|
||||
"depth": 2
|
||||
}
|
||||
```
|
||||
|
||||
### URL Search (Single site)
|
||||
```python
|
||||
# Cost: 1¢ × depth
|
||||
{
|
||||
"url": "https://example.com",
|
||||
"query": "pricing information",
|
||||
"depth": 3
|
||||
}
|
||||
```
|
||||
|
||||
## 🔗 Quick Links
|
||||
|
||||
- [Cloud API Documentation](https://docs.browser-use.com/cloud)
|
||||
- [API Reference](https://docs.browser-use.com/api-reference)
|
||||
- [Pricing](https://cloud.browser-use.com/billing)
|
||||
- [Discord Community](https://link.browser-use.com/discord)
|
||||
|
||||
## 🔧 Production Best Practices
|
||||
|
||||
- **Timeouts**: All examples include 30-second timeouts with retry logic
|
||||
- **Error Handling**: Comprehensive error catching and status code validation
|
||||
- **Security**: Use environment variables, domain restrictions with secrets
|
||||
- **Reliability**: Built-in retries for network issues and rate limits
|
||||
- **Automation**: CLI arguments instead of interactive prompts for CI/CD
|
||||
|
||||
## 🆘 Support
|
||||
|
||||
Need help?
|
||||
|
||||
- 📧 Email: support@browser-use.com
|
||||
- 💬 Discord: [Join our community](https://link.browser-use.com/discord)
|
||||
- 📖 Docs: <https://docs.browser-use.com>
|
||||
|
||||
---
|
||||
|
||||
**💡 Pro Tip**: Start with `01_basic_task.py` and work your way up. Each example builds on the previous ones!
|
||||
@@ -0,0 +1,21 @@
|
||||
# Browser Use Cloud API Configuration
|
||||
# Copy this file to .env and fill in your values
|
||||
|
||||
# Required: Your Browser Use Cloud API key
|
||||
# Get it from: https://cloud.browser-use.com/new-api-key
|
||||
BROWSER_USE_API_KEY=your_api_key_here
|
||||
|
||||
# Optional: Custom API base URL (for enterprise installations)
|
||||
# BROWSER_USE_BASE_URL=https://api.browser-use.com/api/v1
|
||||
|
||||
# Optional: Default model preference
|
||||
# BROWSER_USE_DEFAULT_MODEL=gemini-2.5-flash
|
||||
|
||||
# Optional: Cost limits
|
||||
# BROWSER_USE_MAX_COST_PER_TASK=5.0
|
||||
|
||||
# Optional: Request timeout (seconds)
|
||||
# BROWSER_USE_TIMEOUT=30
|
||||
|
||||
# Optional: Logging configuration
|
||||
# LOG_LEVEL=INFO
|
||||
@@ -0,0 +1,32 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
from browser_use import Agent
|
||||
|
||||
secret_key = os.environ.get('OTP_SECRET_KEY')
|
||||
if not secret_key:
|
||||
# For this example copy the code from the website https://authenticationtest.com/totpChallenge/
|
||||
# For real 2fa just copy the secret key when you setup 2fa, you can get this e.g. in 1Password
|
||||
secret_key = 'JBSWY3DPEHPK3PXP'
|
||||
|
||||
|
||||
sensitive_data = {'bu_2fa_code': secret_key}
|
||||
|
||||
|
||||
task = """
|
||||
1. Go to https://authenticationtest.com/totpChallenge/ and try to log in.
|
||||
2. If prompted for 2FA code:
|
||||
Input the the secret bu_2fa_code.
|
||||
|
||||
When you input bu_2fa_code, the 6 digit code will be generated automatically.
|
||||
"""
|
||||
|
||||
|
||||
Agent(task=task, sensitive_data=sensitive_data).run_sync() # type: ignore
|
||||
@@ -0,0 +1,120 @@
|
||||
"""
|
||||
Action filters (domains) let you limit actions available to the Agent on a step-by-step/page-by-page basis.
|
||||
|
||||
@registry.action(..., domains=['*'])
|
||||
async def some_action(browser_session: BrowserSession):
|
||||
...
|
||||
|
||||
This helps prevent the LLM from deciding to use an action that is not compatible with the current page.
|
||||
It helps limit decision fatigue by scoping actions only to pages where they make sense.
|
||||
It also helps prevent mis-triggering stateful actions or actions that could break other programs or leak secrets.
|
||||
|
||||
For example:
|
||||
- only run on certain domains @registry.action(..., domains=['example.com', '*.example.com', 'example.co.*']) (supports globs, but no regex)
|
||||
- only fill in a password on a specific login page url
|
||||
- only run if this action has not run before on this page (e.g. by looking up the url in a file on disk)
|
||||
|
||||
During each step, the agent recalculates the actions available specifically for that page, and informs the LLM.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from browser_use import ChatOpenAI
|
||||
from browser_use.agent.service import Agent, Tools
|
||||
from browser_use.browser import BrowserSession
|
||||
|
||||
# Initialize tools and registry
|
||||
tools = Tools()
|
||||
registry = tools.registry
|
||||
|
||||
|
||||
# Action will only be available to Agent on Google domains because of the domain filter
|
||||
@registry.action(description='Trigger disco mode', domains=['google.com', '*.google.com'])
|
||||
async def disco_mode(browser_session: BrowserSession):
|
||||
# Execute JavaScript using CDP
|
||||
cdp_session = await browser_session.get_or_create_cdp_session()
|
||||
await cdp_session.cdp_client.send.Runtime.evaluate(
|
||||
params={
|
||||
'expression': """(() => {
|
||||
// define the wiggle animation
|
||||
document.styleSheets[0].insertRule('@keyframes wiggle { 0% { transform: rotate(0deg); } 50% { transform: rotate(10deg); } 100% { transform: rotate(0deg); } }');
|
||||
|
||||
document.querySelectorAll("*").forEach(element => {
|
||||
element.style.animation = "wiggle 0.5s infinite";
|
||||
});
|
||||
})()"""
|
||||
},
|
||||
session_id=cdp_session.session_id,
|
||||
)
|
||||
|
||||
|
||||
# Custom filter function that checks URL
|
||||
async def is_login_page(browser_session: BrowserSession) -> bool:
|
||||
"""Check if current page is a login page."""
|
||||
try:
|
||||
# Get current URL using CDP
|
||||
cdp_session = await browser_session.get_or_create_cdp_session()
|
||||
result = await cdp_session.cdp_client.send.Runtime.evaluate(
|
||||
params={'expression': 'window.location.href', 'returnByValue': True}, session_id=cdp_session.session_id
|
||||
)
|
||||
url = result.get('result', {}).get('value', '')
|
||||
return 'login' in url.lower() or 'signin' in url.lower()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
# Note: page_filter is not directly supported anymore, so we'll just use domains
|
||||
# and check the condition inside the function
|
||||
@registry.action(description='Use the force, luke', domains=['*'])
|
||||
async def use_the_force(browser_session: BrowserSession):
|
||||
# Check if it's a login page
|
||||
if not await is_login_page(browser_session):
|
||||
return # Skip if not a login page
|
||||
|
||||
# Execute JavaScript using CDP
|
||||
cdp_session = await browser_session.get_or_create_cdp_session()
|
||||
await cdp_session.cdp_client.send.Runtime.evaluate(
|
||||
params={
|
||||
'expression': """(() => {
|
||||
document.querySelector('body').innerHTML = 'These are not the droids you are looking for';
|
||||
})()"""
|
||||
},
|
||||
session_id=cdp_session.session_id,
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main function to run the example"""
|
||||
browser_session = BrowserSession()
|
||||
await browser_session.start()
|
||||
llm = ChatOpenAI(model='gpt-4.1-mini')
|
||||
|
||||
# Create the agent
|
||||
agent = Agent( # disco mode will not be triggered on apple.com because the LLM won't be able to see that action available, it should work on Google.com though.
|
||||
task="""
|
||||
Go to apple.com and trigger disco mode (if dont know how to do that, then just move on).
|
||||
Then go to google.com and trigger disco mode.
|
||||
After that, go to the Google login page and Use the force, luke.
|
||||
""",
|
||||
llm=llm,
|
||||
browser_session=browser_session,
|
||||
tools=tools,
|
||||
)
|
||||
|
||||
# Run the agent
|
||||
await agent.run(max_steps=10)
|
||||
|
||||
# Cleanup
|
||||
await browser_session.kill()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,38 @@
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
from browser_use.browser.session import BrowserSession
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from browser_use import ActionResult, Agent, ChatOpenAI, Tools
|
||||
|
||||
tools = Tools()
|
||||
|
||||
llm = ChatOpenAI(model='gpt-4.1-mini')
|
||||
|
||||
|
||||
@tools.registry.action('Click on submit button')
|
||||
async def click_submit_button(browser_session: BrowserSession):
|
||||
page = await browser_session.must_get_current_page()
|
||||
|
||||
submit_button = await page.must_get_element_by_prompt('submit button', llm)
|
||||
await submit_button.click()
|
||||
|
||||
return ActionResult(is_done=True, extracted_content='Submit button clicked!')
|
||||
|
||||
|
||||
async def main():
|
||||
task = 'go to brower-use.com and then click on the submit button'
|
||||
agent = Agent(task=task, llm=llm, tools=tools)
|
||||
|
||||
await agent.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,112 @@
|
||||
import asyncio
|
||||
import http.client
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
import logging
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from browser_use import ActionResult, Agent, ChatOpenAI, Tools
|
||||
from browser_use.browser.profile import BrowserProfile
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Person(BaseModel):
|
||||
name: str
|
||||
email: str | None = None
|
||||
|
||||
|
||||
class PersonList(BaseModel):
|
||||
people: list[Person]
|
||||
|
||||
|
||||
SERP_API_KEY = os.getenv('SERPER_API_KEY')
|
||||
if not SERP_API_KEY:
|
||||
raise ValueError('SERPER_API_KEY is not set')
|
||||
|
||||
tools = Tools(exclude_actions=['search'], output_model=PersonList)
|
||||
|
||||
|
||||
@tools.registry.action('Search the web for a specific query. Returns a short description and links of the results.')
|
||||
async def search_web(query: str):
|
||||
# do a serp search for the query
|
||||
conn = http.client.HTTPSConnection('google.serper.dev')
|
||||
payload = json.dumps({'q': query})
|
||||
headers = {'X-API-KEY': SERP_API_KEY, 'Content-Type': 'application/json'}
|
||||
conn.request('POST', '/search', payload, headers)
|
||||
res = conn.getresponse()
|
||||
data = res.read()
|
||||
serp_data = json.loads(data.decode('utf-8'))
|
||||
|
||||
# exclude searchParameters and credits
|
||||
serp_data = {k: v for k, v in serp_data.items() if k not in ['searchParameters', 'credits']}
|
||||
|
||||
# keep the value of the key "organic"
|
||||
|
||||
organic = serp_data.get('organic', [])
|
||||
# remove the key "position"
|
||||
organic = [{k: v for k, v in d.items() if k != 'position'} for d in organic]
|
||||
|
||||
# print the original data
|
||||
logger.debug(json.dumps(organic, indent=2))
|
||||
|
||||
# to string
|
||||
organic_str = json.dumps(organic)
|
||||
|
||||
return ActionResult(extracted_content=organic_str, include_in_memory=False, include_extracted_content_only_once=True)
|
||||
|
||||
|
||||
names = [
|
||||
'Ruedi Aebersold',
|
||||
'Bernd Bodenmiller',
|
||||
'Eugene Demler',
|
||||
'Erich Fischer',
|
||||
'Pietro Gambardella',
|
||||
'Matthias Huss',
|
||||
'Reto Knutti',
|
||||
'Maksym Kovalenko',
|
||||
'Antonio Lanzavecchia',
|
||||
'Maria Lukatskaya',
|
||||
'Jochen Markard',
|
||||
'Javier Pérez-Ramírez',
|
||||
'Federica Sallusto',
|
||||
'Gisbert Schneider',
|
||||
'Sonia I. Seneviratne',
|
||||
'Michael Siegrist',
|
||||
'Johan Six',
|
||||
'Tanja Stadler',
|
||||
'Shinichi Sunagawa',
|
||||
'Michael Bruce Zimmermann',
|
||||
]
|
||||
|
||||
|
||||
async def main():
|
||||
task = 'use search_web with "find email address of the following ETH professor:" for each of the following persons in a list of actions. Finally return the list with name and email if provided - do always 5 at once'
|
||||
task += '\n' + '\n'.join(names)
|
||||
model = ChatOpenAI(model='gpt-4.1-mini')
|
||||
browser_profile = BrowserProfile()
|
||||
agent = Agent(task=task, llm=model, tools=tools, browser_profile=browser_profile)
|
||||
|
||||
history = await agent.run()
|
||||
|
||||
result = history.final_result()
|
||||
if result:
|
||||
parsed: PersonList = PersonList.model_validate_json(result)
|
||||
|
||||
for person in parsed.people:
|
||||
print(f'{person.name} - {person.email}')
|
||||
else:
|
||||
print('No result')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,347 @@
|
||||
"""
|
||||
OpenAI Computer Use Assistant (CUA) Integration
|
||||
|
||||
This example demonstrates how to integrate OpenAI's Computer Use Assistant as a fallback
|
||||
action when standard browser actions are insufficient to achieve the desired goal.
|
||||
The CUA can perform complex computer interactions that might be difficult to achieve
|
||||
through regular browser-use actions.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import os
|
||||
import sys
|
||||
from io import BytesIO
|
||||
|
||||
from PIL import Image
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from openai import AsyncOpenAI
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from browser_use import Agent, ChatOpenAI, Tools
|
||||
from browser_use.agent.views import ActionResult
|
||||
from browser_use.browser import BrowserSession
|
||||
|
||||
|
||||
class OpenAICUAAction(BaseModel):
|
||||
"""Parameters for OpenAI Computer Use Assistant action."""
|
||||
|
||||
description: str = Field(..., description='Description of your next goal')
|
||||
|
||||
|
||||
async def handle_model_action(browser_session: BrowserSession, action) -> ActionResult:
|
||||
"""
|
||||
Given a computer action (e.g., click, double_click, scroll, etc.),
|
||||
execute the corresponding operation using CDP.
|
||||
"""
|
||||
action_type = action.type
|
||||
ERROR_MSG: str = 'Could not execute the CUA action.'
|
||||
|
||||
if not browser_session.agent_focus_target_id:
|
||||
return ActionResult(error='No active browser session')
|
||||
|
||||
# Get CDP session for the focused target using the public API
|
||||
try:
|
||||
cdp_session = await browser_session.get_or_create_cdp_session(browser_session.agent_focus_target_id, focus=False)
|
||||
except Exception as e:
|
||||
return ActionResult(error=f'Failed to get CDP session: {e}')
|
||||
|
||||
try:
|
||||
match action_type:
|
||||
case 'click':
|
||||
x, y = action.x, action.y
|
||||
button = action.button
|
||||
print(f"Action: click at ({x}, {y}) with button '{button}'")
|
||||
# Not handling things like middle click, etc.
|
||||
if button != 'left' and button != 'right':
|
||||
button = 'left'
|
||||
|
||||
# Use CDP to click
|
||||
await browser_session.cdp_client.send.Input.dispatchMouseEvent(
|
||||
params={
|
||||
'type': 'mousePressed',
|
||||
'x': x,
|
||||
'y': y,
|
||||
'button': button,
|
||||
'clickCount': 1,
|
||||
},
|
||||
session_id=cdp_session.session_id,
|
||||
)
|
||||
await browser_session.cdp_client.send.Input.dispatchMouseEvent(
|
||||
params={
|
||||
'type': 'mouseReleased',
|
||||
'x': x,
|
||||
'y': y,
|
||||
'button': button,
|
||||
},
|
||||
session_id=cdp_session.session_id,
|
||||
)
|
||||
msg = f'Clicked at ({x}, {y}) with button {button}'
|
||||
return ActionResult(extracted_content=msg, include_in_memory=True, long_term_memory=msg)
|
||||
|
||||
case 'scroll':
|
||||
x, y = action.x, action.y
|
||||
scroll_x, scroll_y = action.scroll_x, action.scroll_y
|
||||
print(f'Action: scroll at ({x}, {y}) with offsets (scroll_x={scroll_x}, scroll_y={scroll_y})')
|
||||
|
||||
# Move mouse to position first
|
||||
await browser_session.cdp_client.send.Input.dispatchMouseEvent(
|
||||
params={
|
||||
'type': 'mouseMoved',
|
||||
'x': x,
|
||||
'y': y,
|
||||
},
|
||||
session_id=cdp_session.session_id,
|
||||
)
|
||||
|
||||
# Execute scroll using JavaScript
|
||||
await browser_session.cdp_client.send.Runtime.evaluate(
|
||||
params={
|
||||
'expression': f'window.scrollBy({scroll_x}, {scroll_y})',
|
||||
},
|
||||
session_id=cdp_session.session_id,
|
||||
)
|
||||
msg = f'Scrolled at ({x}, {y}) with offsets (scroll_x={scroll_x}, scroll_y={scroll_y})'
|
||||
return ActionResult(extracted_content=msg, include_in_memory=True, long_term_memory=msg)
|
||||
|
||||
case 'keypress':
|
||||
keys = action.keys
|
||||
for k in keys:
|
||||
print(f"Action: keypress '{k}'")
|
||||
# A simple mapping for common keys; expand as needed.
|
||||
key_code = k
|
||||
if k.lower() == 'enter':
|
||||
key_code = 'Enter'
|
||||
elif k.lower() == 'space':
|
||||
key_code = 'Space'
|
||||
|
||||
# Use CDP to send key
|
||||
await browser_session.cdp_client.send.Input.dispatchKeyEvent(
|
||||
params={
|
||||
'type': 'keyDown',
|
||||
'key': key_code,
|
||||
},
|
||||
session_id=cdp_session.session_id,
|
||||
)
|
||||
await browser_session.cdp_client.send.Input.dispatchKeyEvent(
|
||||
params={
|
||||
'type': 'keyUp',
|
||||
'key': key_code,
|
||||
},
|
||||
session_id=cdp_session.session_id,
|
||||
)
|
||||
msg = f'Pressed keys: {keys}'
|
||||
return ActionResult(extracted_content=msg, include_in_memory=True, long_term_memory=msg)
|
||||
|
||||
case 'type':
|
||||
text = action.text
|
||||
print(f'Action: type text: {text}')
|
||||
|
||||
# Type text character by character
|
||||
for char in text:
|
||||
await browser_session.cdp_client.send.Input.dispatchKeyEvent(
|
||||
params={
|
||||
'type': 'char',
|
||||
'text': char,
|
||||
},
|
||||
session_id=cdp_session.session_id,
|
||||
)
|
||||
msg = f'Typed text: {text}'
|
||||
return ActionResult(extracted_content=msg, include_in_memory=True, long_term_memory=msg)
|
||||
|
||||
case 'wait':
|
||||
print('Action: wait')
|
||||
await asyncio.sleep(2)
|
||||
msg = 'Waited for 2 seconds'
|
||||
return ActionResult(extracted_content=msg, include_in_memory=True, long_term_memory=msg)
|
||||
|
||||
case 'screenshot':
|
||||
# Nothing to do as screenshot is taken at each turn
|
||||
print('Action: screenshot')
|
||||
return ActionResult(error=ERROR_MSG)
|
||||
# Handle other actions here
|
||||
|
||||
case _:
|
||||
print(f'Unrecognized action: {action}')
|
||||
return ActionResult(error=ERROR_MSG)
|
||||
|
||||
except Exception as e:
|
||||
print(f'Error handling action {action}: {e}')
|
||||
return ActionResult(error=ERROR_MSG)
|
||||
|
||||
|
||||
tools = Tools()
|
||||
|
||||
|
||||
@tools.registry.action(
|
||||
'Use OpenAI Computer Use Assistant (CUA) as a fallback when standard browser actions cannot achieve the desired goal. This action sends a screenshot and description to OpenAI CUA and executes the returned computer use actions.',
|
||||
param_model=OpenAICUAAction,
|
||||
)
|
||||
async def openai_cua_fallback(params: OpenAICUAAction, browser_session: BrowserSession):
|
||||
"""
|
||||
Fallback action that uses OpenAI's Computer Use Assistant to perform complex
|
||||
computer interactions when standard browser actions are insufficient.
|
||||
"""
|
||||
print(f'🎯 CUA Action Starting - Goal: {params.description}')
|
||||
|
||||
try:
|
||||
# Get browser state summary
|
||||
state = await browser_session.get_browser_state_summary()
|
||||
page_info = state.page_info
|
||||
if not page_info:
|
||||
raise Exception('Page info not found - cannot execute CUA action')
|
||||
|
||||
print(f'📐 Viewport size: {page_info.viewport_width}x{page_info.viewport_height}')
|
||||
|
||||
screenshot_b64 = state.screenshot
|
||||
if not screenshot_b64:
|
||||
raise Exception('Screenshot not found - cannot execute CUA action')
|
||||
|
||||
print(f'📸 Screenshot captured (base64 length: {len(screenshot_b64)} chars)')
|
||||
|
||||
# Debug: Check screenshot dimensions
|
||||
image = Image.open(BytesIO(base64.b64decode(screenshot_b64)))
|
||||
print(f'📏 Screenshot actual dimensions: {image.size[0]}x{image.size[1]}')
|
||||
|
||||
# rescale the screenshot to the viewport size
|
||||
image = image.resize((page_info.viewport_width, page_info.viewport_height))
|
||||
# Save as PNG to bytes buffer
|
||||
buffer = BytesIO()
|
||||
image.save(buffer, format='PNG')
|
||||
buffer.seek(0)
|
||||
# Convert to base64
|
||||
screenshot_b64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
|
||||
print(f'📸 Rescaled screenshot to viewport size: {page_info.viewport_width}x{page_info.viewport_height}')
|
||||
|
||||
client = AsyncOpenAI(api_key=os.getenv('OPENAI_API_KEY'))
|
||||
print('🔄 Sending request to OpenAI CUA...')
|
||||
|
||||
prompt = f"""
|
||||
You will be given an action to execute and screenshot of the current screen.
|
||||
Output one computer_call object that will achieve this goal.
|
||||
Goal: {params.description}
|
||||
"""
|
||||
response = await client.responses.create(
|
||||
model='computer-use-preview',
|
||||
tools=[
|
||||
{
|
||||
'type': 'computer_use_preview',
|
||||
'display_width': page_info.viewport_width,
|
||||
'display_height': page_info.viewport_height,
|
||||
'environment': 'browser',
|
||||
}
|
||||
],
|
||||
input=[
|
||||
{
|
||||
'role': 'user',
|
||||
'content': [
|
||||
{'type': 'input_text', 'text': prompt},
|
||||
{
|
||||
'type': 'input_image',
|
||||
'detail': 'auto',
|
||||
'image_url': f'data:image/png;base64,{screenshot_b64}',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
truncation='auto',
|
||||
temperature=0.1,
|
||||
)
|
||||
|
||||
print(f'📥 CUA response received: {response}')
|
||||
computer_calls = [item for item in response.output if item.type == 'computer_call']
|
||||
computer_call = computer_calls[0] if computer_calls else None
|
||||
if not computer_call:
|
||||
raise Exception('No computer calls found in CUA response')
|
||||
|
||||
action = computer_call.action
|
||||
print(f'🎬 Executing CUA action: {action.type} - {action}')
|
||||
|
||||
action_result = await handle_model_action(browser_session, action)
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
print('✅ CUA action completed successfully')
|
||||
return action_result
|
||||
|
||||
except Exception as e:
|
||||
msg = f'Error executing CUA action: {e}'
|
||||
print(f'❌ {msg}')
|
||||
return ActionResult(error=msg)
|
||||
|
||||
|
||||
async def main():
|
||||
# Initialize the language model
|
||||
llm = ChatOpenAI(
|
||||
model='o4-mini',
|
||||
temperature=1.0,
|
||||
)
|
||||
|
||||
# Create browser session
|
||||
browser_session = BrowserSession()
|
||||
|
||||
# Example task that might require CUA fallback
|
||||
# This could be a complex interaction that's difficult with standard actions
|
||||
task = """
|
||||
Go to https://csreis.github.io/tests/cross-site-iframe.html
|
||||
Click on "Go cross-site, complex page" using index
|
||||
Use the OpenAI CUA fallback to click on "Tree is open..." link.
|
||||
"""
|
||||
|
||||
# Create agent with our custom tools that includes CUA fallback
|
||||
agent = Agent(
|
||||
task=task,
|
||||
llm=llm,
|
||||
tools=tools,
|
||||
browser_session=browser_session,
|
||||
)
|
||||
|
||||
print('🚀 Starting agent with CUA fallback support...')
|
||||
print(f'Task: {task}')
|
||||
print('-' * 50)
|
||||
|
||||
try:
|
||||
# Run the agent
|
||||
result = await agent.run()
|
||||
print(f'\n✅ Task completed! Result: {result}')
|
||||
|
||||
except Exception as e:
|
||||
print(f'\n❌ Error running agent: {e}')
|
||||
|
||||
finally:
|
||||
# Clean up browser session
|
||||
await browser_session.kill()
|
||||
print('\n🧹 Browser session closed')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Example of different scenarios where CUA might be useful
|
||||
|
||||
print('🔧 OpenAI Computer Use Assistant (CUA) Integration Example')
|
||||
print('=' * 60)
|
||||
print()
|
||||
print("This example shows how to integrate OpenAI's CUA as a fallback action")
|
||||
print('when standard browser-use actions cannot achieve the desired goal.')
|
||||
print()
|
||||
print('CUA is particularly useful for:')
|
||||
print('• Complex mouse interactions (drag & drop, precise clicking)')
|
||||
print('• Keyboard shortcuts and key combinations')
|
||||
print('• Actions that require pixel-perfect precision')
|
||||
print("• Custom UI elements that don't respond to standard actions")
|
||||
print()
|
||||
print('Make sure you have OPENAI_API_KEY set in your environment!')
|
||||
print()
|
||||
|
||||
# Check if OpenAI API key is available
|
||||
if not os.getenv('OPENAI_API_KEY'):
|
||||
print('❌ Error: OPENAI_API_KEY environment variable not set')
|
||||
print('Please set your OpenAI API key to use CUA integration')
|
||||
sys.exit(1)
|
||||
|
||||
# Run the example
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
Example of implementing file upload functionality.
|
||||
|
||||
This shows how to upload files to file input elements on web pages.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import anyio
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from browser_use import ChatOpenAI
|
||||
from browser_use.agent.service import Agent, Tools
|
||||
from browser_use.agent.views import ActionResult
|
||||
from browser_use.browser import BrowserSession
|
||||
from browser_use.browser.events import UploadFileEvent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Initialize tools
|
||||
tools = Tools()
|
||||
|
||||
|
||||
@tools.action('Upload file to interactive element with file path')
|
||||
async def upload_file(index: int, path: str, browser_session: BrowserSession, available_file_paths: list[str]):
|
||||
if path not in available_file_paths:
|
||||
return ActionResult(error=f'File path {path} is not available')
|
||||
|
||||
if not os.path.exists(path):
|
||||
return ActionResult(error=f'File {path} does not exist')
|
||||
|
||||
try:
|
||||
# Get the DOM element by index
|
||||
dom_element = await browser_session.get_dom_element_by_index(index)
|
||||
|
||||
if dom_element is None:
|
||||
msg = f'No element found at index {index}'
|
||||
logger.info(msg)
|
||||
return ActionResult(error=msg)
|
||||
|
||||
# Check if it's a file input element
|
||||
if dom_element.tag_name.lower() != 'input' or dom_element.attributes.get('type') != 'file':
|
||||
msg = f'Element at index {index} is not a file input element'
|
||||
logger.info(msg)
|
||||
return ActionResult(error=msg)
|
||||
|
||||
# Dispatch the upload file event
|
||||
event = browser_session.event_bus.dispatch(UploadFileEvent(node=dom_element, file_path=path))
|
||||
await event
|
||||
|
||||
msg = f'Successfully uploaded file to index {index}'
|
||||
logger.info(msg)
|
||||
return ActionResult(extracted_content=msg, include_in_memory=True)
|
||||
|
||||
except Exception as e:
|
||||
msg = f'Failed to upload file to index {index}: {str(e)}'
|
||||
logger.info(msg)
|
||||
return ActionResult(error=msg)
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main function to run the example"""
|
||||
browser_session = BrowserSession()
|
||||
await browser_session.start()
|
||||
llm = ChatOpenAI(model='gpt-4.1-mini')
|
||||
|
||||
# List of file paths the agent is allowed to upload
|
||||
# In a real scenario, you'd want to be very careful about what files
|
||||
# the agent can access and upload
|
||||
available_file_paths = [
|
||||
'/tmp/test_document.pdf',
|
||||
'/tmp/test_image.jpg',
|
||||
]
|
||||
|
||||
# Create test files if they don't exist
|
||||
for file_path in available_file_paths:
|
||||
if not os.path.exists(file_path):
|
||||
await anyio.Path(file_path).write_text('Test file content for upload example')
|
||||
|
||||
# Create the agent with file upload capability
|
||||
agent = Agent(
|
||||
task="""
|
||||
Go to https://www.w3schools.com/howto/howto_html_file_upload_button.asp and try to upload one of the available test files.
|
||||
""",
|
||||
llm=llm,
|
||||
browser_session=browser_session,
|
||||
tools=tools,
|
||||
# Pass the available file paths to the tools context
|
||||
custom_context={'available_file_paths': available_file_paths},
|
||||
)
|
||||
|
||||
# Run the agent
|
||||
await agent.run(max_steps=10)
|
||||
|
||||
# Cleanup
|
||||
await browser_session.kill()
|
||||
|
||||
# Clean up test files
|
||||
for file_path in available_file_paths:
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,43 @@
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from browser_use import ActionResult, Agent, ChatOpenAI, Tools
|
||||
|
||||
tools = Tools()
|
||||
|
||||
|
||||
@tools.registry.action('Done with task')
|
||||
async def done(text: str):
|
||||
import yagmail # type: ignore
|
||||
|
||||
# To send emails use
|
||||
# STEP 1: go to https://support.google.com/accounts/answer/185833
|
||||
# STEP 2: Create an app password (you can't use here your normal gmail password)
|
||||
# STEP 3: Use the app password in the code below for the password
|
||||
yag = yagmail.SMTP('your_email@gmail.com', 'your_app_password')
|
||||
yag.send(
|
||||
to='recipient@example.com',
|
||||
subject='Test Email',
|
||||
contents=f'result\n: {text}',
|
||||
)
|
||||
|
||||
return ActionResult(is_done=True, extracted_content='Email sent!')
|
||||
|
||||
|
||||
async def main():
|
||||
task = 'go to brower-use.com and then done'
|
||||
model = ChatOpenAI(model='gpt-4.1-mini')
|
||||
agent = Agent(task=task, llm=model, tools=tools)
|
||||
|
||||
await agent.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,56 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from onepassword.client import Client # type: ignore # pip install onepassword-sdk
|
||||
|
||||
from browser_use import ActionResult, Agent, ChatOpenAI, Tools
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
OP_SERVICE_ACCOUNT_TOKEN = os.getenv('OP_SERVICE_ACCOUNT_TOKEN')
|
||||
OP_ITEM_ID = os.getenv('OP_ITEM_ID') # Go to 1Password, right click on the item, click "Copy Secret Reference"
|
||||
|
||||
|
||||
tools = Tools()
|
||||
|
||||
|
||||
@tools.registry.action('Get 2FA code from 1Password for Google Account', domains=['*.google.com', 'google.com'])
|
||||
async def get_1password_2fa() -> ActionResult:
|
||||
"""
|
||||
Custom action to retrieve 2FA/MFA code from 1Password using onepassword.client SDK.
|
||||
"""
|
||||
client = await Client.authenticate(
|
||||
# setup instructions: https://github.com/1Password/onepassword-sdk-python/#-get-started
|
||||
auth=OP_SERVICE_ACCOUNT_TOKEN,
|
||||
integration_name='Browser-Use',
|
||||
integration_version='v1.0.0',
|
||||
)
|
||||
|
||||
mfa_code = await client.secrets.resolve(f'op://Private/{OP_ITEM_ID}/One-time passcode')
|
||||
|
||||
return ActionResult(extracted_content=mfa_code)
|
||||
|
||||
|
||||
async def main():
|
||||
# Example task using the 1Password 2FA action
|
||||
task = 'Go to account.google.com, enter username and password, then if prompted for 2FA code, get 2FA code from 1Password for and enter it'
|
||||
|
||||
model = ChatOpenAI(model='gpt-4.1-mini')
|
||||
agent = Agent(task=task, llm=model, tools=tools)
|
||||
|
||||
result = await agent.run()
|
||||
print(f'Task completed with result: {result}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,310 @@
|
||||
"""
|
||||
Simple parallel multi-agent example.
|
||||
|
||||
This launches multiple agents in parallel to work on different tasks simultaneously.
|
||||
No complex orchestrator - just direct parallel execution.
|
||||
|
||||
@file purpose: Demonstrates parallel multi-agent execution using asyncio
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from browser_use import Agent
|
||||
from browser_use.llm.google import ChatGoogle
|
||||
|
||||
# ============================================================================
|
||||
# 🔧 SIMPLE CONFIGURATION - CHANGE THIS TO YOUR DESIRED TASK
|
||||
# ============================================================================
|
||||
|
||||
MAIN_TASK = 'find age of ronaldo and messi'
|
||||
|
||||
# Simple test - let's start with just one person to see what happens
|
||||
# MAIN_TASK = "find age of elon musk"
|
||||
|
||||
# ============================================================================
|
||||
|
||||
|
||||
async def create_subtasks(main_task: str, llm) -> list[str]:
|
||||
"""
|
||||
Use LLM to break down main task into logical subtasks
|
||||
|
||||
Real examples of how this works:
|
||||
|
||||
Input: "what is the revenue of nvidia, microsoft, tesla"
|
||||
Output: [
|
||||
"Find Nvidia's current revenue and financial data",
|
||||
"Find Microsoft's current revenue and financial data",
|
||||
"Find Tesla's current revenue and financial data"
|
||||
]
|
||||
|
||||
Input: "what are ages of musk, altman, bezos, gates"
|
||||
Output: [
|
||||
"Find Elon Musk's age and birth date",
|
||||
"Find Sam Altman's age and birth date",
|
||||
"Find Jeff Bezos's age and birth date",
|
||||
"Find Bill Gates's age and birth date"
|
||||
]
|
||||
|
||||
Input: "what is the population of tokyo, new york, london, paris"
|
||||
Output: [
|
||||
"Find Tokyo's current population",
|
||||
"Find New York's current population",
|
||||
"Find London's current population",
|
||||
"Find Paris's current population"
|
||||
]
|
||||
|
||||
Input: "name top 10 yc companies by revenue"
|
||||
Output: [
|
||||
"Research Y Combinator's top companies by revenue",
|
||||
"Find revenue data for top YC companies",
|
||||
"Compile list of top 10 YC companies by revenue"
|
||||
]
|
||||
"""
|
||||
|
||||
prompt = f"""
|
||||
Break down this main task into individual, separate subtasks where each subtask focuses on ONLY ONE specific person, company, or item:
|
||||
|
||||
Main task: {main_task}
|
||||
|
||||
RULES:
|
||||
- Each subtask must focus on ONLY ONE person/company/item
|
||||
- Do NOT combine multiple people/companies/items in one subtask
|
||||
- Each subtask should be completely independent
|
||||
- If the main task mentions multiple items, create one subtask per item
|
||||
|
||||
Return only the subtasks, one per line, without numbering or bullets.
|
||||
Each line should focus on exactly ONE person/company/item.
|
||||
"""
|
||||
|
||||
try:
|
||||
# Use the correct method for ChatGoogle
|
||||
response = await llm.ainvoke(prompt)
|
||||
|
||||
# Debug: Print the response type and content
|
||||
print(f'DEBUG: Response type: {type(response)}')
|
||||
print(f'DEBUG: Response content: {response}')
|
||||
|
||||
# Handle different response types - ChatGoogle returns string content
|
||||
if hasattr(response, 'content'):
|
||||
content = response.content
|
||||
elif isinstance(response, str):
|
||||
content = response
|
||||
elif hasattr(response, 'text'):
|
||||
content = response.text
|
||||
else:
|
||||
# Convert to string if it's some other type
|
||||
content = str(response)
|
||||
|
||||
# Split by newlines and clean up
|
||||
subtasks = [task.strip() for task in content.strip().split('\n') if task.strip()]
|
||||
|
||||
# Remove any numbering or bullets that the LLM might add
|
||||
cleaned_subtasks = []
|
||||
for task in subtasks:
|
||||
# Remove common prefixes like "1. ", "- ", "* ", etc.
|
||||
cleaned = task.lstrip('0123456789.-* ')
|
||||
if cleaned:
|
||||
cleaned_subtasks.append(cleaned)
|
||||
|
||||
return cleaned_subtasks if cleaned_subtasks else simple_split_task(main_task)
|
||||
except Exception as e:
|
||||
print(f'Error creating subtasks: {e}')
|
||||
# Fallback to simple split
|
||||
return simple_split_task(main_task)
|
||||
|
||||
|
||||
def simple_split_task(main_task: str) -> list[str]:
|
||||
"""Simple fallback: split task by common separators"""
|
||||
task_lower = main_task.lower()
|
||||
|
||||
# Try to split by common separators
|
||||
if ' and ' in task_lower:
|
||||
parts = main_task.split(' and ')
|
||||
return [part.strip() for part in parts if part.strip()]
|
||||
elif ', ' in main_task:
|
||||
parts = main_task.split(', ')
|
||||
return [part.strip() for part in parts if part.strip()]
|
||||
elif ',' in main_task:
|
||||
parts = main_task.split(',')
|
||||
return [part.strip() for part in parts if part.strip()]
|
||||
|
||||
# If no separators found, return the original task
|
||||
return [main_task]
|
||||
|
||||
|
||||
async def run_single_agent(task: str, llm, agent_id: int) -> tuple[int, str]:
|
||||
"""Run a single agent and return its result"""
|
||||
print(f'🚀 Agent {agent_id} starting: {task}')
|
||||
print(f' 📝 This agent will focus ONLY on: {task}')
|
||||
print(f' 🌐 Creating isolated browser instance for agent {agent_id}')
|
||||
|
||||
try:
|
||||
# Create agent with its own browser session (separate browser instance)
|
||||
import tempfile
|
||||
|
||||
from browser_use.browser import BrowserSession
|
||||
from browser_use.browser.profile import BrowserProfile
|
||||
|
||||
# Create a unique temp directory for this agent's browser data
|
||||
temp_dir = tempfile.mkdtemp(prefix=f'browser_agent_{agent_id}_')
|
||||
|
||||
# Create browser profile with custom user data directory and single tab focus
|
||||
profile = BrowserProfile()
|
||||
profile.user_data_dir = temp_dir
|
||||
profile.headless = False # Set to True if you want headless mode
|
||||
profile.keep_alive = False # Don't keep browser alive after task
|
||||
|
||||
# Add custom args to prevent new tabs and popups
|
||||
profile.args = [
|
||||
'--disable-popup-blocking',
|
||||
'--disable-extensions',
|
||||
'--disable-plugins',
|
||||
'--disable-images', # Faster loading
|
||||
'--no-first-run',
|
||||
'--disable-default-apps',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-backgrounding-occluded-windows',
|
||||
'--disable-renderer-backgrounding',
|
||||
]
|
||||
|
||||
# Create a new browser session for each agent with the custom profile
|
||||
browser_session = BrowserSession(browser_profile=profile)
|
||||
|
||||
# Debug: Check initial tab count
|
||||
try:
|
||||
await browser_session.start()
|
||||
initial_tabs = await browser_session._cdp_get_all_pages()
|
||||
print(f' 📊 Agent {agent_id} initial tab count: {len(initial_tabs)}')
|
||||
except Exception as e:
|
||||
print(f' ⚠️ Could not check initial tabs for agent {agent_id}: {e}')
|
||||
|
||||
# Create agent with the dedicated browser session and disable auto URL detection
|
||||
agent = Agent(task=task, llm=llm, browser_session=browser_session, preload=False)
|
||||
|
||||
# Run the agent with timeout to prevent hanging
|
||||
try:
|
||||
result = await asyncio.wait_for(agent.run(), timeout=300) # 5 minute timeout
|
||||
except TimeoutError:
|
||||
print(f'⏰ Agent {agent_id} timed out after 5 minutes')
|
||||
result = 'Task timed out'
|
||||
|
||||
# Debug: Check final tab count
|
||||
try:
|
||||
final_tabs = await browser_session._cdp_get_all_pages()
|
||||
print(f' 📊 Agent {agent_id} final tab count: {len(final_tabs)}')
|
||||
for i, tab in enumerate(final_tabs):
|
||||
print(f' Tab {i + 1}: {tab.get("url", "unknown")[:50]}...')
|
||||
except Exception as e:
|
||||
print(f' ⚠️ Could not check final tabs for agent {agent_id}: {e}')
|
||||
|
||||
# Extract clean result from the agent history
|
||||
clean_result = extract_clean_result(result)
|
||||
|
||||
# Close the browser session for this agent
|
||||
try:
|
||||
await browser_session.kill()
|
||||
except Exception as e:
|
||||
print(f'⚠️ Warning: Error closing browser for agent {agent_id}: {e}')
|
||||
|
||||
print(f'✅ Agent {agent_id} completed and browser closed: {task}')
|
||||
|
||||
return agent_id, clean_result
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f'Agent {agent_id} failed: {str(e)}'
|
||||
print(f'❌ {error_msg}')
|
||||
return agent_id, error_msg
|
||||
|
||||
|
||||
def extract_clean_result(agent_result) -> str:
|
||||
"""Extract clean result from agent history"""
|
||||
try:
|
||||
# Get the last result from the agent history
|
||||
if hasattr(agent_result, 'all_results') and agent_result.all_results:
|
||||
last_result = agent_result.all_results[-1]
|
||||
if hasattr(last_result, 'extracted_content') and last_result.extracted_content:
|
||||
return last_result.extracted_content
|
||||
|
||||
# Fallback to string representation
|
||||
return str(agent_result)
|
||||
except Exception:
|
||||
return 'Result extraction failed'
|
||||
|
||||
|
||||
async def run_parallel_agents():
|
||||
"""Run multiple agents in parallel on different tasks"""
|
||||
|
||||
# Use Gemini 1.5 Flash
|
||||
llm = ChatGoogle(model='gemini-1.5-flash')
|
||||
|
||||
# Main task to break down - use the simple configuration
|
||||
main_task = MAIN_TASK
|
||||
|
||||
print(f'🎯 Main task: {main_task}')
|
||||
print('🧠 Creating subtasks using LLM...')
|
||||
|
||||
# Create subtasks using LLM
|
||||
subtasks = await create_subtasks(main_task, llm)
|
||||
|
||||
print(f'📋 Created {len(subtasks)} subtasks:')
|
||||
for i, task in enumerate(subtasks, 1):
|
||||
print(f' {i}. {task}')
|
||||
|
||||
print(f'\n🔥 Starting {len(subtasks)} agents in parallel...')
|
||||
print('🔍 Each agent will get its own browser instance with exactly ONE tab')
|
||||
print(f'📊 Expected: {len(subtasks)} browser instances, {len(subtasks)} tabs total')
|
||||
|
||||
# Create tasks for parallel execution
|
||||
agent_tasks = [run_single_agent(task, llm, i + 1) for i, task in enumerate(subtasks)]
|
||||
|
||||
# Run all agents in parallel using asyncio.gather
|
||||
results = await asyncio.gather(*agent_tasks)
|
||||
|
||||
# Print results
|
||||
print('\n' + '=' * 60)
|
||||
print('📊 PARALLEL EXECUTION RESULTS')
|
||||
print('=' * 60)
|
||||
|
||||
for agent_id, result in results:
|
||||
print(f'\n🤖 Agent {agent_id} result:')
|
||||
print(f'Task: {subtasks[agent_id - 1]}')
|
||||
print(f'Result: {result}')
|
||||
print('-' * 50)
|
||||
|
||||
print(f'\n🎉 All {len(subtasks)} parallel agents completed!')
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function to run parallel agents"""
|
||||
# Check if Google API key is available
|
||||
api_key = os.getenv('GOOGLE_API_KEY')
|
||||
if not api_key:
|
||||
print('❌ Error: GOOGLE_API_KEY environment variable not set')
|
||||
print('Please set your Google API key to use parallel agents')
|
||||
print('You can set it with: export GOOGLE_API_KEY="your-key-here"')
|
||||
sys.exit(1)
|
||||
|
||||
# Check if API key looks valid (Google API keys are typically 39 characters)
|
||||
if len(api_key) < 20:
|
||||
print(f'⚠️ Warning: GOOGLE_API_KEY seems too short ({len(api_key)} characters)')
|
||||
print('Google API keys are typically 39 characters long')
|
||||
print('Continuing anyway, but this might cause authentication issues...')
|
||||
|
||||
print('🚀 Starting parallel multi-agent example...')
|
||||
print(f'📝 Task: {MAIN_TASK}')
|
||||
print('This will dynamically create agents based on task complexity')
|
||||
print('-' * 60)
|
||||
|
||||
asyncio.run(run_parallel_agents())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,50 @@
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from browser_use import ChatOpenAI
|
||||
from browser_use.agent.service import Agent
|
||||
from browser_use.tools.service import Tools
|
||||
|
||||
# Initialize tools first
|
||||
tools = Tools()
|
||||
|
||||
|
||||
class Model(BaseModel):
|
||||
title: str
|
||||
url: str
|
||||
likes: int
|
||||
license: str
|
||||
|
||||
|
||||
class Models(BaseModel):
|
||||
models: list[Model]
|
||||
|
||||
|
||||
@tools.action('Save models', param_model=Models)
|
||||
def save_models(params: Models):
|
||||
with open('models.txt', 'a') as f:
|
||||
for model in params.models:
|
||||
f.write(f'{model.title} ({model.url}): {model.likes} likes, {model.license}\n')
|
||||
|
||||
|
||||
# video: https://preview.screen.studio/share/EtOhIk0P
|
||||
async def main():
|
||||
task = 'Look up models with a license of cc-by-sa-4.0 and sort by most likes on Hugging face, save top 5 to file.'
|
||||
|
||||
model = ChatOpenAI(model='gpt-4.1-mini')
|
||||
agent = Agent(task=task, llm=model, tools=tools)
|
||||
|
||||
await agent.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,16 @@
|
||||
import asyncio
|
||||
|
||||
from browser_use import Agent, ChatBrowserUse
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
agent = Agent(
|
||||
task='Please find the latest commit on browser-use/browser-use repo and tell me the commit message. Please summarize what it is about.',
|
||||
llm=ChatBrowserUse(model='bu-2-0'),
|
||||
demo_mode=True,
|
||||
)
|
||||
await agent.run(max_steps=5)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
Show how to use sample_images to add image context for your task
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from browser_use import Agent
|
||||
from browser_use.llm import ChatOpenAI
|
||||
from browser_use.llm.messages import ContentPartImageParam, ContentPartTextParam, ImageURL
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def image_to_base64(image_path: str) -> str:
|
||||
"""
|
||||
Convert image file to base64 string.
|
||||
|
||||
Args:
|
||||
image_path: Path to the image file
|
||||
|
||||
Returns:
|
||||
Base64 encoded string of the image
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If image file doesn't exist
|
||||
IOError: If image file cannot be read
|
||||
"""
|
||||
image_file = Path(image_path)
|
||||
if not image_file.exists():
|
||||
raise FileNotFoundError(f'Image file not found: {image_path}')
|
||||
|
||||
try:
|
||||
with open(image_file, 'rb') as f:
|
||||
encoded_string = base64.b64encode(f.read())
|
||||
return encoded_string.decode('utf-8')
|
||||
except OSError as e:
|
||||
raise OSError(f'Failed to read image file: {e}')
|
||||
|
||||
|
||||
def create_sample_images() -> list[ContentPartTextParam | ContentPartImageParam]:
|
||||
"""
|
||||
Create image context for the agent.
|
||||
|
||||
Returns:
|
||||
list of content parts containing text and image data
|
||||
"""
|
||||
# Image path - replace with your actual image path
|
||||
image_path = 'sample_image.png'
|
||||
|
||||
# Image context configuration
|
||||
image_context: list[dict[str, Any]] = [
|
||||
{
|
||||
'type': 'text',
|
||||
'value': (
|
||||
'The following image explains the google layout. '
|
||||
'The image highlights several buttons with red boxes, '
|
||||
'and next to them are corresponding labels in red text.\n'
|
||||
'Each label corresponds to a button as follows:\n'
|
||||
'Label 1 is the "image" button.'
|
||||
),
|
||||
},
|
||||
{'type': 'image', 'value': image_to_base64(image_path)},
|
||||
]
|
||||
|
||||
# Convert to content parts
|
||||
content_parts = []
|
||||
for item in image_context:
|
||||
if item['type'] == 'text':
|
||||
content_parts.append(ContentPartTextParam(text=item['value']))
|
||||
elif item['type'] == 'image':
|
||||
content_parts.append(
|
||||
ContentPartImageParam(
|
||||
image_url=ImageURL(
|
||||
url=f'data:image/jpeg;base64,{item["value"]}',
|
||||
media_type='image/jpeg',
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return content_parts
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""
|
||||
Main function to run the browser agent with image context.
|
||||
"""
|
||||
# Task configuration
|
||||
task_str = 'goto https://www.google.com/ and click image button'
|
||||
|
||||
# Initialize the language model
|
||||
model = ChatOpenAI(model='gpt-4.1')
|
||||
|
||||
# Create sample images for context
|
||||
try:
|
||||
sample_images = create_sample_images()
|
||||
except (FileNotFoundError, OSError) as e:
|
||||
print(f'Error loading sample images: {e}')
|
||||
print('Continuing without sample images...')
|
||||
sample_images = []
|
||||
|
||||
# Initialize and run the agent
|
||||
agent = Agent(task=task_str, llm=model, sample_images=sample_images)
|
||||
await agent.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,64 @@
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from browser_use import Agent, ChatOpenAI
|
||||
from browser_use.browser import BrowserProfile, BrowserSession
|
||||
|
||||
llm = ChatOpenAI(model='gpt-4o-mini')
|
||||
|
||||
# Example task: Try to navigate to various sites including blocked ones
|
||||
task = 'Navigate to example.com, then try to go to x.com, then facebook.com, and finally visit google.com. Tell me which sites you were able to access.'
|
||||
|
||||
prohibited_domains = [
|
||||
'x.com', # Block X (formerly Twitter) - "locked the f in"
|
||||
'twitter.com', # Block Twitter (redirects to x.com anyway)
|
||||
'facebook.com', # Lock the F in Facebook too
|
||||
'*.meta.com', # Block all Meta properties (wildcard pattern)
|
||||
'*.adult-site.com', # Block all subdomains of adult sites
|
||||
'https://explicit-content.org', # Block specific protocol/domain
|
||||
'gambling-site.net', # Block gambling sites
|
||||
]
|
||||
|
||||
# Note: For lists with 100+ domains, automatic optimization kicks in:
|
||||
# - Converts list to set for O(1) lookup (blazingly fast!)
|
||||
# - Pattern matching (*.domain) is disabled for large lists
|
||||
# - Both www.example.com and example.com variants are checked automatically
|
||||
# Perfect for ad blockers or large malware domain lists (e.g., 400k+ domains)
|
||||
|
||||
browser_session = BrowserSession(
|
||||
browser_profile=BrowserProfile(
|
||||
prohibited_domains=prohibited_domains,
|
||||
headless=False, # Set to True to run without visible browser
|
||||
user_data_dir='~/.config/browseruse/profiles/blocked-demo',
|
||||
),
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
task=task,
|
||||
llm=llm,
|
||||
browser_session=browser_session,
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
print('Demo: Blocked Domains Feature - "Lock the F in" Edition')
|
||||
print("We're literally locking the F in Facebook and X!")
|
||||
print(f'Prohibited domains: {prohibited_domains}')
|
||||
print('The agent will try to visit various sites, but blocked domains will be prevented.')
|
||||
print()
|
||||
|
||||
await agent.run(max_steps=10)
|
||||
|
||||
input('Press Enter to close the browser...')
|
||||
await browser_session.kill()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,51 @@
|
||||
"""
|
||||
Generate CSV files with automatic normalization.
|
||||
|
||||
The agent's file system automatically normalizes CSV output using Python's csv module,
|
||||
so fields containing commas, quotes, or empty values are properly handled per RFC 4180.
|
||||
This means the agent doesn't need to worry about manual quoting — it's fixed at the
|
||||
infrastructure level.
|
||||
|
||||
Common LLM mistakes that are auto-corrected:
|
||||
- Unquoted fields containing commas (e.g. "San Francisco, CA" without quotes)
|
||||
- Unescaped double quotes inside fields
|
||||
- Inconsistent empty field handling
|
||||
- Stray blank lines
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from browser_use import Agent, ChatBrowserUse
|
||||
|
||||
|
||||
async def main():
|
||||
agent = Agent(
|
||||
task=(
|
||||
'Go to https://en.wikipedia.org/wiki/List_of_largest_cities and extract the top 10 cities. '
|
||||
'Create a CSV file called "top_cities.csv" with columns: rank, city name, country, population. '
|
||||
'Make sure to include all cities even if some data is missing — leave those cells empty.'
|
||||
),
|
||||
llm=ChatBrowserUse(model='bu-2-0'),
|
||||
)
|
||||
|
||||
history = await agent.run()
|
||||
|
||||
# Check the generated CSV file
|
||||
if agent.file_system:
|
||||
csv_file = agent.file_system.get_file('top_cities.csv')
|
||||
if csv_file:
|
||||
print('\nGenerated CSV content:')
|
||||
print(csv_file.content)
|
||||
print(f'\nFile saved to: {agent.file_system.get_dir() / csv_file.full_name}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
Show how to use custom outputs.
|
||||
|
||||
@dev You need to add OPENAI_API_KEY to your environment variables.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from browser_use import Agent, ChatOpenAI
|
||||
|
||||
|
||||
class Post(BaseModel):
|
||||
post_title: str
|
||||
post_url: str
|
||||
num_comments: int
|
||||
hours_since_post: int
|
||||
|
||||
|
||||
class Posts(BaseModel):
|
||||
posts: list[Post]
|
||||
|
||||
|
||||
async def main():
|
||||
task = 'Go to hackernews show hn and give me the first 5 posts'
|
||||
model = ChatOpenAI(model='gpt-4.1-mini')
|
||||
agent = Agent(task=task, llm=model, output_model_schema=Posts)
|
||||
|
||||
history = await agent.run()
|
||||
|
||||
result = history.final_result()
|
||||
if result:
|
||||
parsed: Posts = Posts.model_validate_json(result)
|
||||
|
||||
for post in parsed.posts:
|
||||
print('\n--------------------------------')
|
||||
print(f'Title: {post.post_title}')
|
||||
print(f'URL: {post.post_url}')
|
||||
print(f'Comments: {post.num_comments}')
|
||||
print(f'Hours since post: {post.hours_since_post}')
|
||||
else:
|
||||
print('No result')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,38 @@
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
from browser_use import Agent, ChatOpenAI
|
||||
|
||||
extend_system_message = (
|
||||
'REMEMBER the most important RULE: ALWAYS open first a new tab and go first to url wikipedia.com no matter the task!!!'
|
||||
)
|
||||
|
||||
# or use override_system_message to completely override the system prompt
|
||||
|
||||
|
||||
async def main():
|
||||
task = 'do google search to find images of Elon Musk'
|
||||
model = ChatOpenAI(model='gpt-4.1-mini')
|
||||
agent = Agent(task=task, llm=model, extend_system_message=extend_system_message)
|
||||
|
||||
print(
|
||||
json.dumps(
|
||||
agent.message_manager.system_prompt.model_dump(exclude_unset=True),
|
||||
indent=4,
|
||||
)
|
||||
)
|
||||
|
||||
await agent.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,34 @@
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
from browser_use import Agent, Browser, ChatGoogle
|
||||
|
||||
api_key = os.getenv('GOOGLE_API_KEY')
|
||||
if not api_key:
|
||||
raise ValueError('GOOGLE_API_KEY is not set')
|
||||
|
||||
llm = ChatGoogle(model='gemini-2.5-flash', api_key=api_key)
|
||||
|
||||
|
||||
browser = Browser(downloads_path='~/Downloads/tmp')
|
||||
|
||||
|
||||
async def run_download():
|
||||
agent = Agent(
|
||||
task='Go to "https://file-examples.com/" and download the smallest doc file. then go back and get the next file.',
|
||||
llm=llm,
|
||||
browser=browser,
|
||||
)
|
||||
await agent.run(max_steps=25)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(run_download())
|
||||
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
Example: Using a fallback LLM model.
|
||||
|
||||
When the primary LLM fails with rate limits (429), authentication errors (401),
|
||||
payment/credit errors (402), or server errors (500, 502, 503, 504), the agent
|
||||
automatically switches to the fallback model and continues execution.
|
||||
|
||||
Note: The primary LLM will first exhaust its own retry logic (typically 5 attempts
|
||||
with exponential backoff) before the fallback is triggered. This means transient errors
|
||||
are handled by the provider's built-in retries, and the fallback only kicks in when
|
||||
the provider truly can't recover.
|
||||
|
||||
This is useful for:
|
||||
- High availability: Keep your agent running even when one provider has issues
|
||||
- Cost optimization: Use a cheaper model as fallback when the primary is rate limited
|
||||
- Multi-provider resilience: Switch between OpenAI, Anthropic, Google, etc.
|
||||
|
||||
@dev You need to add OPENAI_API_KEY and ANTHROPIC_API_KEY to your environment variables.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from browser_use import Agent
|
||||
from browser_use.llm import ChatAnthropic, ChatOpenAI
|
||||
|
||||
llm = ChatAnthropic(model='claude-sonnet-4-0')
|
||||
fallback_llm = ChatOpenAI(model='gpt-4o')
|
||||
|
||||
agent = Agent(
|
||||
task='Go to github.com and find the browser-use repository',
|
||||
llm=llm,
|
||||
fallback_llm=fallback_llm,
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
result = await agent.run()
|
||||
print(result)
|
||||
|
||||
# You can check if fallback was used:
|
||||
if agent.is_using_fallback_llm:
|
||||
print('Note: Agent switched to fallback LLM during execution')
|
||||
print(f'Current model: {agent.current_llm_model}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,24 @@
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from browser_use import Agent, Browser
|
||||
|
||||
load_dotenv()
|
||||
|
||||
import asyncio
|
||||
|
||||
|
||||
async def main():
|
||||
browser = Browser(keep_alive=True)
|
||||
|
||||
await browser.start()
|
||||
|
||||
agent = Agent(task='search for browser-use.', browser_session=browser)
|
||||
await agent.run(max_steps=2)
|
||||
agent.add_new_task('return the title of first result')
|
||||
await agent.run()
|
||||
|
||||
await browser.kill()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,32 @@
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
from browser_use.browser.profile import BrowserProfile
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from browser_use import Agent
|
||||
|
||||
profile = BrowserProfile(keep_alive=True)
|
||||
|
||||
|
||||
task = """Go to reddit.com"""
|
||||
|
||||
|
||||
async def main():
|
||||
agent = Agent(task=task, browser_profile=profile)
|
||||
await agent.run(max_steps=1)
|
||||
|
||||
while True:
|
||||
user_response = input('\n👤 New task or "q" to quit: ')
|
||||
agent.add_new_task(f'New task: {user_response}')
|
||||
await agent.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,31 @@
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from browser_use import Agent, ChatOpenAI
|
||||
|
||||
llm = ChatOpenAI(model='gpt-4.1-mini')
|
||||
|
||||
initial_actions = [
|
||||
{'navigate': {'url': 'https://www.google.com', 'new_tab': True}},
|
||||
{'navigate': {'url': 'https://en.wikipedia.org/wiki/Randomness', 'new_tab': True}},
|
||||
]
|
||||
agent = Agent(
|
||||
task='What theories are displayed on the page?',
|
||||
initial_actions=initial_actions,
|
||||
llm=llm,
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
await agent.run(max_steps=10)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,47 @@
|
||||
"""
|
||||
Setup:
|
||||
1. Get your API key from https://cloud.browser-use.com/new-api-key
|
||||
2. Set environment variable: export BROWSER_USE_API_KEY="your-key"
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add the parent directory to the path so we can import browser_use
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from browser_use import Agent
|
||||
from browser_use.llm.browser_use.chat import ChatBrowserUse
|
||||
|
||||
# task from GAIA
|
||||
task = """
|
||||
If Eliud Kipchoge could maintain his record-making marathon pace indefinitely, how many thousand hours would it take him to run the distance between the Earth and the Moon its closest approach?
|
||||
Please use the minimum perigee value on the Wikipedia page for the Moon when carrying out your calculation.
|
||||
Round your result to the nearest 1000 hours and do not use any comma separators if necessary.
|
||||
"""
|
||||
|
||||
|
||||
async def main():
|
||||
llm = ChatBrowserUse(model='bu-2-0')
|
||||
agent = Agent(
|
||||
task=task,
|
||||
llm=llm,
|
||||
use_judge=True,
|
||||
judge_llm=llm,
|
||||
ground_truth='16', # The TRUE answer is 17 but we put 16 to demonstrate judge can detect when the answer is wrong.
|
||||
)
|
||||
history = await agent.run()
|
||||
|
||||
# Get the judgement result
|
||||
if history.is_judged():
|
||||
judgement = history.judgement()
|
||||
print(f'Agent history judgement: {judgement}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,117 @@
|
||||
"""
|
||||
Example: Using large blocklists (400k+ domains) with automatic optimization
|
||||
|
||||
This example demonstrates:
|
||||
1. Loading a real-world blocklist (HaGeZi's Pro++ with 439k+ domains)
|
||||
2. Automatic conversion to set for O(1) lookup performance
|
||||
3. Testing that blocked domains are actually blocked
|
||||
|
||||
Performance: ~0.02ms per domain check (50,000+ checks/second!)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from browser_use import Agent, ChatOpenAI
|
||||
from browser_use.browser import BrowserProfile, BrowserSession
|
||||
|
||||
llm = ChatOpenAI(model='gpt-4.1-mini')
|
||||
|
||||
|
||||
def load_blocklist_from_url(url: str) -> list[str]:
|
||||
"""Load and parse a blocklist from a URL.
|
||||
|
||||
Args:
|
||||
url: URL to the blocklist file
|
||||
|
||||
Returns:
|
||||
List of domain strings (comments and empty lines removed)
|
||||
"""
|
||||
import urllib.request
|
||||
|
||||
print(f'📥 Downloading blocklist from {url}...')
|
||||
|
||||
domains = []
|
||||
with urllib.request.urlopen(url) as response:
|
||||
for line in response:
|
||||
line = line.decode('utf-8').strip()
|
||||
# Skip comments and empty lines
|
||||
if line and not line.startswith('#'):
|
||||
domains.append(line)
|
||||
|
||||
print(f'✅ Loaded {len(domains):,} domains')
|
||||
return domains
|
||||
|
||||
|
||||
async def main():
|
||||
# Load HaGeZi's Pro++ blocklist (blocks ads, tracking, malware, etc.)
|
||||
# Source: https://github.com/hagezi/dns-blocklists
|
||||
blocklist_url = 'https://gitlab.com/hagezi/mirror/-/raw/main/dns-blocklists/domains/pro.plus.txt'
|
||||
|
||||
print('=' * 70)
|
||||
print('🚀 Large Blocklist Demo - 439k+ Blocked Domains')
|
||||
print('=' * 70)
|
||||
print()
|
||||
|
||||
# Load the blocklist
|
||||
prohibited_domains = load_blocklist_from_url(blocklist_url)
|
||||
|
||||
# Sample some blocked domains to test
|
||||
test_blocked = [prohibited_domains[0], prohibited_domains[1000], prohibited_domains[-1]]
|
||||
print(f'\n📋 Sample blocked domains: {", ".join(test_blocked[:3])}')
|
||||
|
||||
print(f'\n🔧 Creating browser with {len(prohibited_domains):,} blocked domains...')
|
||||
print(' (Auto-optimizing to set for O(1) lookup performance)')
|
||||
|
||||
# Create browser with the blocklist
|
||||
# The list will be automatically optimized to a set for fast lookups
|
||||
browser_session = BrowserSession(
|
||||
browser_profile=BrowserProfile(
|
||||
prohibited_domains=prohibited_domains,
|
||||
headless=False,
|
||||
user_data_dir='~/.config/browseruse/profiles/blocklist-demo',
|
||||
),
|
||||
)
|
||||
|
||||
# Task: Try to visit a blocked domain and a safe domain
|
||||
blocked_site = test_blocked[0] # Will be blocked
|
||||
safe_site = 'github.com' # Will be allowed
|
||||
|
||||
task = f"""
|
||||
Try to navigate to these websites and report what happens:
|
||||
1. First, try to visit https://{blocked_site}
|
||||
2. Then, try to visit https://{safe_site}
|
||||
|
||||
Tell me which sites you were able to access and which were blocked.
|
||||
"""
|
||||
|
||||
agent = Agent(
|
||||
task=task,
|
||||
llm=llm,
|
||||
browser_session=browser_session,
|
||||
)
|
||||
|
||||
print(f'\n🤖 Agent task: Try to visit {blocked_site} (blocked) and {safe_site} (allowed)')
|
||||
print('\n' + '=' * 70)
|
||||
|
||||
await agent.run(max_steps=5)
|
||||
|
||||
print('\n' + '=' * 70)
|
||||
print('✅ Demo complete!')
|
||||
print(f'💡 The blocklist with {len(prohibited_domains):,} domains was optimized to a set')
|
||||
print(' for instant O(1) domain checking (vs slow O(n) pattern matching)')
|
||||
print('=' * 70)
|
||||
|
||||
input('\nPress Enter to close the browser...')
|
||||
await browser_session.kill()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
Simple try of the agent.
|
||||
|
||||
@dev You need to add OPENAI_API_KEY to your environment variables.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from browser_use import Agent, ChatOpenAI
|
||||
|
||||
# video: https://preview.screen.studio/share/clenCmS6
|
||||
llm = ChatOpenAI(model='gpt-4.1-mini')
|
||||
agent = Agent(
|
||||
task='open 3 tabs with elon musk, sam altman, and steve jobs, then go back to the first and stop',
|
||||
llm=llm,
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
await agent.run()
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,51 @@
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from browser_use import ChatOpenAI
|
||||
from browser_use.agent.service import Agent
|
||||
from browser_use.browser import BrowserProfile, BrowserSession
|
||||
|
||||
browser_session = BrowserSession(
|
||||
browser_profile=BrowserProfile(
|
||||
keep_alive=True,
|
||||
headless=False,
|
||||
record_video_dir=Path('./tmp/recordings'),
|
||||
user_data_dir='~/.config/browseruse/profiles/default',
|
||||
)
|
||||
)
|
||||
llm = ChatOpenAI(model='gpt-4.1-mini')
|
||||
|
||||
|
||||
# NOTE: This is experimental - you will have multiple agents running in the same browser session
|
||||
async def main():
|
||||
await browser_session.start()
|
||||
agents = [
|
||||
Agent(task=task, llm=llm, browser_session=browser_session)
|
||||
for task in [
|
||||
'Search Google for weather in Tokyo',
|
||||
'Check Reddit front page title',
|
||||
'Look up Bitcoin price on Coinbase',
|
||||
# 'Find NASA image of the day',
|
||||
# 'Check top story on CNN',
|
||||
# 'Search latest SpaceX launch date',
|
||||
# 'Look up population of Paris',
|
||||
# 'Find current time in Sydney',
|
||||
# 'Check who won last Super Bowl',
|
||||
# 'Search trending topics on Twitter',
|
||||
]
|
||||
]
|
||||
|
||||
print(await asyncio.gather(*[agent.run() for agent in agents]))
|
||||
await browser_session.kill()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,55 @@
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from pprint import pprint
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from browser_use import Agent, ChatOpenAI
|
||||
from browser_use.agent.views import AgentHistoryList
|
||||
from browser_use.browser import BrowserProfile, BrowserSession
|
||||
from browser_use.browser.profile import ViewportSize
|
||||
|
||||
llm = ChatOpenAI(model='gpt-4.1-mini')
|
||||
|
||||
|
||||
async def main():
|
||||
browser_session = BrowserSession(
|
||||
browser_profile=BrowserProfile(
|
||||
headless=False,
|
||||
traces_dir='./tmp/result_processing',
|
||||
window_size=ViewportSize(width=1280, height=1000),
|
||||
user_data_dir='~/.config/browseruse/profiles/default',
|
||||
)
|
||||
)
|
||||
await browser_session.start()
|
||||
try:
|
||||
agent = Agent(
|
||||
task="go to google.com and type 'OpenAI' click search and give me the first url",
|
||||
llm=llm,
|
||||
browser_session=browser_session,
|
||||
)
|
||||
history: AgentHistoryList = await agent.run(max_steps=3)
|
||||
|
||||
print('Final Result:')
|
||||
pprint(history.final_result(), indent=4)
|
||||
|
||||
print('\nErrors:')
|
||||
pprint(history.errors(), indent=4)
|
||||
|
||||
# e.g. xPaths the model clicked on
|
||||
print('\nModel Outputs:')
|
||||
pprint(history.model_actions(), indent=4)
|
||||
|
||||
print('\nThoughts:')
|
||||
pprint(history.model_thoughts(), indent=4)
|
||||
finally:
|
||||
await browser_session.stop()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,141 @@
|
||||
"""
|
||||
Example: Rerunning saved agent history with variable detection and substitution
|
||||
|
||||
This example shows how to:
|
||||
1. Run an agent and save its history (including initial URL navigation)
|
||||
2. Detect variables in the saved history (emails, names, dates, etc.)
|
||||
3. Rerun the history with substituted values (different data)
|
||||
4. Get AI-generated summary of rerun completion (with screenshot analysis)
|
||||
|
||||
Useful for:
|
||||
- Debugging agent behavior
|
||||
- Testing changes with consistent scenarios
|
||||
- Replaying successful workflows with different data
|
||||
- Understanding what values can be substituted in reruns
|
||||
- Getting automated verification of rerun success
|
||||
|
||||
Note: Initial actions (like opening URLs from tasks) are now automatically
|
||||
saved to history and will be replayed during rerun, so you don't need to
|
||||
worry about manually specifying URLs when rerunning.
|
||||
|
||||
AI Features During Rerun:
|
||||
|
||||
1. AI Step for Extract Actions:
|
||||
When an 'extract' action is replayed, the rerun automatically uses AI to
|
||||
re-analyze the current page content (since it may have changed with new data).
|
||||
This ensures the extracted content reflects the current state, not cached results.
|
||||
|
||||
2. AI Summary:
|
||||
At the end of the rerun, an AI summary analyzes the final screenshot and
|
||||
execution statistics to determine success/failure.
|
||||
|
||||
Custom LLM Usage:
|
||||
# Option 1: Use agent's LLM (default)
|
||||
results = await agent.load_and_rerun(history_file)
|
||||
|
||||
# Option 2: Use custom LLMs for AI steps and summary
|
||||
from browser_use.llm import ChatOpenAI
|
||||
custom_llm = ChatOpenAI(model='gpt-4.1-mini')
|
||||
results = await agent.load_and_rerun(
|
||||
history_file,
|
||||
ai_step_llm=custom_llm, # For extract action re-evaluation
|
||||
summary_llm=custom_llm, # For final summary
|
||||
)
|
||||
|
||||
The AI summary will be the last item in results and will have:
|
||||
- extracted_content: The summary text
|
||||
- success: Whether rerun was successful
|
||||
- is_done: Always True for summary
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
from browser_use import Agent
|
||||
from browser_use.llm import ChatBrowserUse
|
||||
|
||||
|
||||
async def main():
|
||||
# Example task to demonstrate history saving and rerunning
|
||||
history_file = Path('agent_history.json')
|
||||
task = 'Go to https://browser-use.github.io/stress-tests/challenges/reference-number-form.html and fill the form with example data and submit and extract the refernence number.'
|
||||
llm = ChatBrowserUse(model='bu-2-0')
|
||||
|
||||
# Optional: Use custom LLMs for AI features during rerun
|
||||
# Uncomment to use a custom LLM:
|
||||
# from browser_use.llm import ChatOpenAI
|
||||
# custom_llm = ChatOpenAI(model='gpt-4.1-mini')
|
||||
# ai_step_llm = custom_llm # For re-evaluating extract actions
|
||||
# summary_llm = custom_llm # For final summary
|
||||
ai_step_llm = None # Set to None to use agent's LLM (default)
|
||||
summary_llm = None # Set to None to use agent's LLM (default)
|
||||
|
||||
# Step 1: Run the agent and save history
|
||||
print('=== Running Agent ===')
|
||||
agent = Agent(task=task, llm=llm, max_actions_per_step=1)
|
||||
await agent.run(max_steps=10)
|
||||
agent.save_history(history_file)
|
||||
print(f'✓ History saved to {history_file}')
|
||||
|
||||
# Step 2: Detect variables in the saved history
|
||||
print('\n=== Detecting Variables ===')
|
||||
variables = agent.detect_variables()
|
||||
if variables:
|
||||
print(f'Found {len(variables)} variable(s):')
|
||||
for var_name, var_info in variables.items():
|
||||
format_info = f' (format: {var_info.format})' if var_info.format else ''
|
||||
print(f' • {var_name}: "{var_info.original_value}"{format_info}')
|
||||
else:
|
||||
print('No variables detected in history')
|
||||
|
||||
# Step 3: Rerun the history with substituted values
|
||||
if variables:
|
||||
print('\n=== Rerunning History (Substituted Values) ===')
|
||||
# Create new values for the detected variables
|
||||
new_values = {}
|
||||
for var_name, var_info in variables.items():
|
||||
# Map detected variables to new values
|
||||
if var_name == 'email':
|
||||
new_values[var_name] = 'jane.smith@example.com'
|
||||
elif var_name == 'full_name':
|
||||
new_values[var_name] = 'Jane Smith'
|
||||
elif var_name.startswith('full_name_'):
|
||||
new_values[var_name] = 'General Information'
|
||||
elif var_name == 'first_name':
|
||||
new_values[var_name] = 'Jane'
|
||||
elif var_name == 'date':
|
||||
new_values[var_name] = '1995-05-15'
|
||||
elif var_name == 'country':
|
||||
new_values[var_name] = 'Canada'
|
||||
# You can add more variable substitutions as needed
|
||||
|
||||
if new_values:
|
||||
print(f'Substituting {len(new_values)} variable(s):')
|
||||
for var_name, new_value in new_values.items():
|
||||
old_value = variables[var_name].original_value
|
||||
print(f' • {var_name}: "{old_value}" → "{new_value}"')
|
||||
|
||||
# Rerun with substituted values and optional custom LLMs
|
||||
substitute_agent = Agent(task='', llm=llm)
|
||||
results = await substitute_agent.load_and_rerun(
|
||||
history_file,
|
||||
variables=new_values,
|
||||
ai_step_llm=ai_step_llm, # For extract action re-evaluation
|
||||
summary_llm=summary_llm, # For final summary
|
||||
max_step_interval=20,
|
||||
delay_between_actions=1,
|
||||
)
|
||||
|
||||
# Display AI-generated summary (last result)
|
||||
if results and results[-1].is_done:
|
||||
summary = results[-1]
|
||||
print('\n📊 AI Summary:')
|
||||
print(f' Summary: {summary.extracted_content}')
|
||||
print(f' Success: {summary.success}')
|
||||
print('✓ History rerun with substituted values complete')
|
||||
else:
|
||||
print('\n⚠️ No variables detected, skipping substitution rerun')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,43 @@
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from browser_use import Agent, ChatOpenAI
|
||||
from browser_use.browser import BrowserProfile, BrowserSession
|
||||
|
||||
llm = ChatOpenAI(model='gpt-4.1-mini')
|
||||
task = (
|
||||
"go to google.com and search for openai.com and click on the first link then extract content and scroll down - what's there?"
|
||||
)
|
||||
|
||||
allowed_domains = ['google.com']
|
||||
|
||||
browser_session = BrowserSession(
|
||||
browser_profile=BrowserProfile(
|
||||
executable_path='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
allowed_domains=allowed_domains,
|
||||
user_data_dir='~/.config/browseruse/profiles/default',
|
||||
),
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
task=task,
|
||||
llm=llm,
|
||||
browser_session=browser_session,
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
await agent.run(max_steps=25)
|
||||
|
||||
input('Press Enter to close the browser...')
|
||||
await browser_session.kill()
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,51 @@
|
||||
"""
|
||||
Save any webpage as a PDF using the save_as_pdf action.
|
||||
|
||||
The agent can save the current page as a PDF at any point during a task.
|
||||
Supports custom filenames, paper sizes (Letter, A4, Legal, A3, Tabloid),
|
||||
landscape orientation, and background printing.
|
||||
|
||||
By default the PDF includes page metadata in the margins (just like Chrome's
|
||||
Print dialog): the date in the header and the page URL plus page numbers in the
|
||||
footer. Pass display_header_footer=False for a clean PDF, or supply custom
|
||||
header_template / footer_template HTML to control exactly what's printed.
|
||||
|
||||
Setup:
|
||||
1. Get your API key from https://cloud.browser-use.com/new-api-key
|
||||
2. Set environment variable: export BROWSER_USE_API_KEY="your-key"
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from browser_use import Agent, ChatBrowserUse
|
||||
|
||||
|
||||
async def main():
|
||||
agent = Agent(
|
||||
task=(
|
||||
'Go to https://news.ycombinator.com and save the front page as a PDF named "hackernews". '
|
||||
'Then go to https://en.wikipedia.org/wiki/Web_browser and save just that article as a PDF in A4 format.'
|
||||
),
|
||||
llm=ChatBrowserUse(model='bu-2-0'),
|
||||
)
|
||||
|
||||
history = await agent.run()
|
||||
|
||||
# Print paths of any PDF files the agent saved
|
||||
print('\nSaved files:')
|
||||
for result in history.action_results():
|
||||
if result.attachments:
|
||||
for path in result.attachments:
|
||||
print(f' {path}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,96 @@
|
||||
# Goal: Automates webpage scrolling with various scrolling actions, including element-specific scrolling.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from browser_use import Agent, ChatOpenAI
|
||||
from browser_use.browser import BrowserProfile, BrowserSession
|
||||
|
||||
if not os.getenv('OPENAI_API_KEY'):
|
||||
raise ValueError('OPENAI_API_KEY is not set')
|
||||
|
||||
"""
|
||||
Example: Enhanced 'Scroll' action with page amounts and element-specific scrolling.
|
||||
|
||||
This script demonstrates the new enhanced scrolling capabilities:
|
||||
|
||||
1. PAGE-LEVEL SCROLLING:
|
||||
- Scrolling by specific page amounts using 'num_pages' parameter (0.5, 1.0, 2.0, etc.)
|
||||
- Scrolling up or down using the 'down' parameter
|
||||
- Uses JavaScript window.scrollBy() or smart container detection
|
||||
|
||||
2. ELEMENT-SPECIFIC SCROLLING:
|
||||
- NEW: Optional 'index' parameter to scroll within specific elements
|
||||
- Perfect for dropdowns, sidebars, and custom UI components
|
||||
- Uses direct scrollTop manipulation (no mouse events that might close dropdowns)
|
||||
- Automatically finds scroll containers in the element hierarchy
|
||||
- Falls back to page scrolling if no container found
|
||||
|
||||
3. IMPLEMENTATION DETAILS:
|
||||
- Does NOT use mouse movement or wheel events
|
||||
- Direct DOM manipulation for precision and reliability
|
||||
- Container-aware scrolling prevents unwanted side effects
|
||||
"""
|
||||
|
||||
llm = ChatOpenAI(model='gpt-4.1-mini')
|
||||
|
||||
browser_profile = BrowserProfile(headless=False)
|
||||
browser_session = BrowserSession(browser_profile=browser_profile)
|
||||
|
||||
# Example 1: Basic page scrolling with custom amounts
|
||||
agent1 = Agent(
|
||||
task="Navigate to 'https://en.wikipedia.org/wiki/Internet' and scroll down by one page - then scroll up by 0.5 pages - then scroll down by 0.25 pages - then scroll down by 2 pages.",
|
||||
llm=llm,
|
||||
browser_session=browser_session,
|
||||
)
|
||||
|
||||
# Example 2: Element-specific scrolling (dropdowns and containers)
|
||||
agent2 = Agent(
|
||||
task="""Go to https://semantic-ui.com/modules/dropdown.html#/definition and:
|
||||
1. Scroll down in the left sidebar by 2 pages
|
||||
2. Then scroll down 1 page in the main content area
|
||||
3. Click on the State dropdown and scroll down 1 page INSIDE the dropdown to see more states
|
||||
4. The dropdown should stay open while scrolling inside it""",
|
||||
llm=llm,
|
||||
browser_session=browser_session,
|
||||
)
|
||||
|
||||
# Example 3: Text-based scrolling alternative
|
||||
agent3 = Agent(
|
||||
task="Navigate to 'https://en.wikipedia.org/wiki/Internet' and scroll to the text 'The vast majority of computer'",
|
||||
llm=llm,
|
||||
browser_session=browser_session,
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
print('Choose which scrolling example to run:')
|
||||
print('1. Basic page scrolling with custom amounts (Wikipedia)')
|
||||
print('2. Element-specific scrolling (Semantic UI dropdowns)')
|
||||
print('3. Text-based scrolling (Wikipedia)')
|
||||
|
||||
choice = input('Enter choice (1-3): ').strip()
|
||||
|
||||
if choice == '1':
|
||||
print('🚀 Running Example 1: Basic page scrolling...')
|
||||
await agent1.run()
|
||||
elif choice == '2':
|
||||
print('🚀 Running Example 2: Element-specific scrolling...')
|
||||
await agent2.run()
|
||||
elif choice == '3':
|
||||
print('🚀 Running Example 3: Text-based scrolling...')
|
||||
await agent3.run()
|
||||
else:
|
||||
print('❌ Invalid choice. Running Example 1 by default...')
|
||||
await agent1.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
Azure OpenAI example with data privacy and high-scale configuration.
|
||||
|
||||
Environment Variables Required:
|
||||
- AZURE_OPENAI_KEY (or AZURE_OPENAI_API_KEY)
|
||||
- AZURE_OPENAI_ENDPOINT
|
||||
- AZURE_OPENAI_DEPLOYMENT (optional)
|
||||
|
||||
DATA PRIVACY WITH AZURE OPENAI:
|
||||
✅ Good News: No Training on Your Data by Default
|
||||
|
||||
Azure OpenAI Service already protects your data:
|
||||
✅ NOT used to train OpenAI models
|
||||
✅ NOT shared with other customers
|
||||
✅ NOT accessible to OpenAI directly
|
||||
✅ NOT used to improve Microsoft/third-party products
|
||||
✅ Hosted entirely within Azure (not OpenAI's servers)
|
||||
|
||||
⚠️ Default Data Retention (30 Days)
|
||||
- Prompts and completions stored for up to 30 days
|
||||
- Purpose: Abuse monitoring and compliance
|
||||
- Access: Microsoft authorized personnel (only if abuse detected)
|
||||
|
||||
🔒 How to Disable Data Logging Completely
|
||||
Apply for Microsoft's "Limited Access Program":
|
||||
1. Contact Microsoft Azure support
|
||||
2. Submit Limited Access Program request
|
||||
3. Demonstrate legitimate business need
|
||||
4. After approval: Zero data logging, immediate deletion, no human review
|
||||
|
||||
For high-scale deployments (500+ agents), consider:
|
||||
- Multiple deployments across regions
|
||||
|
||||
|
||||
How to Verify This Yourself, that there is no data logging:
|
||||
- Network monitoring: Run with network monitoring tools
|
||||
- Firewall rules: Block all domains except Azure OpenAI and your target sites
|
||||
|
||||
Contact us if you need help with this: support@browser-use.com
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
os.environ['ANONYMIZED_TELEMETRY'] = 'false'
|
||||
|
||||
|
||||
from browser_use import Agent, BrowserProfile, ChatAzureOpenAI
|
||||
|
||||
# Configuration LLM
|
||||
api_key = os.getenv('AZURE_OPENAI_KEY')
|
||||
azure_endpoint = os.getenv('AZURE_OPENAI_ENDPOINT')
|
||||
llm = ChatAzureOpenAI(model='gpt-4.1-mini', api_key=api_key, azure_endpoint=azure_endpoint)
|
||||
|
||||
# Configuration Task
|
||||
task = 'Find the founders of the sensitive company_name'
|
||||
|
||||
# Configuration Browser (optional)
|
||||
browser_profile = BrowserProfile(allowed_domains=['*google.com', 'browser-use.com'], enable_default_extensions=False)
|
||||
|
||||
# Sensitive data (optional) - {key: sensitive_information} - we filter out the sensitive_information from any input to the LLM, it will only work with placeholder.
|
||||
# By default we pass screenshots to the LLM which can contain your information. Set use_vision=False to disable this.
|
||||
# If you trust your LLM endpoint, you don't need to worry about this.
|
||||
sensitive_data = {'company_name': 'browser-use'}
|
||||
|
||||
|
||||
# Create Agent
|
||||
agent = Agent(task=task, llm=llm, browser_profile=browser_profile, sensitive_data=sensitive_data) # type: ignore
|
||||
|
||||
|
||||
async def main():
|
||||
await agent.run(max_steps=10)
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,47 @@
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from browser_use import Agent, ChatOpenAI
|
||||
|
||||
# Initialize the model
|
||||
llm = ChatOpenAI(
|
||||
model='gpt-4.1',
|
||||
temperature=0.0,
|
||||
)
|
||||
# Simple case: the model will see x_name and x_password, but never the actual values.
|
||||
# sensitive_data = {'x_name': 'my_x_name', 'x_password': 'my_x_password'}
|
||||
|
||||
# Advanced case: domain-specific credentials with reusable data
|
||||
# Define a single credential set that can be reused
|
||||
company_credentials: dict[str, str] = {'telephone': '9123456789', 'email': 'user@example.com', 'name': 'John Doe'}
|
||||
|
||||
# Map the same credentials to multiple domains for secure access control
|
||||
# Type annotation to satisfy pyright
|
||||
sensitive_data: dict[str, str | dict[str, str]] = {
|
||||
# 'https://example.com': company_credentials,
|
||||
# 'https://admin.example.com': company_credentials,
|
||||
# 'https://*.example-staging.com': company_credentials,
|
||||
# 'http*://test.example.com': company_credentials,
|
||||
'httpbin.org': company_credentials,
|
||||
# # You can also add domain-specific credentials
|
||||
# 'https://google.com': {'g_email': 'user@gmail.com', 'g_pass': 'google_password'}
|
||||
}
|
||||
# Update task to use one of the credentials above
|
||||
task = 'Go to https://httpbin.org/forms/post and put the secure information in the relevant fields.'
|
||||
|
||||
agent = Agent(task=task, llm=llm, sensitive_data=sensitive_data)
|
||||
|
||||
|
||||
async def main():
|
||||
await agent.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,27 @@
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from browser_use import Agent, ChatOpenAI
|
||||
|
||||
# This uses a bigger model for the planning
|
||||
# And a smaller model for the page content extraction
|
||||
# THink of it like a subagent which only task is to extract content from the current page
|
||||
llm = ChatOpenAI(model='gpt-4.1')
|
||||
small_llm = ChatOpenAI(model='gpt-4.1-mini')
|
||||
task = 'Find the founders of browser-use in ycombinator, extract all links and open the links one by one'
|
||||
agent = Agent(task=task, llm=llm, page_extraction_llm=small_llm)
|
||||
|
||||
|
||||
async def main():
|
||||
await agent.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,43 @@
|
||||
import asyncio
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
|
||||
from browser_use.llm.google.chat import ChatGoogle
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from browser_use import Agent
|
||||
|
||||
llm = ChatGoogle(model='gemini-3-flash-preview', temperature=1.0)
|
||||
|
||||
|
||||
def check_is_task_stopped():
|
||||
async def _internal_check_is_task_stopped() -> bool:
|
||||
if random.random() < 0.1:
|
||||
print('[TASK STOPPER] Task is stopped')
|
||||
return True
|
||||
else:
|
||||
print('[TASK STOPPER] Task is not stopped')
|
||||
return False
|
||||
|
||||
return _internal_check_is_task_stopped
|
||||
|
||||
|
||||
task = """
|
||||
Go to https://browser-use.github.io/stress-tests/challenges/wufoo-style-form.html and complete the Wufoo-style form by filling in all required fields and submitting.
|
||||
"""
|
||||
|
||||
agent = Agent(task=task, llm=llm, flash_mode=True, register_should_stop_callback=check_is_task_stopped(), max_actions_per_step=1)
|
||||
|
||||
|
||||
async def main():
|
||||
await agent.run(max_steps=30)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,25 @@
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
from browser_use import Agent, Browser, ChatOpenAI
|
||||
|
||||
# NOTE: To use this example, install imageio[ffmpeg], e.g. with uv pip install "browser-use[video]"
|
||||
|
||||
|
||||
async def main():
|
||||
browser_session = Browser(record_video_dir=Path('./tmp/recordings'))
|
||||
|
||||
agent = Agent(
|
||||
task='Go to github.com/trending then navigate to the first trending repository and report how many commits it has.',
|
||||
llm=ChatOpenAI(model='gpt-4.1-mini'),
|
||||
browser_session=browser_session,
|
||||
)
|
||||
|
||||
await agent.run(max_steps=5)
|
||||
|
||||
# The video will be saved automatically when the agent finishes and the session closes.
|
||||
print('Agent run finished. Check the ./tmp/recordings directory for the video.')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,37 @@
|
||||
import asyncio
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from browser_use import Agent, ChatOpenAI
|
||||
|
||||
load_dotenv()
|
||||
|
||||
SCRIPT_DIR = pathlib.Path(os.path.dirname(os.path.abspath(__file__)))
|
||||
agent_dir = SCRIPT_DIR / 'alphabet_earnings'
|
||||
agent_dir.mkdir(exist_ok=True)
|
||||
|
||||
task = """
|
||||
Go to https://abc.xyz/assets/cc/27/3ada14014efbadd7a58472f1f3f4/2025q2-alphabet-earnings-release.pdf.
|
||||
Read the PDF and save 3 interesting data points in "alphabet_earnings.pdf" and share it with me!
|
||||
""".strip('\n')
|
||||
|
||||
agent = Agent(
|
||||
task=task,
|
||||
llm=ChatOpenAI(model='o4-mini'),
|
||||
file_system_path=str(agent_dir / 'fs'),
|
||||
flash_mode=True,
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
await agent.run()
|
||||
input(f'Press Enter to clean the file system at {agent_dir}...')
|
||||
# clean the file system
|
||||
shutil.rmtree(str(agent_dir / 'fs'))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,37 @@
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
from browser_use.llm.openai.chat import ChatOpenAI
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from browser_use import Agent
|
||||
|
||||
llm = ChatOpenAI(model='o4-mini')
|
||||
|
||||
|
||||
task = (
|
||||
'Find current stock price of companies Meta and Amazon. Then, make me a CSV file with 2 columns: company name, stock price.'
|
||||
)
|
||||
|
||||
agent = Agent(task=task, llm=llm)
|
||||
|
||||
|
||||
async def main():
|
||||
import time
|
||||
|
||||
start_time = time.time()
|
||||
history = await agent.run()
|
||||
# token usage
|
||||
print(history.usage)
|
||||
end_time = time.time()
|
||||
print(f'Time taken: {end_time - start_time} seconds')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,50 @@
|
||||
import asyncio
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from browser_use import Agent, ChatOpenAI
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
SCRIPT_DIR = pathlib.Path(os.path.dirname(os.path.abspath(__file__)))
|
||||
agent_dir = SCRIPT_DIR / 'file_system'
|
||||
agent_dir.mkdir(exist_ok=True)
|
||||
conversation_dir = agent_dir / 'conversations' / 'conversation'
|
||||
print(f'Agent logs directory: {agent_dir}')
|
||||
|
||||
|
||||
task = """
|
||||
Go to https://mertunsall.github.io/posts/post1.html
|
||||
Save the title of the article in "data.md"
|
||||
Then, use append_file to add the first sentence of the article to "data.md"
|
||||
Then, read the file to see its content and make sure it's correct.
|
||||
Finally, share the file with me.
|
||||
|
||||
NOTE: DO NOT USE extract action - everything is visible in browser state.
|
||||
""".strip('\n')
|
||||
|
||||
llm = ChatOpenAI(model='gpt-4.1-mini')
|
||||
|
||||
agent = Agent(
|
||||
task=task,
|
||||
llm=llm,
|
||||
save_conversation_path=str(conversation_dir),
|
||||
file_system_path=str(agent_dir / 'fs'),
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
agent_history = await agent.run()
|
||||
print(f'Final result: {agent_history.final_result()}', flush=True)
|
||||
|
||||
input('Press Enter to clean the file system...')
|
||||
# clean the file system
|
||||
shutil.rmtree(str(agent_dir / 'fs'))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,29 @@
|
||||
"""
|
||||
Setup:
|
||||
1. Get your API key from https://cloud.browser-use.com/new-api-key
|
||||
2. Set environment variable: export BROWSER_USE_API_KEY="your-key"
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add the parent directory to the path so we can import browser_use
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from browser_use import Agent, ChatBrowserUse
|
||||
|
||||
|
||||
async def main():
|
||||
llm = ChatBrowserUse(model='bu-2-0')
|
||||
task = "Search Google for 'what is browser automation' and tell me the top 3 results"
|
||||
agent = Agent(task=task, llm=llm)
|
||||
await agent.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
Getting Started Example 2: Form Filling
|
||||
|
||||
This example demonstrates how to:
|
||||
- Navigate to a website with forms
|
||||
- Fill out input fields
|
||||
- Submit forms
|
||||
- Handle basic form interactions
|
||||
|
||||
This builds on the basic search example by showing more complex interactions.
|
||||
|
||||
Setup:
|
||||
1. Get your API key from https://cloud.browser-use.com/new-api-key
|
||||
2. Set environment variable: export BROWSER_USE_API_KEY="your-key"
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add the parent directory to the path so we can import browser_use
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from browser_use import Agent, ChatBrowserUse
|
||||
|
||||
|
||||
async def main():
|
||||
# Initialize the model
|
||||
llm = ChatBrowserUse(model='bu-2-0')
|
||||
|
||||
# Define a form filling task
|
||||
task = """
|
||||
Go to https://httpbin.org/forms/post and fill out the contact form with:
|
||||
- Customer name: John Doe
|
||||
- Telephone: 555-123-4567
|
||||
- Email: john.doe@example.com
|
||||
- Size: Medium
|
||||
- Topping: cheese
|
||||
- Delivery time: now
|
||||
- Comments: This is a test form submission
|
||||
|
||||
Then submit the form and tell me what response you get.
|
||||
"""
|
||||
|
||||
# Create and run the agent
|
||||
agent = Agent(task=task, llm=llm)
|
||||
await agent.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
Getting Started Example 3: Data Extraction
|
||||
|
||||
This example demonstrates how to:
|
||||
- Navigate to a website with structured data
|
||||
- Extract specific information from the page
|
||||
- Process and organize the extracted data
|
||||
- Return structured results
|
||||
|
||||
This builds on previous examples by showing how to get valuable data from websites.
|
||||
|
||||
Setup:
|
||||
1. Get your API key from https://cloud.browser-use.com/new-api-key
|
||||
2. Set environment variable: export BROWSER_USE_API_KEY="your-key"
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add the parent directory to the path so we can import browser_use
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from browser_use import Agent, ChatBrowserUse
|
||||
|
||||
|
||||
async def main():
|
||||
# Initialize the model
|
||||
llm = ChatBrowserUse(model='bu-2-0')
|
||||
|
||||
# Define a data extraction task
|
||||
task = """
|
||||
Go to https://quotes.toscrape.com/ and extract the following information:
|
||||
- The first 5 quotes on the page
|
||||
- The author of each quote
|
||||
- The tags associated with each quote
|
||||
|
||||
Present the information in a clear, structured format like:
|
||||
Quote 1: "[quote text]" - Author: [author name] - Tags: [tag1, tag2, ...]
|
||||
Quote 2: "[quote text]" - Author: [author name] - Tags: [tag1, tag2, ...]
|
||||
etc.
|
||||
"""
|
||||
|
||||
# Create and run the agent
|
||||
agent = Agent(task=task, llm=llm)
|
||||
await agent.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
Getting Started Example 4: Multi-Step Task
|
||||
|
||||
This example demonstrates how to:
|
||||
- Perform a complex workflow with multiple steps
|
||||
- Navigate between different pages
|
||||
- Combine search, form filling, and data extraction
|
||||
- Handle a realistic end-to-end scenario
|
||||
|
||||
This is the most advanced getting started example, combining all previous concepts.
|
||||
|
||||
Setup:
|
||||
1. Get your API key from https://cloud.browser-use.com/new-api-key
|
||||
2. Set environment variable: export BROWSER_USE_API_KEY="your-key"
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add the parent directory to the path so we can import browser_use
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from browser_use import Agent, ChatBrowserUse
|
||||
|
||||
|
||||
async def main():
|
||||
# Initialize the model
|
||||
llm = ChatBrowserUse(model='bu-2-0')
|
||||
|
||||
# Define a multi-step task
|
||||
task = """
|
||||
I want you to research Python web scraping libraries. Here's what I need:
|
||||
|
||||
1. First, search Google for "best Python web scraping libraries 2024"
|
||||
2. Find a reputable article or blog post about this topic
|
||||
3. From that article, extract the top 3 recommended libraries
|
||||
4. For each library, visit its official website or GitHub page
|
||||
5. Extract key information about each library:
|
||||
- Name
|
||||
- Brief description
|
||||
- Main features or advantages
|
||||
- GitHub stars (if available)
|
||||
|
||||
Present your findings in a summary format comparing the three libraries.
|
||||
"""
|
||||
|
||||
# Create and run the agent
|
||||
agent = Agent(task=task, llm=llm)
|
||||
await agent.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,64 @@
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add the parent directory to the path so we can import browser_use
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
from browser_use import Agent, BrowserProfile
|
||||
|
||||
# Speed optimization instructions for the model
|
||||
SPEED_OPTIMIZATION_PROMPT = """
|
||||
Speed optimization instructions:
|
||||
- Be extremely concise and direct in your responses
|
||||
- Get to the goal as quickly as possible
|
||||
- Use multi-action sequences whenever possible to reduce steps
|
||||
"""
|
||||
|
||||
|
||||
async def main():
|
||||
# 1. Use fast LLM - Llama 4 on Groq for ultra-fast inference
|
||||
from browser_use import ChatGroq
|
||||
|
||||
llm = ChatGroq(
|
||||
model='meta-llama/llama-4-maverick-17b-128e-instruct',
|
||||
temperature=0.0,
|
||||
)
|
||||
# from browser_use import ChatGoogle
|
||||
|
||||
# llm = ChatGoogle(model='gemini-3.1-flash-lite')
|
||||
|
||||
# 2. Create speed-optimized browser profile
|
||||
browser_profile = BrowserProfile(
|
||||
minimum_wait_page_load_time=0.1,
|
||||
wait_between_actions=0.1,
|
||||
headless=False,
|
||||
)
|
||||
|
||||
# 3. Define a speed-focused task
|
||||
task = """
|
||||
1. Go to reddit https://www.reddit.com/search/?q=browser+agent&type=communities
|
||||
2. Click directly on the first 5 communities to open each in new tabs
|
||||
3. Find out what the latest post is about, and switch directly to the next tab
|
||||
4. Return the latest post summary for each page
|
||||
"""
|
||||
|
||||
# 4. Create agent with all speed optimizations
|
||||
agent = Agent(
|
||||
task=task,
|
||||
llm=llm,
|
||||
flash_mode=True, # Disables thinking in the LLM output for maximum speed
|
||||
browser_profile=browser_profile,
|
||||
extend_system_message=SPEED_OPTIMIZATION_PROMPT,
|
||||
)
|
||||
|
||||
await agent.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,30 @@
|
||||
# Integration examples
|
||||
|
||||
This directory is for examples that show Browser Use working with external products, APIs, and services.
|
||||
|
||||
## Where to put integration contributions
|
||||
|
||||
- Use `examples/integrations/<provider>/` for small, runnable examples that demonstrate Browser Use with a specific third-party service.
|
||||
- Use `examples/custom-functions/` for provider-agnostic custom tool patterns.
|
||||
- Use `browser_use/integrations/<provider>/` only when the integration is shipped as part of the Browser Use package and has tests.
|
||||
- Keep product-specific workflows, full applications, or large third-party projects in their own repositories.
|
||||
- Add third-party projects to the community list below instead of vendoring their code into this repository.
|
||||
|
||||
## Example checklist
|
||||
|
||||
- Use `uv` in setup instructions.
|
||||
- Keep the example focused on the Browser Use integration point.
|
||||
- Document required environment variables, OAuth scopes, and local services.
|
||||
- Do not commit secrets, tokens, generated credentials, or private account data.
|
||||
- Prefer `ChatBrowserUse()` unless the example is specifically about another model.
|
||||
- Include the command that runs the example from the repository root.
|
||||
|
||||
## Community integrations
|
||||
|
||||
External projects listed here are maintained outside this repository. A listing is a pointer for users, not a support guarantee from Browser Use maintainers.
|
||||
|
||||
Add entries in this format:
|
||||
|
||||
```markdown
|
||||
- [Project name](https://github.com/org/project) - One sentence about what it integrates with. Maintained by @github-handle.
|
||||
```
|
||||
@@ -0,0 +1,42 @@
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
from agentmail import AsyncAgentMail # type: ignore
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from browser_use import Agent, Browser, ChatBrowserUse
|
||||
from examples.integrations.agentmail.email_tools import EmailTools
|
||||
|
||||
TASK = """
|
||||
Go to reddit.com, create a new account (use the get_email_address), make up password and all other information, confirm the 2fa with get_latest_email, and like latest post on r/elon subreddit.
|
||||
"""
|
||||
|
||||
|
||||
async def main():
|
||||
# Create email inbox
|
||||
# Get an API key from https://agentmail.to/
|
||||
email_client = AsyncAgentMail()
|
||||
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 LLM for browser-use agent
|
||||
llm = ChatBrowserUse(model='bu-2-0')
|
||||
|
||||
# Set your local browser path
|
||||
browser = Browser(executable_path='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome')
|
||||
|
||||
agent = Agent(task=TASK, tools=tools, llm=llm, browser=browser)
|
||||
|
||||
await agent.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,187 @@
|
||||
"""
|
||||
Email management to enable 2fa.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
# run `pip install agentmail` to install the library
|
||||
from agentmail import AsyncAgentMail, Message, MessageReceivedEvent, Subscribe # type: ignore
|
||||
from agentmail.inboxes.types.inbox import Inbox # type: ignore
|
||||
from agentmail.inboxes.types.inbox_id import InboxId # type: ignore
|
||||
|
||||
from browser_use import Tools
|
||||
|
||||
# Configure basic logging if not already configured
|
||||
if not logging.getLogger().handlers:
|
||||
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'<script\b[^>]*>.*?</script\s*>', '', html, flags=re.DOTALL | re.IGNORECASE)
|
||||
html = re.sub(r'<style\b[^>]*>.*?</style\s*>', '', 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'
|
||||
# logger.info(f'Latest message: {latest_message}')
|
||||
return self._serialize_message_for_llm(latest_message)
|
||||
@@ -0,0 +1,123 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
import discord # type: ignore
|
||||
from discord.ext import commands # type: ignore
|
||||
|
||||
from browser_use.agent.service import Agent
|
||||
from browser_use.browser import BrowserProfile, BrowserSession
|
||||
from browser_use.llm import BaseChatModel
|
||||
|
||||
|
||||
class DiscordBot(commands.Bot):
|
||||
"""Discord bot implementation for Browser-Use tasks.
|
||||
|
||||
This bot allows users to run browser automation tasks through Discord messages.
|
||||
Processes tasks asynchronously and sends the result back to the user in response to the message.
|
||||
Messages must start with the configured prefix (default: "$bu") followed by the task description.
|
||||
|
||||
Args:
|
||||
llm (BaseChatModel): Language model instance to use for task processing
|
||||
prefix (str, optional): Command prefix for triggering browser tasks. Defaults to "$bu"
|
||||
ack (bool, optional): Whether to acknowledge task receipt with a message. Defaults to False
|
||||
browser_profile (BrowserProfile, optional): Browser profile settings.
|
||||
Defaults to headless mode
|
||||
|
||||
Usage:
|
||||
```python
|
||||
from browser_use import ChatOpenAI
|
||||
|
||||
llm = ChatOpenAI()
|
||||
bot = DiscordBot(llm=llm, prefix='$bu', ack=True)
|
||||
bot.run('YOUR_DISCORD_TOKEN')
|
||||
```
|
||||
|
||||
Discord Usage:
|
||||
Send messages starting with the prefix:
|
||||
"$bu search for python tutorials"
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
llm: BaseChatModel,
|
||||
prefix: str = '$bu',
|
||||
ack: bool = False,
|
||||
browser_profile: BrowserProfile = BrowserProfile(headless=True),
|
||||
):
|
||||
self.llm = llm
|
||||
self.prefix = prefix.strip()
|
||||
self.ack = ack
|
||||
self.browser_profile = browser_profile
|
||||
|
||||
# Define intents.
|
||||
intents = discord.Intents.default() # type: ignore
|
||||
intents.message_content = True # Enable message content intent
|
||||
intents.members = True # Enable members intent for user info
|
||||
|
||||
# Initialize the bot with a command prefix and intents.
|
||||
super().__init__(command_prefix='!', intents=intents) # You may not need prefix, just here for flexibility
|
||||
|
||||
# self.tree = app_commands.CommandTree(self) # Initialize command tree for slash commands.
|
||||
|
||||
async def on_ready(self):
|
||||
"""Called when the bot is ready."""
|
||||
try:
|
||||
print(f'We have logged in as {self.user}')
|
||||
cmds = await self.tree.sync() # Sync the command tree with discord
|
||||
|
||||
except Exception as e:
|
||||
print(f'Error during bot startup: {e}')
|
||||
|
||||
async def on_message(self, message):
|
||||
"""Called when a message is received."""
|
||||
try:
|
||||
if message.author == self.user: # Ignore the bot's messages
|
||||
return
|
||||
if message.content.strip().startswith(f'{self.prefix} '):
|
||||
if self.ack:
|
||||
try:
|
||||
await message.reply(
|
||||
'Starting browser use task...',
|
||||
mention_author=True, # Don't ping the user
|
||||
)
|
||||
except Exception as e:
|
||||
print(f'Error sending start message: {e}')
|
||||
|
||||
try:
|
||||
agent_message = await self.run_agent(message.content.replace(f'{self.prefix} ', '').strip())
|
||||
await message.channel.send(content=f'{agent_message}', reference=message, mention_author=True)
|
||||
except Exception as e:
|
||||
await message.channel.send(
|
||||
content=f'Error during task execution: {str(e)}',
|
||||
reference=message,
|
||||
mention_author=True,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f'Error in message handling: {e}')
|
||||
|
||||
# await self.process_commands(message) # Needed to process bot commands
|
||||
|
||||
async def run_agent(self, task: str) -> str:
|
||||
try:
|
||||
browser_session = BrowserSession(browser_profile=self.browser_profile)
|
||||
agent = Agent(task=(task), llm=self.llm, browser_session=browser_session)
|
||||
result = await agent.run()
|
||||
|
||||
agent_message = None
|
||||
if result.is_done():
|
||||
agent_message = result.history[-1].result[0].extracted_content
|
||||
|
||||
if agent_message is None:
|
||||
agent_message = 'Oops! Something went wrong while running Browser-Use.'
|
||||
|
||||
return agent_message
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f'Browser-use task failed: {str(e)}')
|
||||
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
This examples requires you to have a Discord bot token and the bot already added to a server.
|
||||
|
||||
Five Steps to create and invite a Discord bot:
|
||||
|
||||
1. Create a Discord Application:
|
||||
* Go to the Discord Developer Portal: https://discord.com/developers/applications
|
||||
* Log in to the Discord website.
|
||||
* Click on "New Application".
|
||||
* Give the application a name and click "Create".
|
||||
2. Configure the Bot:
|
||||
* Navigate to the "Bot" tab on the left side of the screen.
|
||||
* Make sure "Public Bot" is ticked if you want others to invite your bot.
|
||||
* Generate your bot token by clicking on "Reset Token", Copy the token and save it securely.
|
||||
* Do not share the bot token. Treat it like a password. If the token is leaked, regenerate it.
|
||||
3. Enable Privileged Intents:
|
||||
* Scroll down to the "Privileged Gateway Intents" section.
|
||||
* Enable the necessary intents (e.g., "Server Members Intent" and "Message Content Intent").
|
||||
--> Note: Enabling privileged intents for bots in over 100 guilds requires bot verification. You may need to contact Discord support to enable privileged intents for verified bots.
|
||||
4. Generate Invite URL:
|
||||
* Go to "OAuth2" tab and "OAuth2 URL Generator" section.
|
||||
* Under "scopes", tick the "bot" checkbox.
|
||||
* Tick the permissions required for your bot to function under “Bot Permissions”.
|
||||
* e.g. "Send Messages", "Send Messages in Threads", "Read Message History", "Mention Everyone".
|
||||
* Copy the generated URL under the "GENERATED URL" section at the bottom.
|
||||
5. Invite the Bot:
|
||||
* Paste the URL into your browser.
|
||||
* Choose a server to invite the bot to.
|
||||
* Click “Authorize”.
|
||||
--> Note: The person adding the bot needs "Manage Server" permissions.
|
||||
6. Run the code below to start the bot with your bot token.
|
||||
7. Write e.g. "/bu what's the weather in Tokyo?" to start a browser-use task and get a response inside the Discord channel.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
from browser_use.browser import BrowserProfile
|
||||
from browser_use.llm import ChatGoogle
|
||||
from examples.integrations.discord.discord_api import DiscordBot
|
||||
|
||||
# load credentials from environment variables
|
||||
bot_token = os.getenv('DISCORD_BOT_TOKEN')
|
||||
if not bot_token:
|
||||
raise ValueError('Discord bot token not found in .env file.')
|
||||
|
||||
api_key = os.getenv('GOOGLE_API_KEY')
|
||||
if not api_key:
|
||||
raise ValueError('GOOGLE_API_KEY is not set')
|
||||
|
||||
llm = ChatGoogle(model='gemini-2.0-flash-exp', api_key=api_key)
|
||||
|
||||
bot = DiscordBot(
|
||||
llm=llm, # required; instance of BaseChatModel
|
||||
prefix='$bu', # optional; prefix of messages to trigger browser-use, defaults to "$bu"
|
||||
ack=True, # optional; whether to acknowledge task receipt with a message, defaults to False
|
||||
browser_profile=BrowserProfile(
|
||||
headless=False
|
||||
), # optional; useful for changing headless mode or other browser configs, defaults to headless mode
|
||||
)
|
||||
|
||||
bot.run(
|
||||
token=bot_token, # required; Discord bot token
|
||||
)
|
||||
@@ -0,0 +1,331 @@
|
||||
"""
|
||||
Gmail 2FA Integration Example with Grant Mechanism
|
||||
This example demonstrates how to use the Gmail integration for handling 2FA codes
|
||||
during web automation with a robust credential grant and re-authentication system.
|
||||
|
||||
Features:
|
||||
- Automatic credential validation and setup
|
||||
- Interactive OAuth grant flow when credentials are missing/invalid
|
||||
- Fallback re-authentication mechanisms
|
||||
- Clear error handling and user guidance
|
||||
|
||||
Setup:
|
||||
1. Enable Gmail API in Google Cloud Console
|
||||
2. Create OAuth 2.0 credentials and download JSON
|
||||
3. Save credentials as ~/.config/browseruse/gmail_credentials.json
|
||||
4. Run this example - it will guide you through OAuth setup if needed
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Add the parent directory to the path so we can import browser_use
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from browser_use import Agent, ChatOpenAI, Tools
|
||||
from browser_use.config import CONFIG
|
||||
from browser_use.integrations.gmail import GmailService, register_gmail_actions
|
||||
|
||||
|
||||
class GmailGrantManager:
|
||||
"""
|
||||
Manages Gmail OAuth credential grants and authentication flows.
|
||||
Provides a robust mechanism for setting up and maintaining Gmail API access.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.config_dir = CONFIG.BROWSER_USE_CONFIG_DIR
|
||||
self.credentials_file = self.config_dir / 'gmail_credentials.json'
|
||||
self.token_file = self.config_dir / 'gmail_token.json'
|
||||
print(f'GmailGrantManager initialized with config_dir: {self.config_dir}')
|
||||
print(f'GmailGrantManager initialized with credentials_file: {self.credentials_file}')
|
||||
print(f'GmailGrantManager initialized with token_file: {self.token_file}')
|
||||
|
||||
def check_credentials_exist(self) -> bool:
|
||||
"""Check if OAuth credentials file exists."""
|
||||
return self.credentials_file.exists()
|
||||
|
||||
def check_token_exists(self) -> bool:
|
||||
"""Check if saved token file exists."""
|
||||
return self.token_file.exists()
|
||||
|
||||
def validate_credentials_format(self) -> tuple[bool, str]:
|
||||
"""
|
||||
Validate that the credentials file has the correct format.
|
||||
Returns (is_valid, error_message)
|
||||
"""
|
||||
if not self.check_credentials_exist():
|
||||
return False, 'Credentials file not found'
|
||||
|
||||
try:
|
||||
with open(self.credentials_file) as f:
|
||||
creds = json.load(f)
|
||||
|
||||
# Accept if either 'web' or 'installed' section exists and is not empty
|
||||
if creds.get('web') or creds.get('installed'):
|
||||
return True, 'Credentials file is valid'
|
||||
return False, "Invalid credentials format - neither 'web' nor 'installed' sections found"
|
||||
|
||||
except json.JSONDecodeError:
|
||||
return False, 'Credentials file is not valid JSON'
|
||||
except Exception as e:
|
||||
return False, f'Error reading credentials file: {e}'
|
||||
|
||||
async def setup_oauth_credentials(self) -> bool:
|
||||
"""
|
||||
Guide user through OAuth credentials setup process.
|
||||
Returns True if setup is successful.
|
||||
"""
|
||||
print('\n🔐 Gmail OAuth Credentials Setup Required')
|
||||
print('=' * 50)
|
||||
|
||||
if not self.check_credentials_exist():
|
||||
print('❌ Gmail credentials file not found')
|
||||
else:
|
||||
is_valid, error = self.validate_credentials_format()
|
||||
if not is_valid:
|
||||
print(f'❌ Gmail credentials file is invalid: {error}')
|
||||
|
||||
print('\n📋 To set up Gmail API access:')
|
||||
print('1. Go to https://console.cloud.google.com/')
|
||||
print('2. Create a new project or select an existing one')
|
||||
print('3. Enable the Gmail API:')
|
||||
print(' - Go to "APIs & Services" > "Library"')
|
||||
print(' - Search for "Gmail API" and enable it')
|
||||
print('4. Create OAuth 2.0 credentials:')
|
||||
print(' - Go to "APIs & Services" > "Credentials"')
|
||||
print(' - Click "Create Credentials" > "OAuth client ID"')
|
||||
print(' - Choose "Desktop application"')
|
||||
print(' - Download the JSON file')
|
||||
print(f'5. Save the JSON file as: {self.credentials_file}')
|
||||
print(f'6. Ensure the directory exists: {self.config_dir}')
|
||||
|
||||
# Create config directory if it doesn't exist
|
||||
self.config_dir.mkdir(parents=True, exist_ok=True)
|
||||
print(f'\n✅ Created config directory: {self.config_dir}')
|
||||
|
||||
# Wait for user to set up credentials
|
||||
while True:
|
||||
user_input = input('\n❓ Have you saved the credentials file? (y/n/skip): ').lower().strip()
|
||||
|
||||
if user_input == 'skip':
|
||||
print('⏭️ Skipping credential validation for now')
|
||||
return False
|
||||
elif user_input == 'y':
|
||||
if self.check_credentials_exist():
|
||||
is_valid, error = self.validate_credentials_format()
|
||||
if is_valid:
|
||||
print('✅ Credentials file found and validated!')
|
||||
return True
|
||||
else:
|
||||
print(f'❌ Credentials file is invalid: {error}')
|
||||
print('Please check the file format and try again.')
|
||||
else:
|
||||
print(f'❌ Credentials file still not found at: {self.credentials_file}')
|
||||
elif user_input == 'n':
|
||||
print('⏳ Please complete the setup steps above and try again.')
|
||||
else:
|
||||
print('Please enter y, n, or skip')
|
||||
|
||||
async def test_authentication(self, gmail_service: GmailService) -> tuple[bool, str]:
|
||||
"""
|
||||
Test Gmail authentication and return status.
|
||||
Returns (success, message)
|
||||
"""
|
||||
try:
|
||||
print('🔍 Testing Gmail authentication...')
|
||||
success = await gmail_service.authenticate()
|
||||
|
||||
if success and gmail_service.is_authenticated():
|
||||
print('✅ Gmail authentication successful!')
|
||||
return True, 'Authentication successful'
|
||||
else:
|
||||
return False, 'Authentication failed - invalid credentials or OAuth flow failed'
|
||||
|
||||
except Exception as e:
|
||||
return False, f'Authentication error: {e}'
|
||||
|
||||
async def handle_authentication_failure(self, gmail_service: GmailService, error_msg: str) -> bool:
|
||||
"""
|
||||
Handle authentication failures with fallback mechanisms.
|
||||
Returns True if recovery was successful.
|
||||
"""
|
||||
print(f'\n❌ Gmail authentication failed: {error_msg}')
|
||||
print('\n🔧 Attempting recovery...')
|
||||
|
||||
# Option 1: Try removing old token file
|
||||
if self.token_file.exists():
|
||||
print('🗑️ Removing old token file to force re-authentication...')
|
||||
try:
|
||||
self.token_file.unlink()
|
||||
print('✅ Old token file removed')
|
||||
|
||||
# Try authentication again
|
||||
success = await gmail_service.authenticate()
|
||||
if success:
|
||||
print('✅ Re-authentication successful!')
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f'❌ Failed to remove token file: {e}')
|
||||
|
||||
# Option 2: Validate and potentially re-setup credentials
|
||||
is_valid, cred_error = self.validate_credentials_format()
|
||||
if not is_valid:
|
||||
print(f'\n❌ Credentials file issue: {cred_error}')
|
||||
print('🔧 Initiating credential re-setup...')
|
||||
|
||||
return await self.setup_oauth_credentials()
|
||||
|
||||
# Option 3: Provide manual troubleshooting steps
|
||||
print('\n🔧 Manual troubleshooting steps:')
|
||||
print('1. Check that Gmail API is enabled in Google Cloud Console')
|
||||
print('2. Verify OAuth consent screen is configured')
|
||||
print('3. Ensure redirect URIs include http://localhost:8080')
|
||||
print('4. Check if credentials file is for the correct project')
|
||||
print('5. Try regenerating OAuth credentials in Google Cloud Console')
|
||||
|
||||
retry = input('\n❓ Would you like to retry authentication? (y/n): ').lower().strip()
|
||||
if retry == 'y':
|
||||
success = await gmail_service.authenticate()
|
||||
return success
|
||||
|
||||
return False
|
||||
|
||||
|
||||
async def main():
|
||||
print('🚀 Gmail 2FA Integration Example with Grant Mechanism')
|
||||
print('=' * 60)
|
||||
|
||||
# Initialize grant manager
|
||||
grant_manager = GmailGrantManager()
|
||||
|
||||
# Step 1: Check and validate credentials
|
||||
print('🔍 Step 1: Validating Gmail credentials...')
|
||||
|
||||
if not grant_manager.check_credentials_exist():
|
||||
print('❌ No Gmail credentials found')
|
||||
setup_success = await grant_manager.setup_oauth_credentials()
|
||||
if not setup_success:
|
||||
print('⏹️ Setup cancelled or failed. Exiting...')
|
||||
return
|
||||
else:
|
||||
is_valid, error = grant_manager.validate_credentials_format()
|
||||
if not is_valid:
|
||||
print(f'❌ Invalid credentials: {error}')
|
||||
setup_success = await grant_manager.setup_oauth_credentials()
|
||||
if not setup_success:
|
||||
print('⏹️ Setup cancelled or failed. Exiting...')
|
||||
return
|
||||
else:
|
||||
print('✅ Gmail credentials file found and validated')
|
||||
|
||||
# Step 2: Initialize Gmail service and test authentication
|
||||
print('\n🔍 Step 2: Testing Gmail authentication...')
|
||||
|
||||
gmail_service = GmailService()
|
||||
auth_success, auth_message = await grant_manager.test_authentication(gmail_service)
|
||||
|
||||
if not auth_success:
|
||||
print(f'❌ Initial authentication failed: {auth_message}')
|
||||
recovery_success = await grant_manager.handle_authentication_failure(gmail_service, auth_message)
|
||||
|
||||
if not recovery_success:
|
||||
print('❌ Failed to recover Gmail authentication. Please check your setup.')
|
||||
return
|
||||
|
||||
# Step 3: Initialize tools with authenticated service
|
||||
print('\n🔍 Step 3: Registering Gmail actions...')
|
||||
|
||||
tools = Tools()
|
||||
register_gmail_actions(tools, gmail_service=gmail_service)
|
||||
|
||||
print('✅ Gmail actions registered with tools')
|
||||
print('Available Gmail actions:')
|
||||
print('- get_recent_emails: Get recent emails with filtering')
|
||||
print()
|
||||
|
||||
# Initialize LLM
|
||||
llm = ChatOpenAI(model='gpt-4.1-mini')
|
||||
|
||||
# Step 4: Test Gmail functionality
|
||||
print('🔍 Step 4: Testing Gmail email retrieval...')
|
||||
|
||||
agent = Agent(task='Get recent emails from Gmail to test the integration is working properly', llm=llm, tools=tools)
|
||||
|
||||
try:
|
||||
history = await agent.run()
|
||||
print('✅ Gmail email retrieval test completed')
|
||||
except Exception as e:
|
||||
print(f'❌ Gmail email retrieval test failed: {e}')
|
||||
# Try one more recovery attempt
|
||||
print('🔧 Attempting final recovery...')
|
||||
recovery_success = await grant_manager.handle_authentication_failure(gmail_service, str(e))
|
||||
if recovery_success:
|
||||
print('✅ Recovery successful, re-running test...')
|
||||
history = await agent.run()
|
||||
else:
|
||||
print('❌ Final recovery failed. Please check your Gmail API setup.')
|
||||
return
|
||||
|
||||
print('\n' + '=' * 60)
|
||||
|
||||
# Step 5: Demonstrate 2FA code finding
|
||||
print('🔍 Step 5: Testing 2FA code detection...')
|
||||
|
||||
agent2 = Agent(
|
||||
task='Search for any 2FA verification codes or OTP codes in recent Gmail emails from the last 30 minutes',
|
||||
llm=llm,
|
||||
tools=tools,
|
||||
)
|
||||
|
||||
history2 = await agent2.run()
|
||||
print('✅ 2FA code search completed')
|
||||
|
||||
print('\n' + '=' * 60)
|
||||
|
||||
# Step 6: Simulate complete login flow
|
||||
print('🔍 Step 6: Demonstrating complete 2FA login flow...')
|
||||
|
||||
agent3 = Agent(
|
||||
task="""
|
||||
Demonstrate a complete 2FA-enabled login flow:
|
||||
1. Check for any existing 2FA codes in recent emails
|
||||
2. Explain how the agent would handle a typical login:
|
||||
- Navigate to a login page
|
||||
- Enter credentials
|
||||
- Wait for 2FA prompt
|
||||
- Use get_recent_emails to find the verification code
|
||||
- Extract and enter the 2FA code
|
||||
3. Show what types of emails and codes can be detected
|
||||
""",
|
||||
llm=llm,
|
||||
tools=tools,
|
||||
)
|
||||
|
||||
history3 = await agent3.run()
|
||||
print('✅ Complete 2FA flow demonstration completed')
|
||||
|
||||
print('\n' + '=' * 60)
|
||||
print('🎉 Gmail 2FA Integration with Grant Mechanism completed successfully!')
|
||||
print('\n💡 Key features demonstrated:')
|
||||
print('- ✅ Automatic credential validation and setup')
|
||||
print('- ✅ Robust error handling and recovery mechanisms')
|
||||
print('- ✅ Interactive OAuth grant flow')
|
||||
print('- ✅ Token refresh and re-authentication')
|
||||
print('- ✅ 2FA code detection and extraction')
|
||||
print('\n🔧 Grant mechanism benefits:')
|
||||
print('- Handles missing or invalid credentials gracefully')
|
||||
print('- Provides clear setup instructions')
|
||||
print('- Automatically recovers from authentication failures')
|
||||
print('- Validates credential format before use')
|
||||
print('- Offers multiple fallback options')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,76 @@
|
||||
# Slack Integration
|
||||
|
||||
Steps to create and configure a Slack bot:
|
||||
|
||||
1. Create a Slack App:
|
||||
* Go to the Slack API: https://api.slack.com/apps
|
||||
* Click on "Create New App".
|
||||
* Choose "From scratch" and give your app a name and select the workspace.
|
||||
* Provide a name and description for your bot (these are required fields).
|
||||
2. Configure the Bot:
|
||||
* Navigate to the "OAuth & Permissions" tab on the left side of the screen.
|
||||
* Under "Scopes", add the necessary bot token scopes (add these "chat:write", "channels:history", "im:history").
|
||||
3. Enable Event Subscriptions:
|
||||
* Navigate to the "Event Subscriptions" tab.
|
||||
* Enable events and add the necessary bot events (add these "message.channels", "message.im").
|
||||
* Add your request URL (you can use ngrok to expose your local server if needed). [See how to set up ngrok](#installing-and-starting-ngrok).
|
||||
* **Note:** The URL provided by ngrok is ephemeral and will change each time ngrok is started. You will need to update the request URL in the bot's settings each time you restart ngrok. [See how to update the request URL](#updating-the-request-url-in-bots-settings).
|
||||
4. Add the bot to your Slack workspace:
|
||||
* Navigate to the "OAuth & Permissions" tab.
|
||||
* Under "OAuth Tokens for Your Workspace", click on "Install App to Workspace".
|
||||
* Follow the prompts to authorize the app and add it to your workspace.
|
||||
5. Set up environment variables:
|
||||
* Obtain the `SLACK_SIGNING_SECRET`:
|
||||
* Go to the Slack API: https://api.slack.com/apps
|
||||
* Select your app.
|
||||
* Navigate to the "Basic Information" tab.
|
||||
* Copy the "Signing Secret".
|
||||
* Obtain the `SLACK_BOT_TOKEN`:
|
||||
* Go to the Slack API: https://api.slack.com/apps
|
||||
* Select your app.
|
||||
* Navigate to the "OAuth & Permissions" tab.
|
||||
* Copy the "Bot User OAuth Token".
|
||||
* Create a `.env` file in the root directory of your project and add the following lines:
|
||||
```env
|
||||
SLACK_SIGNING_SECRET=your-signing-secret
|
||||
SLACK_BOT_TOKEN=your-bot-token
|
||||
```
|
||||
6. Invite the bot to a channel:
|
||||
* Use the `/invite @your-bot-name` command in the Slack channel where you want the bot to be active.
|
||||
7. Run the code in `examples/slack_example.py` to start the bot with your bot token and signing secret.
|
||||
8. Write e.g. "$bu what's the weather in Tokyo?" to start a browser-use task and get a response inside the Slack channel.
|
||||
|
||||
## Installing and Starting ngrok
|
||||
|
||||
To expose your local server to the internet, you can use ngrok. Follow these steps to install and start ngrok:
|
||||
|
||||
1. Download ngrok from the official website: https://ngrok.com/download
|
||||
2. Create a free account and follow the official steps to install ngrok.
|
||||
3. Start ngrok by running the following command in your terminal:
|
||||
```sh
|
||||
ngrok http 3000
|
||||
```
|
||||
Replace `3000` with the port number your local server is running on.
|
||||
|
||||
## Updating the Request URL in Bot's Settings
|
||||
|
||||
If you need to update the request URL (e.g., when the ngrok URL changes), follow these steps:
|
||||
|
||||
1. Go to the Slack API: https://api.slack.com/apps
|
||||
2. Select your app.
|
||||
3. Navigate to the "Event Subscriptions" tab.
|
||||
4. Update the "Request URL" field with the new ngrok URL. The URL should be something like: `https://<ngrok-id>.ngrok-free.app/slack/events`
|
||||
5. Save the changes.
|
||||
|
||||
## Installing Required Packages
|
||||
|
||||
To run this example, you need to install the following packages:
|
||||
|
||||
- `fastapi`
|
||||
- `uvicorn`
|
||||
- `slack_sdk`
|
||||
|
||||
You can install these packages using pip:
|
||||
|
||||
```sh
|
||||
pip install fastapi uvicorn slack_sdk
|
||||
@@ -0,0 +1,130 @@
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from typing import Annotated
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from fastapi import Depends, FastAPI, HTTPException, Request
|
||||
from slack_sdk.errors import SlackApiError # type: ignore
|
||||
from slack_sdk.signature import SignatureVerifier # type: ignore
|
||||
from slack_sdk.web.async_client import AsyncWebClient # type: ignore
|
||||
|
||||
from browser_use.agent.service import Agent
|
||||
from browser_use.browser import BrowserProfile, BrowserSession
|
||||
from browser_use.llm import BaseChatModel
|
||||
from browser_use.logging_config import setup_logging
|
||||
|
||||
setup_logging()
|
||||
logger = logging.getLogger('slack')
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class SlackBot:
|
||||
def __init__(
|
||||
self,
|
||||
llm: BaseChatModel,
|
||||
bot_token: str,
|
||||
signing_secret: str,
|
||||
ack: bool = False,
|
||||
browser_profile: BrowserProfile = BrowserProfile(headless=True),
|
||||
):
|
||||
if not bot_token or not signing_secret:
|
||||
raise ValueError('Bot token and signing secret must be provided')
|
||||
|
||||
self.llm = llm
|
||||
self.ack = ack
|
||||
self.browser_profile = browser_profile
|
||||
self.client = AsyncWebClient(token=bot_token)
|
||||
self.signature_verifier = SignatureVerifier(signing_secret)
|
||||
self.processed_events = set()
|
||||
logger.info('SlackBot initialized')
|
||||
|
||||
async def handle_event(self, event, event_id):
|
||||
try:
|
||||
logger.info(f'Received event id: {event_id}')
|
||||
if not event_id:
|
||||
logger.warning('Event ID missing in event data')
|
||||
return
|
||||
|
||||
if event_id in self.processed_events:
|
||||
logger.info(f'Event {event_id} already processed')
|
||||
return
|
||||
self.processed_events.add(event_id)
|
||||
|
||||
if 'subtype' in event and event['subtype'] == 'bot_message':
|
||||
return
|
||||
|
||||
text = event.get('text')
|
||||
user_id = event.get('user')
|
||||
if text and text.startswith('$bu '):
|
||||
task = text[len('$bu ') :].strip()
|
||||
if self.ack:
|
||||
try:
|
||||
await self.send_message(
|
||||
event['channel'], f'<@{user_id}> Starting browser use task...', thread_ts=event.get('ts')
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f'Error sending start message: {e}')
|
||||
|
||||
try:
|
||||
agent_message = await self.run_agent(task)
|
||||
await self.send_message(event['channel'], f'<@{user_id}> {agent_message}', thread_ts=event.get('ts'))
|
||||
except Exception as e:
|
||||
await self.send_message(event['channel'], f'Error during task execution: {str(e)}', thread_ts=event.get('ts'))
|
||||
except Exception as e:
|
||||
logger.error(f'Error in handle_event: {str(e)}')
|
||||
|
||||
async def run_agent(self, task: str) -> str:
|
||||
try:
|
||||
browser_session = BrowserSession(browser_profile=self.browser_profile)
|
||||
agent = Agent(task=task, llm=self.llm, browser_session=browser_session)
|
||||
result = await agent.run()
|
||||
|
||||
agent_message = None
|
||||
if result.is_done():
|
||||
agent_message = result.history[-1].result[0].extracted_content
|
||||
|
||||
if agent_message is None:
|
||||
agent_message = 'Oops! Something went wrong while running Browser-Use.'
|
||||
|
||||
return agent_message
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Error during task execution: {str(e)}')
|
||||
return f'Error during task execution: {str(e)}'
|
||||
|
||||
async def send_message(self, channel, text, thread_ts=None):
|
||||
try:
|
||||
await self.client.chat_postMessage(channel=channel, text=text, thread_ts=thread_ts)
|
||||
except SlackApiError as e:
|
||||
logger.error(f'Error sending message: {e.response["error"]}')
|
||||
|
||||
|
||||
@app.post('/slack/events')
|
||||
async def slack_events(request: Request, slack_bot: Annotated[SlackBot, Depends()]):
|
||||
try:
|
||||
if not slack_bot.signature_verifier.is_valid_request(await request.body(), dict(request.headers)):
|
||||
logger.warning('Request verification failed')
|
||||
raise HTTPException(status_code=400, detail='Request verification failed')
|
||||
|
||||
event_data = await request.json()
|
||||
logger.info(f'Received event data: {event_data}')
|
||||
if 'challenge' in event_data:
|
||||
return {'challenge': event_data['challenge']}
|
||||
|
||||
if 'event' in event_data:
|
||||
try:
|
||||
await slack_bot.handle_event(event_data.get('event'), event_data.get('event_id'))
|
||||
except Exception as e:
|
||||
logger.error(f'Error handling event: {str(e)}')
|
||||
|
||||
return {}
|
||||
except Exception as e:
|
||||
logger.error(f'Error in slack_events: {str(e)}')
|
||||
raise HTTPException(status_code=500, detail='Internal Server Error')
|
||||
@@ -0,0 +1,45 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
from browser_use.browser import BrowserProfile
|
||||
from browser_use.llm import ChatGoogle
|
||||
from examples.integrations.slack.slack_api import SlackBot, app
|
||||
|
||||
# load credentials from environment variables
|
||||
bot_token = os.getenv('SLACK_BOT_TOKEN')
|
||||
if not bot_token:
|
||||
raise ValueError('Slack bot token not found in .env file.')
|
||||
|
||||
signing_secret = os.getenv('SLACK_SIGNING_SECRET')
|
||||
if not signing_secret:
|
||||
raise ValueError('Slack signing secret not found in .env file.')
|
||||
|
||||
api_key = os.getenv('GOOGLE_API_KEY')
|
||||
if not api_key:
|
||||
raise ValueError('GOOGLE_API_KEY is not set')
|
||||
|
||||
llm = ChatGoogle(model='gemini-2.0-flash-exp', api_key=api_key)
|
||||
|
||||
slack_bot = SlackBot(
|
||||
llm=llm, # required; instance of BaseChatModel
|
||||
bot_token=bot_token, # required; Slack bot token
|
||||
signing_secret=signing_secret, # required; Slack signing secret
|
||||
ack=True, # optional; whether to acknowledge task receipt with a message, defaults to False
|
||||
browser_profile=BrowserProfile(
|
||||
headless=True
|
||||
), # optional; useful for changing headless mode or other browser configs, defaults to headless mode
|
||||
)
|
||||
|
||||
app.dependency_overrides[SlackBot] = lambda: slack_bot
|
||||
|
||||
if __name__ == '__main__':
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run('integrations.slack.slack_api:app', host='0.0.0.0', port=3000)
|
||||
@@ -0,0 +1,100 @@
|
||||
"""
|
||||
AWS Bedrock Examples
|
||||
|
||||
This file demonstrates how to use AWS Bedrock models with browser-use.
|
||||
We provide two classes:
|
||||
1. ChatAnthropicBedrock - Convenience class for Anthropic Claude models
|
||||
2. ChatAWSBedrock - General AWS Bedrock client supporting all providers
|
||||
|
||||
Requirements:
|
||||
- AWS credentials configured via environment variables
|
||||
- boto3 installed: pip install boto3
|
||||
- Access to AWS Bedrock models in your region
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from browser_use import Agent
|
||||
from browser_use.llm import ChatAnthropicBedrock, ChatAWSBedrock
|
||||
|
||||
|
||||
async def example_anthropic_bedrock():
|
||||
"""Example using ChatAnthropicBedrock - convenience class for Claude models."""
|
||||
print('🔹 ChatAnthropicBedrock Example')
|
||||
|
||||
# Initialize with Anthropic Claude via AWS Bedrock
|
||||
llm = ChatAnthropicBedrock(
|
||||
model='us.anthropic.claude-sonnet-4-20250514-v1:0',
|
||||
aws_region='us-east-1',
|
||||
temperature=0.7,
|
||||
)
|
||||
|
||||
print(f'Model: {llm.name}')
|
||||
print(f'Provider: {llm.provider}')
|
||||
|
||||
# Create agent
|
||||
agent = Agent(
|
||||
task="Navigate to google.com and search for 'AWS Bedrock pricing'",
|
||||
llm=llm,
|
||||
)
|
||||
|
||||
print("Task: Navigate to google.com and search for 'AWS Bedrock pricing'")
|
||||
|
||||
# Run the agent
|
||||
result = await agent.run(max_steps=2)
|
||||
print(f'Result: {result}')
|
||||
|
||||
|
||||
async def example_aws_bedrock():
|
||||
"""Example using ChatAWSBedrock - general client for any Bedrock model."""
|
||||
print('\n🔹 ChatAWSBedrock Example')
|
||||
|
||||
# Initialize with any AWS Bedrock model (using Meta Llama as example)
|
||||
llm = ChatAWSBedrock(
|
||||
model='us.meta.llama4-maverick-17b-instruct-v1:0',
|
||||
aws_region='us-east-1',
|
||||
temperature=0.5,
|
||||
)
|
||||
|
||||
print(f'Model: {llm.name}')
|
||||
print(f'Provider: {llm.provider}')
|
||||
|
||||
# Create agent
|
||||
agent = Agent(
|
||||
task='Go to github.com and find the most popular Python repository',
|
||||
llm=llm,
|
||||
)
|
||||
|
||||
print('Task: Go to github.com and find the most popular Python repository')
|
||||
|
||||
# Run the agent
|
||||
result = await agent.run(max_steps=2)
|
||||
print(f'Result: {result}')
|
||||
|
||||
|
||||
async def main():
|
||||
"""Run AWS Bedrock examples."""
|
||||
print('🚀 AWS Bedrock Examples')
|
||||
print('=' * 40)
|
||||
|
||||
print('Make sure you have AWS credentials configured:')
|
||||
print('export AWS_ACCESS_KEY_ID=your_key')
|
||||
print('export AWS_SECRET_ACCESS_KEY=your_secret')
|
||||
print('export AWS_DEFAULT_REGION=us-east-1')
|
||||
print('=' * 40)
|
||||
|
||||
try:
|
||||
# Run both examples
|
||||
await example_aws_bedrock()
|
||||
await example_anthropic_bedrock()
|
||||
|
||||
except Exception as e:
|
||||
print(f'❌ Error: {e}')
|
||||
print('Make sure you have:')
|
||||
print('- Valid AWS credentials configured')
|
||||
print('- Access to AWS Bedrock in your region')
|
||||
print('- boto3 installed: pip install boto3')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,51 @@
|
||||
"""
|
||||
Simple try of the agent with Azure OpenAI.
|
||||
|
||||
@dev You need to add AZURE_OPENAI_KEY and AZURE_OPENAI_ENDPOINT to your environment variables.
|
||||
|
||||
For GPT-5.1 Codex models (gpt-5.1-codex-mini, etc.), use:
|
||||
llm = ChatAzureOpenAI(
|
||||
model='gpt-5.1-codex-mini',
|
||||
api_version='2025-03-01-preview', # Required for Responses API
|
||||
# use_responses_api='auto', # Default: auto-detects based on model
|
||||
)
|
||||
|
||||
The Responses API is automatically used for models that require it.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
from browser_use import Agent
|
||||
from browser_use.llm import ChatAzureOpenAI
|
||||
|
||||
# Make sure your deployment exists, double check the region and model name
|
||||
api_key = os.getenv('AZURE_OPENAI_KEY')
|
||||
azure_endpoint = os.getenv('AZURE_OPENAI_ENDPOINT')
|
||||
llm = ChatAzureOpenAI(
|
||||
model='gpt-5.1-codex-mini', api_key=api_key, azure_endpoint=azure_endpoint, api_version='2025-03-01-preview'
|
||||
)
|
||||
|
||||
TASK = """
|
||||
Go to google.com/travel/flights and find the cheapest flight from New York to Paris on next Sunday
|
||||
"""
|
||||
|
||||
agent = Agent(
|
||||
task=TASK,
|
||||
llm=llm,
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
await agent.run(max_steps=25)
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,36 @@
|
||||
"""
|
||||
Example of the fastest + smartest LLM for browser automation.
|
||||
|
||||
Setup:
|
||||
1. Get your API key from https://cloud.browser-use.com/new-api-key
|
||||
2. Set environment variable: export BROWSER_USE_API_KEY="your-key"
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from browser_use import Agent, ChatBrowserUse
|
||||
|
||||
load_dotenv()
|
||||
|
||||
if not os.getenv('BROWSER_USE_API_KEY'):
|
||||
raise ValueError('BROWSER_USE_API_KEY is not set')
|
||||
|
||||
|
||||
async def main():
|
||||
# `bu-2-0` is the optimized default. ChatBrowserUse can also route to
|
||||
# provider-prefixed models (e.g. 'anthropic/claude-sonnet-4-6', 'openai/gpt-5.5',
|
||||
# 'google/gemini-3-pro') through the same gateway - see browser_use_provider_models.py.
|
||||
agent = Agent(
|
||||
task='Find the number of stars of the browser-use repo',
|
||||
llm=ChatBrowserUse(model='bu-2-0'),
|
||||
)
|
||||
|
||||
# Run the agent
|
||||
await agent.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,46 @@
|
||||
"""
|
||||
Point ChatBrowserUse at provider-prefixed models via the Browser Use gateway.
|
||||
|
||||
`ChatBrowserUse` isn't limited to the `bu-*` models - it also accepts
|
||||
provider-prefixed ids:
|
||||
|
||||
- 'anthropic/claude-sonnet-4-6'
|
||||
- 'openai/gpt-5.5'
|
||||
- 'google/gemini-3-pro'
|
||||
|
||||
A single `BROWSER_USE_API_KEY` reaches Claude, GPT, and Gemini without
|
||||
juggling separate OpenAI / Anthropic / Google keys. For the best speed and
|
||||
cost, the default `bu-*` models are still recommended.
|
||||
|
||||
Setup:
|
||||
1. Get your API key from https://cloud.browser-use.com/new-api-key
|
||||
2. Set environment variable: export BROWSER_USE_API_KEY="your-key"
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from browser_use import Agent, ChatBrowserUse
|
||||
|
||||
load_dotenv()
|
||||
|
||||
if not os.getenv('BROWSER_USE_API_KEY'):
|
||||
raise ValueError('BROWSER_USE_API_KEY is not set')
|
||||
|
||||
# Swap this for any provider-prefixed id the gateway supports, e.g.
|
||||
# 'openai/gpt-5.5' or 'google/gemini-3-pro'
|
||||
MODEL = 'anthropic/claude-sonnet-4-6'
|
||||
|
||||
|
||||
async def main():
|
||||
agent = Agent(
|
||||
task='Find the number of stars of the browser-use repo',
|
||||
llm=ChatBrowserUse(model=MODEL),
|
||||
)
|
||||
await agent.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
Setup:
|
||||
1. Get your API key from https://cloud.browser-use.com/new-api-key
|
||||
2. Set environment variable: export BROWSER_USE_API_KEY="your-key"
|
||||
"""
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from browser_use import Agent, ChatBrowserUse
|
||||
|
||||
load_dotenv()
|
||||
|
||||
try:
|
||||
from lmnr import Laminar
|
||||
|
||||
Laminar.initialize()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Point to local llm-use server for testing
|
||||
llm = ChatBrowserUse(
|
||||
model='browser-use/bu-30b-a3b-preview', # BU Open Source Model!!
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
task='Find the number of stars of browser-use and stagehand. Tell me which one has more stars :)',
|
||||
llm=llm,
|
||||
flash_mode=True,
|
||||
)
|
||||
agent.run_sync()
|
||||
@@ -0,0 +1,79 @@
|
||||
"""
|
||||
Example of using Cerebras with browser-use.
|
||||
|
||||
To use this example:
|
||||
1. Set your CEREBRAS_API_KEY environment variable
|
||||
2. Run this script
|
||||
|
||||
Cerebras integration is working great for:
|
||||
- Direct text generation
|
||||
- Simple tasks without complex structured output
|
||||
- Fast inference for web automation
|
||||
|
||||
Available Cerebras models (9 total):
|
||||
Small/Fast models (8B-32B):
|
||||
- cerebras_llama3_1_8b (8B parameters, fast)
|
||||
- cerebras_llama_4_scout_17b_16e_instruct (17B, instruction-tuned)
|
||||
- cerebras_llama_4_maverick_17b_128e_instruct (17B, extended context)
|
||||
- cerebras_qwen_3_32b (32B parameters)
|
||||
|
||||
Large/Capable models (70B-480B):
|
||||
- cerebras_llama3_3_70b (70B parameters, latest version)
|
||||
- cerebras_gpt_oss_120b (120B parameters, OpenAI's model)
|
||||
- cerebras_qwen_3_235b_a22b_instruct_2507 (235B, instruction-tuned)
|
||||
- cerebras_qwen_3_235b_a22b_thinking_2507 (235B, complex reasoning)
|
||||
- cerebras_qwen_3_coder_480b (480B, code generation)
|
||||
|
||||
Note: Cerebras has some limitations with complex structured output due to JSON schema compatibility.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from browser_use import Agent
|
||||
|
||||
|
||||
async def main():
|
||||
# Set your API key (recommended to use environment variable)
|
||||
api_key = os.getenv('CEREBRAS_API_KEY')
|
||||
if not api_key:
|
||||
raise ValueError('Please set CEREBRAS_API_KEY environment variable')
|
||||
|
||||
# Option 1: Use the pre-configured model instance (recommended)
|
||||
from browser_use import llm
|
||||
|
||||
# Choose your model:
|
||||
# Small/Fast models:
|
||||
# model = llm.cerebras_llama3_1_8b # 8B, fast
|
||||
# model = llm.cerebras_llama_4_scout_17b_16e_instruct # 17B, instruction-tuned
|
||||
# model = llm.cerebras_llama_4_maverick_17b_128e_instruct # 17B, extended context
|
||||
# model = llm.cerebras_qwen_3_32b # 32B
|
||||
|
||||
# Large/Capable models:
|
||||
# model = llm.cerebras_llama3_3_70b # 70B, latest
|
||||
# model = llm.cerebras_gpt_oss_120b # 120B, OpenAI's model
|
||||
# model = llm.cerebras_qwen_3_235b_a22b_instruct_2507 # 235B, instruction-tuned
|
||||
model = llm.cerebras_qwen_3_235b_a22b_thinking_2507 # 235B, complex reasoning
|
||||
# model = llm.cerebras_qwen_3_coder_480b # 480B, code generation
|
||||
|
||||
# Option 2: Create the model instance directly
|
||||
# model = ChatCerebras(
|
||||
# model="qwen-3-coder-480b", # or any other model ID
|
||||
# api_key=os.getenv("CEREBRAS_API_KEY"),
|
||||
# temperature=0.2,
|
||||
# max_tokens=4096,
|
||||
# )
|
||||
|
||||
# Create and run the agent with a simple task
|
||||
task = 'Explain the concept of quantum entanglement in simple terms.'
|
||||
agent = Agent(task=task, llm=model)
|
||||
|
||||
print(f'Running task with Cerebras {model.name} (ID: {model.model}): {task}')
|
||||
history = await agent.run(max_steps=3)
|
||||
result = history.final_result()
|
||||
|
||||
print(f'Result: {result}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
Simple script that runs the task of opening amazon and searching.
|
||||
@dev Ensure we have a `ANTHROPIC_API_KEY` variable in our `.env` file.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from browser_use import Agent
|
||||
from browser_use.llm import ChatAnthropic
|
||||
|
||||
llm = ChatAnthropic(model='claude-sonnet-4-0', temperature=0.0)
|
||||
|
||||
agent = Agent(
|
||||
task='Go to amazon.com, search for laptop, sort by best rating, and give me the price of the first result',
|
||||
llm=llm,
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
await agent.run(max_steps=10)
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,36 @@
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from browser_use import Agent
|
||||
from browser_use.llm import ChatDeepSeek
|
||||
|
||||
# Add your custom instructions
|
||||
extend_system_message = """
|
||||
Remember the most important rules:
|
||||
1. When performing a search task, open https://www.google.com/ first for search.
|
||||
2. Final output.
|
||||
"""
|
||||
deepseek_api_key = os.getenv('DEEPSEEK_API_KEY')
|
||||
if deepseek_api_key is None:
|
||||
print('Make sure you have DEEPSEEK_API_KEY:')
|
||||
print('export DEEPSEEK_API_KEY=your_key')
|
||||
exit(0)
|
||||
|
||||
|
||||
async def main():
|
||||
llm = ChatDeepSeek(
|
||||
base_url='https://api.deepseek.com/v1',
|
||||
model='deepseek-chat',
|
||||
api_key=deepseek_api_key,
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
task='What should we pay attention to in the recent new rules on tariffs in China-US trade?',
|
||||
llm=llm,
|
||||
use_vision=False,
|
||||
extend_system_message=extend_system_message,
|
||||
)
|
||||
await agent.run()
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,31 @@
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from browser_use import Agent, ChatGoogle
|
||||
|
||||
load_dotenv()
|
||||
|
||||
api_key = os.getenv('GOOGLE_API_KEY')
|
||||
if not api_key:
|
||||
raise ValueError('GOOGLE_API_KEY is not set')
|
||||
|
||||
|
||||
async def run_search():
|
||||
llm = ChatGoogle(model='gemini-3-pro-preview', api_key=api_key)
|
||||
|
||||
agent = Agent(
|
||||
llm=llm,
|
||||
task='How many stars does the browser-use repo have?',
|
||||
flash_mode=True,
|
||||
)
|
||||
|
||||
await agent.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(run_search())
|
||||
@@ -0,0 +1,30 @@
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from browser_use import Agent, ChatGoogle
|
||||
|
||||
load_dotenv()
|
||||
|
||||
api_key = os.getenv('GOOGLE_API_KEY')
|
||||
if not api_key:
|
||||
raise ValueError('GOOGLE_API_KEY is not set')
|
||||
|
||||
|
||||
async def run_search():
|
||||
llm = ChatGoogle(model='gemini-3-flash-preview', api_key=api_key)
|
||||
agent = Agent(
|
||||
llm=llm,
|
||||
task='How many stars does the browser-use repo have?',
|
||||
flash_mode=True,
|
||||
)
|
||||
|
||||
await agent.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(run_search())
|
||||
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
Simple try of the agent.
|
||||
|
||||
@dev You need to add OPENAI_API_KEY to your environment variables.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from browser_use import Agent, ChatOpenAI
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# All the models are type safe from OpenAI in case you need a list of supported models
|
||||
llm = ChatOpenAI(model='gpt-4.1-mini')
|
||||
agent = Agent(
|
||||
task='Go to amazon.com, click on the first link, and give me the title of the page',
|
||||
llm=llm,
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
await agent.run(max_steps=10)
|
||||
input('Press Enter to continue...')
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
Simple try of the agent.
|
||||
|
||||
@dev You need to add OPENAI_API_KEY to your environment variables.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from browser_use import Agent, ChatOpenAI
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# All the models are type safe from OpenAI in case you need a list of supported models
|
||||
llm = ChatOpenAI(model='gpt-5-mini')
|
||||
agent = Agent(
|
||||
llm=llm,
|
||||
task='Find out which one is cooler: the monkey park or a dolphin tour in Tenerife?',
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
await agent.run(max_steps=20)
|
||||
input('Press Enter to continue...')
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,33 @@
|
||||
# Langchain Models (legacy)
|
||||
|
||||
This directory contains example of how to still use Langchain models with the new Browser Use chat models.
|
||||
|
||||
## How to use
|
||||
|
||||
```python
|
||||
from langchain_openai import ChatOpenAI
|
||||
|
||||
from browser_use import Agent
|
||||
from .chat import ChatLangchain
|
||||
|
||||
async def main():
|
||||
"""Basic example using ChatLangchain with OpenAI through LangChain."""
|
||||
|
||||
# Create a LangChain model (OpenAI)
|
||||
langchain_model = ChatOpenAI(
|
||||
model='gpt-4.1-mini',
|
||||
temperature=0.1,
|
||||
)
|
||||
|
||||
# Wrap it with ChatLangchain to make it compatible with browser-use
|
||||
llm = ChatLangchain(chat=langchain_model)
|
||||
|
||||
agent = Agent(
|
||||
task="Go to google.com and search for 'browser automation with Python'",
|
||||
llm=llm,
|
||||
)
|
||||
|
||||
history = await agent.run()
|
||||
|
||||
print(history.history)
|
||||
```
|
||||
@@ -0,0 +1,195 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, TypeVar, overload
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from browser_use.llm.base import BaseChatModel
|
||||
from browser_use.llm.exceptions import ModelProviderError
|
||||
from browser_use.llm.messages import BaseMessage
|
||||
from browser_use.llm.views import ChatInvokeCompletion, ChatInvokeUsage
|
||||
from examples.models.langchain.serializer import LangChainMessageSerializer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langchain_core.language_models.chat_models import BaseChatModel as LangChainBaseChatModel # type: ignore
|
||||
from langchain_core.messages import AIMessage as LangChainAIMessage # type: ignore
|
||||
|
||||
T = TypeVar('T', bound=BaseModel)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChatLangchain(BaseChatModel):
|
||||
"""
|
||||
A wrapper around LangChain BaseChatModel that implements the browser-use BaseChatModel protocol.
|
||||
|
||||
This class allows you to use any LangChain-compatible model with browser-use.
|
||||
"""
|
||||
|
||||
# The LangChain model to wrap
|
||||
chat: 'LangChainBaseChatModel'
|
||||
|
||||
@property
|
||||
def model(self) -> str:
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def provider(self) -> str:
|
||||
"""Return the provider name based on the LangChain model class."""
|
||||
model_class_name = self.chat.__class__.__name__.lower()
|
||||
if 'openai' in model_class_name:
|
||||
return 'openai'
|
||||
elif 'anthropic' in model_class_name or 'claude' in model_class_name:
|
||||
return 'anthropic'
|
||||
elif 'google' in model_class_name or 'gemini' in model_class_name:
|
||||
return 'google'
|
||||
elif 'groq' in model_class_name:
|
||||
return 'groq'
|
||||
elif 'ollama' in model_class_name:
|
||||
return 'ollama'
|
||||
elif 'deepseek' in model_class_name:
|
||||
return 'deepseek'
|
||||
else:
|
||||
return 'langchain'
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the model name."""
|
||||
# Try to get model name from the LangChain model using getattr to avoid type errors
|
||||
model_name = getattr(self.chat, 'model_name', None)
|
||||
if model_name:
|
||||
return str(model_name)
|
||||
|
||||
model_attr = getattr(self.chat, 'model', None)
|
||||
if model_attr:
|
||||
return str(model_attr)
|
||||
|
||||
return self.chat.__class__.__name__
|
||||
|
||||
def _get_usage(self, response: 'LangChainAIMessage') -> ChatInvokeUsage | None:
|
||||
usage = response.usage_metadata
|
||||
if usage is None:
|
||||
return None
|
||||
|
||||
prompt_tokens = usage['input_tokens'] or 0
|
||||
completion_tokens = usage['output_tokens'] or 0
|
||||
total_tokens = usage['total_tokens'] or 0
|
||||
|
||||
input_token_details = usage.get('input_token_details', None)
|
||||
|
||||
if input_token_details is not None:
|
||||
prompt_cached_tokens = input_token_details.get('cache_read', None)
|
||||
prompt_cache_creation_tokens = input_token_details.get('cache_creation', None)
|
||||
else:
|
||||
prompt_cached_tokens = None
|
||||
prompt_cache_creation_tokens = None
|
||||
|
||||
return ChatInvokeUsage(
|
||||
prompt_tokens=prompt_tokens,
|
||||
prompt_cached_tokens=prompt_cached_tokens,
|
||||
prompt_cache_creation_tokens=prompt_cache_creation_tokens,
|
||||
prompt_image_tokens=None,
|
||||
completion_tokens=completion_tokens,
|
||||
total_tokens=total_tokens,
|
||||
)
|
||||
|
||||
@overload
|
||||
async def ainvoke(self, messages: list[BaseMessage], output_format: None = None) -> ChatInvokeCompletion[str]: ...
|
||||
|
||||
@overload
|
||||
async def ainvoke(self, messages: list[BaseMessage], output_format: type[T]) -> ChatInvokeCompletion[T]: ...
|
||||
|
||||
async def ainvoke(
|
||||
self, messages: list[BaseMessage], output_format: type[T] | None = None
|
||||
) -> ChatInvokeCompletion[T] | ChatInvokeCompletion[str]:
|
||||
"""
|
||||
Invoke the LangChain model with the given messages.
|
||||
|
||||
Args:
|
||||
messages: List of browser-use chat messages
|
||||
output_format: Optional Pydantic model class for structured output (not supported in basic LangChain integration)
|
||||
|
||||
Returns:
|
||||
Either a string response or an instance of output_format
|
||||
"""
|
||||
|
||||
# Convert browser-use messages to LangChain messages
|
||||
langchain_messages = LangChainMessageSerializer.serialize_messages(messages)
|
||||
|
||||
try:
|
||||
if output_format is None:
|
||||
# Return string response
|
||||
response = await self.chat.ainvoke(langchain_messages) # type: ignore
|
||||
|
||||
# Import at runtime for isinstance check
|
||||
from langchain_core.messages import AIMessage as LangChainAIMessage # type: ignore
|
||||
|
||||
if not isinstance(response, LangChainAIMessage):
|
||||
raise ModelProviderError(
|
||||
message=f'Response is not an AIMessage: {type(response)}',
|
||||
model=self.name,
|
||||
)
|
||||
|
||||
# Extract content from LangChain response
|
||||
content = response.content if hasattr(response, 'content') else str(response)
|
||||
|
||||
usage = self._get_usage(response)
|
||||
return ChatInvokeCompletion(
|
||||
completion=str(content),
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
else:
|
||||
# Use LangChain's structured output capability
|
||||
try:
|
||||
structured_chat = self.chat.with_structured_output(output_format)
|
||||
parsed_object = await structured_chat.ainvoke(langchain_messages)
|
||||
|
||||
# For structured output, usage metadata is typically not available
|
||||
# in the parsed object since it's a Pydantic model, not an AIMessage
|
||||
usage = None
|
||||
|
||||
# Type cast since LangChain's with_structured_output returns the correct type
|
||||
return ChatInvokeCompletion(
|
||||
completion=parsed_object, # type: ignore
|
||||
usage=usage,
|
||||
)
|
||||
except AttributeError:
|
||||
# Fall back to manual parsing if with_structured_output is not available
|
||||
response = await self.chat.ainvoke(langchain_messages) # type: ignore
|
||||
|
||||
if not isinstance(response, 'LangChainAIMessage'):
|
||||
raise ModelProviderError(
|
||||
message=f'Response is not an AIMessage: {type(response)}',
|
||||
model=self.name,
|
||||
)
|
||||
|
||||
content = response.content if hasattr(response, 'content') else str(response)
|
||||
|
||||
try:
|
||||
if isinstance(content, str):
|
||||
import json
|
||||
|
||||
parsed_data = json.loads(content)
|
||||
if isinstance(parsed_data, dict):
|
||||
parsed_object = output_format(**parsed_data)
|
||||
else:
|
||||
raise ValueError('Parsed JSON is not a dictionary')
|
||||
else:
|
||||
raise ValueError('Content is not a string and structured output not supported')
|
||||
except Exception as e:
|
||||
raise ModelProviderError(
|
||||
message=f'Failed to parse response as {output_format.__name__}: {e}',
|
||||
model=self.name,
|
||||
) from e
|
||||
|
||||
usage = self._get_usage(response)
|
||||
return ChatInvokeCompletion(
|
||||
completion=parsed_object,
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# Convert any LangChain errors to browser-use ModelProviderError
|
||||
raise ModelProviderError(
|
||||
message=f'LangChain model error: {str(e)}',
|
||||
model=self.name,
|
||||
) from e
|
||||
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
Example of using LangChain models with browser-use.
|
||||
|
||||
This example demonstrates how to:
|
||||
1. Wrap a LangChain model with ChatLangchain
|
||||
2. Use it with a browser-use Agent
|
||||
3. Run a simple web automation task
|
||||
|
||||
@file purpose: Example usage of LangChain integration with browser-use
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from langchain_openai import ChatOpenAI # pyright: ignore
|
||||
|
||||
from browser_use import Agent
|
||||
from examples.models.langchain.chat import ChatLangchain
|
||||
|
||||
|
||||
async def main():
|
||||
"""Basic example using ChatLangchain with OpenAI through LangChain."""
|
||||
|
||||
# Create a LangChain model (OpenAI)
|
||||
langchain_model = ChatOpenAI(
|
||||
model='gpt-4.1-mini',
|
||||
temperature=0.1,
|
||||
)
|
||||
|
||||
# Wrap it with ChatLangchain to make it compatible with browser-use
|
||||
llm = ChatLangchain(chat=langchain_model)
|
||||
|
||||
# Create a simple task
|
||||
task = "Go to google.com and search for 'browser automation with Python'"
|
||||
|
||||
# Create and run the agent
|
||||
agent = Agent(
|
||||
task=task,
|
||||
llm=llm,
|
||||
)
|
||||
|
||||
print(f'🚀 Starting task: {task}')
|
||||
print(f'🤖 Using model: {llm.name} (provider: {llm.provider})')
|
||||
|
||||
# Run the agent
|
||||
history = await agent.run()
|
||||
|
||||
print(f'✅ Task completed! Steps taken: {len(history.history)}')
|
||||
|
||||
# Print the final result if available
|
||||
if history.final_result():
|
||||
print(f'📋 Final result: {history.final_result()}')
|
||||
|
||||
return history
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print('🌐 Browser-use LangChain Integration Example')
|
||||
print('=' * 45)
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,149 @@
|
||||
import json
|
||||
from typing import overload
|
||||
|
||||
from langchain_core.messages import ( # pyright: ignore
|
||||
AIMessage,
|
||||
HumanMessage,
|
||||
SystemMessage,
|
||||
)
|
||||
from langchain_core.messages import ( # pyright: ignore
|
||||
ToolCall as LangChainToolCall,
|
||||
)
|
||||
from langchain_core.messages.base import BaseMessage as LangChainBaseMessage # pyright: ignore
|
||||
|
||||
from browser_use.llm.messages import (
|
||||
AssistantMessage,
|
||||
BaseMessage,
|
||||
ContentPartImageParam,
|
||||
ContentPartRefusalParam,
|
||||
ContentPartTextParam,
|
||||
ToolCall,
|
||||
UserMessage,
|
||||
)
|
||||
from browser_use.llm.messages import (
|
||||
SystemMessage as BrowserUseSystemMessage,
|
||||
)
|
||||
|
||||
|
||||
class LangChainMessageSerializer:
|
||||
"""Serializer for converting between browser-use message types and LangChain message types."""
|
||||
|
||||
@staticmethod
|
||||
def _serialize_user_content(
|
||||
content: str | list[ContentPartTextParam | ContentPartImageParam],
|
||||
) -> str | list[str | dict]:
|
||||
"""Convert user message content for LangChain compatibility."""
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
|
||||
serialized_parts = []
|
||||
for part in content:
|
||||
if part.type == 'text':
|
||||
serialized_parts.append(
|
||||
{
|
||||
'type': 'text',
|
||||
'text': part.text,
|
||||
}
|
||||
)
|
||||
elif part.type == 'image_url':
|
||||
# LangChain format for images
|
||||
serialized_parts.append(
|
||||
{'type': 'image_url', 'image_url': {'url': part.image_url.url, 'detail': part.image_url.detail}}
|
||||
)
|
||||
|
||||
return serialized_parts
|
||||
|
||||
@staticmethod
|
||||
def _serialize_system_content(
|
||||
content: str | list[ContentPartTextParam],
|
||||
) -> str:
|
||||
"""Convert system message content to text string for LangChain compatibility."""
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
|
||||
text_parts = []
|
||||
for part in content:
|
||||
if part.type == 'text':
|
||||
text_parts.append(part.text)
|
||||
|
||||
return '\n'.join(text_parts)
|
||||
|
||||
@staticmethod
|
||||
def _serialize_assistant_content(
|
||||
content: str | list[ContentPartTextParam | ContentPartRefusalParam] | None,
|
||||
) -> str:
|
||||
"""Convert assistant message content to text string for LangChain compatibility."""
|
||||
if content is None:
|
||||
return ''
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
|
||||
text_parts = []
|
||||
for part in content:
|
||||
if part.type == 'text':
|
||||
text_parts.append(part.text)
|
||||
# elif part.type == 'refusal':
|
||||
# # Include refusal content as text
|
||||
# text_parts.append(f'[Refusal: {part.refusal}]')
|
||||
|
||||
return '\n'.join(text_parts)
|
||||
|
||||
@staticmethod
|
||||
def _serialize_tool_call(tool_call: ToolCall) -> LangChainToolCall:
|
||||
"""Convert browser-use ToolCall to LangChain ToolCall."""
|
||||
# Parse the arguments string to a dict for LangChain
|
||||
try:
|
||||
args_dict = json.loads(tool_call.function.arguments)
|
||||
except json.JSONDecodeError:
|
||||
# If parsing fails, wrap in a dict
|
||||
args_dict = {'arguments': tool_call.function.arguments}
|
||||
|
||||
return LangChainToolCall(
|
||||
name=tool_call.function.name,
|
||||
args=args_dict,
|
||||
id=tool_call.id,
|
||||
)
|
||||
|
||||
# region - Serialize overloads
|
||||
@overload
|
||||
@staticmethod
|
||||
def serialize(message: UserMessage) -> HumanMessage: ...
|
||||
|
||||
@overload
|
||||
@staticmethod
|
||||
def serialize(message: BrowserUseSystemMessage) -> SystemMessage: ...
|
||||
|
||||
@overload
|
||||
@staticmethod
|
||||
def serialize(message: AssistantMessage) -> AIMessage: ...
|
||||
|
||||
@staticmethod
|
||||
def serialize(message: BaseMessage) -> LangChainBaseMessage:
|
||||
"""Serialize a browser-use message to a LangChain message."""
|
||||
|
||||
if isinstance(message, UserMessage):
|
||||
content = LangChainMessageSerializer._serialize_user_content(message.content)
|
||||
return HumanMessage(content=content, name=message.name)
|
||||
|
||||
elif isinstance(message, BrowserUseSystemMessage):
|
||||
content = LangChainMessageSerializer._serialize_system_content(message.content)
|
||||
return SystemMessage(content=content, name=message.name)
|
||||
|
||||
elif isinstance(message, AssistantMessage):
|
||||
# Handle content
|
||||
content = LangChainMessageSerializer._serialize_assistant_content(message.content)
|
||||
|
||||
# For simplicity, we'll ignore tool calls in LangChain integration
|
||||
# as requested by the user
|
||||
return AIMessage(
|
||||
content=content,
|
||||
name=message.name,
|
||||
)
|
||||
|
||||
else:
|
||||
raise ValueError(f'Unknown message type: {type(message)}')
|
||||
|
||||
@staticmethod
|
||||
def serialize_messages(messages: list[BaseMessage]) -> list[LangChainBaseMessage]:
|
||||
"""Serialize a list of browser-use messages to LangChain messages."""
|
||||
return [LangChainMessageSerializer.serialize(m) for m in messages]
|
||||
@@ -0,0 +1,6 @@
|
||||
from browser_use import Agent, models
|
||||
|
||||
# available providers for this import style: openai, azure, google
|
||||
agent = Agent(task='Find founders of browser-use', llm=models.azure_gpt_4_1_mini)
|
||||
|
||||
agent.run_sync()
|
||||
@@ -0,0 +1,39 @@
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
from browser_use import Agent
|
||||
from browser_use.llm import ChatGroq
|
||||
|
||||
groq_api_key = os.environ.get('GROQ_API_KEY')
|
||||
llm = ChatGroq(
|
||||
model='meta-llama/llama-4-maverick-17b-128e-instruct',
|
||||
# temperature=0.1,
|
||||
)
|
||||
|
||||
# llm = ChatGroq(
|
||||
# model='meta-llama/llama-4-maverick-17b-128e-instruct',
|
||||
# api_key=os.environ.get('GROQ_API_KEY'),
|
||||
# temperature=0.0,
|
||||
# )
|
||||
|
||||
task = 'Go to amazon.com, search for laptop, sort by best rating, and give me the price of the first result'
|
||||
|
||||
|
||||
async def main():
|
||||
agent = Agent(
|
||||
task=task,
|
||||
llm=llm,
|
||||
)
|
||||
await agent.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
Simple agent run with Mistral.
|
||||
|
||||
You need to set MISTRAL_API_KEY in your environment (and optionally MISTRAL_BASE_URL).
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from browser_use import Agent
|
||||
from browser_use.llm.mistral import ChatMistral
|
||||
|
||||
load_dotenv()
|
||||
|
||||
llm = ChatMistral(model='mistral-small-2506', temperature=0.6)
|
||||
agent = Agent(
|
||||
llm=llm,
|
||||
task='List two fun weekend activities in Barcelona.',
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
await agent.run(max_steps=10)
|
||||
input('Press Enter to continue...')
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,34 @@
|
||||
"""
|
||||
Simple try of the agent.
|
||||
|
||||
@dev You need to add MODELSCOPE_API_KEY to your environment variables.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from browser_use import Agent, ChatOpenAI
|
||||
|
||||
# dotenv
|
||||
load_dotenv()
|
||||
|
||||
api_key = os.getenv('MODELSCOPE_API_KEY', '')
|
||||
if not api_key:
|
||||
raise ValueError('MODELSCOPE_API_KEY is not set')
|
||||
|
||||
|
||||
async def run_search():
|
||||
agent = Agent(
|
||||
# task=('go to amazon.com, search for laptop'),
|
||||
task=('go to google, search for modelscope'),
|
||||
llm=ChatOpenAI(base_url='https://api-inference.modelscope.cn/v1/', model='Qwen/Qwen2.5-VL-72B-Instruct', api_key=api_key),
|
||||
use_vision=False,
|
||||
)
|
||||
|
||||
await agent.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(run_search())
|
||||
@@ -0,0 +1,38 @@
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from browser_use import Agent, ChatOpenAI
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Get API key from environment variable
|
||||
api_key = os.getenv('MOONSHOT_API_KEY')
|
||||
if api_key is None:
|
||||
print('Make sure you have MOONSHOT_API_KEY set in your .env file')
|
||||
print('Get your API key from https://platform.moonshot.ai/console/api-keys ')
|
||||
exit(1)
|
||||
|
||||
# Configure Moonshot AI model
|
||||
llm = ChatOpenAI(
|
||||
model='kimi-k2-thinking',
|
||||
base_url='https://api.moonshot.ai/v1',
|
||||
api_key=api_key,
|
||||
add_schema_to_system_prompt=True,
|
||||
remove_min_items_from_schema=True, # Moonshot doesn't support minItems in JSON schema
|
||||
remove_defaults_from_schema=True, # Moonshot doesn't allow default values with anyOf
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
agent = Agent(
|
||||
task='Search for the latest news about AI and summarize the top 3 articles',
|
||||
llm=llm,
|
||||
flash_mode=True,
|
||||
)
|
||||
await agent.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
Simple try of the agent.
|
||||
|
||||
@dev You need to add NOVITA_API_KEY to your environment variables.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
from browser_use import Agent, ChatOpenAI
|
||||
|
||||
api_key = os.getenv('NOVITA_API_KEY', '')
|
||||
if not api_key:
|
||||
raise ValueError('NOVITA_API_KEY is not set')
|
||||
|
||||
|
||||
async def run_search():
|
||||
agent = Agent(
|
||||
task=(
|
||||
'1. Go to https://www.reddit.com/r/LocalLLaMA '
|
||||
"2. Search for 'browser use' in the search bar"
|
||||
'3. Click on first result'
|
||||
'4. Return the first comment'
|
||||
),
|
||||
llm=ChatOpenAI(
|
||||
base_url='https://api.novita.ai/v3/openai',
|
||||
model='deepseek/deepseek-v3-0324',
|
||||
api_key=api_key,
|
||||
),
|
||||
use_vision=False,
|
||||
)
|
||||
|
||||
await agent.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(run_search())
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user