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,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())
|
||||
Reference in New Issue
Block a user