Python email agent that triages unread Gmail messages, applies productivity labels, and creates Gmail reply drafts.
- Stack:
openai-agents-python+ Gmail API OAuth2 - Model backend: OpenAI API or any OpenAI-compatible endpoint (including Ollama)
- Safety: creates drafts only, never auto-sends
Each run (python -m app.main) does this:
- Reads inbox emails (unread-only by default; last 12 hours by default; can include read emails for backfill/testing).
- Triage agent decides:
IGNOREREPLYSUSPICIOUS
- Triage agent assigns one topic label:
Personal & DirectFinanceSales & OutreachEvents & CalendarNewslettersSecurity & AdminProfessional NetworkReceipts & BillingSaaS & Tools
- App applies exactly one topic label (exclusive among managed topic labels).
- If action is
REPLY, app also appliesAction Requiredas an overlay label. - For
REPLY, draft agent writes a short reply and saves it as a Gmail draft in the same thread. - Logs summary counts to stdout.
.
|- README.md
|- pyproject.toml
|- requirements.txt
|- .env.example
|- scripts/
| |- first_run_windows.ps1
| |- run_windows.ps1
| |- enable_scheduler_windows.ps1
| |- disable_scheduler_windows.ps1
| |- first_run_mac.sh
| |- run_mac.sh
| |- enable_scheduler_mac.sh
| |- disable_scheduler_mac.sh
| `- send_stress_test_emails.py
`- app/
|- __init__.py
|- agents.py
|- cleanup_labels.py
|- config.py
|- gmail_client.py
|- main.py
|- setup_wizard.py
|- tools.py
`- workflows.py
- Python 3.11+
- Google Cloud project with Gmail API enabled
- Gmail OAuth desktop client JSON (
credentials.json) - Model backend:
- OpenAI API key, or
- local Ollama endpoint
Use the first-run scripts. They handle venv, dependency install, and setup wizard automatically.
Windows:
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
.\scripts\first_run_windows.ps1macOS/Linux:
bash scripts/first_run_mac.shIf Python is not installed, the script prints a direct install link and exits cleanly.
Place OAuth client JSON in project root as credentials.json.
Google Cloud quick path:
- Open Google Cloud Console
- Create/select project
- Enable Gmail API
- Configure OAuth consent screen
- Create OAuth Client ID of type Desktop app
- Download JSON as
credentials.json
After first-run setup:
Windows:
.\scripts\run_windows.ps1macOS/Linux:
bash scripts/run_mac.shManual alternative (if preferred):
python -m app.mainFirst run opens browser consent and writes token.json.
ollama pull qwen3.5:9b
ollama serveSet in .env:
OPENAI_API_KEY="ollama"
OPENAI_BASE_URL="http://localhost:11434/v1"
OPENAI_MODEL_TRIAGE="qwen3.5:9b"
OPENAI_MODEL_DRAFT="qwen3.5:9b"
OPENAI_AGENTS_DISABLE_TRACING="true"If ollama serve says port 11434 is already in use, Ollama is already running. Just keep it running and execute the agent.
Use the included script to generate randomized synthetic inbox emails for classification testing. Every run produces different subjects/bodies/amounts/dates unless you pass a fixed seed.
python -m scripts.send_stress_test_emailsOptional overrides:
python -m scripts.send_stress_test_emails --to-email "you@example.com"
python -m scripts.send_stress_test_emails --prefix "[AI-STRESS-MYRUN]"
python -m scripts.send_stress_test_emails --count 25
python -m scripts.send_stress_test_emails --seed 42Recipient selection order:
--to-emailSTRESS_TEST_TO_EMAIL(env var)- Authenticated Gmail address from
users.getProfile(userId="me")
Notes:
- Emails are sent from the authenticated Gmail account (
userId="me"), so stress tests run from the user's side. - Default volume is 15 emails per run (configurable with
--countorSTRESS_TEST_COUNT).
Configure these in .env to match your Gmail labels:
LABEL_PERSONAL_DIRECTLABEL_FINANCELABEL_SALES_OUTREACHLABEL_EVENTS_CALENDARLABEL_NEWSLETTERSLABEL_SECURITY_ADMINLABEL_PROFESSIONAL_NETWORKLABEL_RECEIPTS_BILLINGLABEL_SAAS_TOOLSLABEL_ACTION_REQUIRED(overlay label forREPLYitems)
If your Gmail label names include emojis, the app resolves labels by normalized text so plain names still map correctly.
Dry run:
python -m app.cleanup_labels --dry-runDelete legacy labels:
python -m app.cleanup_labelsThis removes old AI/... and ORG/... labels from prior logic.
Core:
OPENAI_API_KEYOPENAI_BASE_URLOPENAI_MODEL_TRIAGEOPENAI_MODEL_DRAFTOPENAI_AGENTS_DISABLE_TRACING
Triage quality gates:
SUSPICIOUS_CONFIDENCE_THRESHOLD(0.0-1.0)SUSPICIOUS_MIN_SIGNALS(int >= 1)TRUSTED_SENDER_DOMAINS(CSV)TRUSTED_SENDER_EMAILS(CSV)
Gmail:
GOOGLE_CREDENTIALS_FILEGOOGLE_TOKEN_FILE
Runtime:
LOG_LEVELMAX_EMAILS_PER_RUNMAX_EMAIL_AGE_HOURS(default12; only process emails newer than this age, set0to disable)INCLUDE_READ_INBOX_EMAILS(falseby default; settruefor backfill/testing)INBOX_SUBJECT_CONTAINS(optional subject filter; useful with include-read mode)STRESS_TEST_TO_EMAIL(optional default recipient forscripts.send_stress_test_emails)STRESS_TEST_COUNT(optional default number of generated stress emails; default15)CATEGORY_LABELING_ENABLEDEXCLUDE_ALREADY_LABELED(setfalsewhen you intentionally want to reprocess already-labeled emails)
This app is run-per-execution. For always-on behavior, schedule it every few minutes.
Enable every 2 minutes:
.\scripts\enable_scheduler_windows.ps1 -EveryMinutes 2Disable:
.\scripts\disable_scheduler_windows.ps1Enable every 2 minutes:
bash scripts/enable_scheduler_mac.sh 2Disable:
bash scripts/disable_scheduler_mac.sh- Do not commit
.env,credentials.json, ortoken.json. - Keep
.env.examplewith placeholders only. - Every user should provide their own OAuth and model credentials.