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

This commit is contained in:
wehub-resource-sync
2026-07-13 12:02:32 +08:00
commit 4cd2d4af2b
475 changed files with 121829 additions and 0 deletions
+32
View File
@@ -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
+120
View File
@@ -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())
+38
View File
@@ -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())
+347
View File
@@ -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())
+113
View File
@@ -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())
+43
View File
@@ -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())