Autonomous Email AI Agent
Monitors → Classifies → Researches → Acts — with human-in-the-loop approval over WhatsApp
MailMind is a daemon that sits behind your Gmail inbox and acts on every new email automatically:
| Email Type | MailMind Action |
|---|---|
| Interview invite | Checks calendar → drafts reply → researches company + role → writes prep plan into Calendar event; if a conflict is detected the Calendar event is skipped and the reply mentions a suggested reschedule time |
| Assessment / task | Creates a Trello card → researches the topic → writes action plan into the card; relative deadlines ("3 days", "in 2 days") are resolved to exact dates |
| Low-confidence / ambiguous | Sends you a WhatsApp approval request |
| Anything else | Ignores it |
Everything above a 0.9 confidence threshold happens without you touching anything. Between 0.6 – 0.9 you get a WhatsApp ping. Below 0.6 it's silently rejected.
flowchart TD
A([Gmail Inbox]) -->|IMAP IDLE| B[imap_listener.py\n120s heartbeat]
B -->|new UID| C[EmailFetcher\nMIME → EmailDTO]
C -->|Celery task| D[(PostgreSQL\nidempotent claim)]
D -->|claimed| E[LangGraph Workflow]
E --> F[ClassifierAgent\nrule-based + Groq fallback]
F --> G[ExtractionAgent\nLLM + Calendar conflict check]
G --> H{DecisionMaker\nthreshold logic}
H -->|confidence ≥ 0.9\nAUTO_EXECUTE| I[PlanningAgent\nLLM → plan_type]
H -->|0.6 ≤ conf < 0.9\nREVIEW| J[WhatsAppNode\nTwilio notify + Redis state]
H -->|confidence < 0.6\nREJECT| K([Ignored])
I --> L{plan_type?}
L -->|SET_REMINDER\nUPDATE_TASK| M[AutoReplyNode\nSMTP]
L -->|REQUEST_REVIEW| J
M -->|reply sent| N[ResearchNode\nTavily search + Groq plan]
N -->|SET_REMINDER| O[RemainderNode\nCalendar + prep plan]
N -->|UPDATE_TASK| P[TaskNode\nTrello + action plan]
M & O & P -->|action_failed=True| Q[FailureNode\nlog + WhatsApp escalation]
J -->|APPROVE id| R[WhatsApp Webhook\nFastAPI POST /whatsapp_hook]
R -->|deserialize Redis state\nset AUTO_EXECUTE| I
J -->|REJECT id| K
sequenceDiagram
participant G as Gmail
participant W as MailMind Worker
participant R as Redis
participant T as Twilio WhatsApp
participant U as You
participant F as FastAPI Webhook
G->>W: New email arrives (IMAP IDLE)
W->>W: Classify + Extract + DecisionMaker → REVIEW
W->>R: Save WorkflowState (key: state:{ref_id}, TTL 24h)
W->>T: Send approval message with APPROVE/REJECT {ref_id}
T->>U: WhatsApp notification
U->>T: Reply "APPROVE abc123"
T->>F: POST /whatsapp_hook
F->>R: Load WorkflowState
F->>W: Celery: whatsapp_processing_task
W->>W: Set decision=AUTO_EXECUTE, re-enter at Planning
W->>W: AutoReply → Research → Calendar / Trello
After sending the auto-reply, ResearchNode runs Tavily web searches and feeds the results to the LLM to generate an actionable plan. The plan is written directly into the Calendar event description or Trello card, so it's waiting for you when you open it.
| Email type | Tavily queries | Plan sections |
|---|---|---|
| Interview | {company} {role} interview questions · {company} interview process culture |
Company Overview · Likely Topics · Preparation Steps · Useful Resources |
| Task / Assessment | {task} tutorial guide how to · {role} assessment best practices |
Task Overview · Recommended Approach · Key Resources · Time Breakdown |
Calendar event description (Interview):
Source Email: ...
From: ...
Role: ...
Company: ...
Meeting Link: ...
---
## Preparation Plan
## Company Overview
Acme Corp is a Series B fintech startup focused on...
## Likely Interview Topics
- System design (distributed payments)
- ...
## Preparation Steps
1. Review Stripe and Plaid API patterns
...
## Useful Resources
- [Acme Engineering Blog](https://acme.com/blog)
- ...
Trello card description (Task):
Source Email: ...
Priority: high
Description: Build a REST API for user auth...
---
## Action Plan
## Task Overview
Implement JWT-based auth with refresh token rotation...
## Recommended Approach
1. Set up project scaffolding
...
flowchart LR
subgraph ClassifierAgent
A[Email text] --> B{Rule-based\nregex score}
B -->|confidence ≥ 0.7| C[Result]
B -->|confidence < 0.7| D[Groq LLM\nllama-4-scout]
D --> C
end
subgraph DecisionMaker
C --> E{confidence?}
E -->|≥ 0.9| F[AUTO_EXECUTE]
E -->|0.6 – 0.9| G[REVIEW]
E -->|< 0.6| H[REJECT]
end
subgraph PlanningAgent
F --> I{LLM reasoning\nor rule fallback}
I -->|interview| J[SET_REMINDER]
I -->|assessment| K[UPDATE_TASK]
I -->|task/reminder| L[SET_REMINDER]
I -->|ambiguous| M[REQUEST_REVIEW]
end
- Python 3.11+
- PostgreSQL — database
email_agent_db - Redis —
localhost:6379 - Google OAuth2
token.jsonwithcalendar.eventsscope (pre-generate via OAuth2 flow) - Tavily API key — free tier at tavily.com
pip install -r requirements.txtCREATE TABLE email_agent_table (
msgid VARCHAR PRIMARY KEY,
status VARCHAR,
updated_at TIMESTAMP
);# ── Email ──────────────────────────────────────────
EMAIL_USER_NAME=you@gmail.com
EMAIL_PASSWORD=your_app_password # Gmail App Password, not your login password
IMAP_SERVER=imap.gmail.com
# ── LLM (Groq) ─────────────────────────────────────
GROQ_API_KEY=gsk_...
# ── Web Search (Tavily) ─────────────────────────────
TAVILY_API_KEY=tvly-... # get free key at tavily.com
# ── WhatsApp via Twilio ─────────────────────────────
TWILIO_ACCOUNT_SID=AC...
TWILIO_AUTH_TOKEN=...
TWILIO_PHONE_NUMBER=+14155238886
TARGET_PHONE_NUMBER=+91... # your WhatsApp number
# ── Trello ─────────────────────────────────────────
TRELLO_API_KEY=...
TRELLO_API_TOKEN=ATTA...
TRELLO_LIST_ID=... # target list ID for new cards
# ── Google Calendar ─────────────────────────────────
TOKEN_PATH=token.json # path to your OAuth2 token
# ── PostgreSQL ──────────────────────────────────────
DB_HOST=localhost
DB_NAME=email_agent_db
DB_USER=postgres
DB_PASSWORD=...
# ── Celery / Redis ──────────────────────────────────
CELERY_BROKER_URL=redis://localhost:6379/1
# ── Decision thresholds ─────────────────────────────
AUTO_EXECUTE_THRESHOLD=0.9
REVIEW_THRESHOLD=0.6Open three terminals:
# Terminal 1 — IMAP listener daemon
python main.py
# Terminal 2 — Celery async worker
celery -A core.task_scheduler worker --loglevel=info
# Terminal 3 — FastAPI webhook (WhatsApp approvals)
uvicorn core.task_scheduler:app --host 0.0.0.0 --port 8000WorkflowState is the single TypedDict that flows through every node:
WorkflowState
├── email_data EmailDTO raw parsed email
├── classification ClassificationResult category, priority, confidence
├── extracted_data ExtractionResult meeting_at, deadline, company, role, suggested_reply
├── decision DecisionType AUTO_EXECUTE | REVIEW | REJECT
├── plan_type PlanningEvent which action to take
├── event_type str EMAIL | APPROVED
├── research_result str | None markdown action plan from ResearchNode
├── calendar_conflict dict {has_conflict, alternate_time}
├── action_failed bool set by action nodes on error
└── execution_result str human-readable outcome
| Node | Module | Type | Responsibility |
|---|---|---|---|
ClassifierAgent |
nodes/classifer_agent.py |
LLM (Groq) | Hybrid regex + LLM classification; lazy LLM init |
ExtractionAgent |
nodes/extraction_agent.py |
LLM (Groq) | Structured extraction from subject + body; Google Calendar conflict check; resolves relative day counts to exact ISO dates |
DecisionMakerNode |
nodes/decision_maker_node.py |
Rule-based | Confidence thresholds → decision enum |
PlanningNode |
nodes/planning_node.py |
LLM (Groq) | LLM plan selection with rule-based fallback |
AutoReplyNode |
nodes/auto_reply_node.py |
Stateless | SMTP reply via tools/mail_tool.py |
ResearchNode |
nodes/research_node.py |
LLM (Groq) + Tavily | Web search + LLM synthesis → markdown action plan |
TaskNode |
nodes/task_node.py |
Stateless | Trello card creation with research plan; due date set from extracted deadline (relative day counts pre-resolved); duplicate + 5/day guard |
RemainderNode |
nodes/remainder_node.py |
Stateless | Google Calendar event with prep plan; skips event creation if a calendar conflict was detected, avoiding double-booking |
WhatsAppNode |
nodes/whatsapp_node.py |
Stateless | Twilio message + Redis state serialization (24h TTL) |
FailureNode |
nodes/failure_node.py |
Stateless | Log failure + escalate via WhatsAppNode |
| Tool | File | Wraps |
|---|---|---|
| Calendar | tools/calendar_tool.py |
Google Calendar API — conflict check + event creation |
| Trello | tools/trello_tool.py |
Trello REST API — list cards + create card |
tools/mail_tool.py |
Gmail SMTP — send reply | |
| Tavily | tools/tavily_tool.py |
Tavily Search API — web research for planning |
class DataClassifier(str, Enum):
assessment = "assessment"
interview = "interview"
task = "task"
not_classified = "not_classified"
class DecisionType(str, Enum):
AUTO_EXECUTE = "AUTO_EXECUTE"
REVIEW = "REVIEW"
REJECT = "REJECT"
class PlanningEvent(str, Enum):
UPDATE_TASK = "UPDATE_TASK"
SET_REMINDER = "SET_REMINDER"
REQUEST_REVIEW = "REQUEST_REVIEW"
IGNORE = "IGNORE"WhatsAppNode sends category-aware approval messages:
INTERVIEW
🔔 *Interview Approval Required*
📩 From: {sender}
📌 Subject: {subject}
🏢 Company: {company}
🎯 Role: {role}
📅 Meeting: {meeting_time}
🌍 Timezone: {timezone}
🚦 Priority: {priority}
🔁 Alternate Time: {alternate_time} ← only if calendar conflict
ASSESSMENT
🔔 *Assessment Review Required*
📩 From: {sender}
📌 Subject: {subject}
🏢 Company: {company}
📋 Task: {role}
⏰ Deadline: {deadline}
📏 Estimated Time: {time_estimate}
🚦 Priority: {priority}
TASK
🔔 *Task Review Required*
📩 From: {sender}
📌 Subject: {subject}
🏢 Company: {company}
✓ Task: {role}
⏳ Due: {deadline}
🔗 Dependencies: {dependencies}
🚦 Priority: {priority}
Reply to approve or reject:
APPROVE abc123
REJECT abc123
graph LR
subgraph Local
A[main.py\nIMAP listener] -->|Celery task| B[Redis\nlocalhost:6379]
C[Celery worker] -->|reads| B
D[FastAPI :8000] -->|reads/writes| B
C -->|status| E[(PostgreSQL\nemail_agent_db)]
end
subgraph External APIs
C -->|SMTP| F[Gmail]
C -->|REST| G[Trello]
C -->|OAuth2| H[Google Calendar]
C & D -->|Twilio SDK| I[WhatsApp]
C -->|Groq SDK| J[Groq LLM\nllama-4-scout]
C -->|REST| K[Tavily Search]
end
| Key pattern | TTL | Purpose |
|---|---|---|
state:{ref_id} |
24 h | Serialized WorkflowState awaiting human review |
| (Celery internals) | — | Task queue on db=1 |
mailmind/
├── main.py Entry point — starts IMAP IDLE listener
│
├── core/
│ ├── imap_listener.py IMAP IDLE loop, 120s heartbeat, dispatches Celery tasks
│ ├── imap_connector.py IMAP connection factory
│ ├── email_fetcher.py MIME parser → EmailDTO
│ ├── task_scheduler.py Celery app + FastAPI webhook + task definitions
│ └── database.py PostgreSQL — idempotent INSERT … ON CONFLICT DO NOTHING
│
├── nodes/
│ ├── classifer_agent.py ClassifierAgent (LLM-backed)
│ ├── extraction_agent.py ExtractionAgent (LLM-backed)
│ ├── decision_maker_node.py DecisionMakerNode (rule-based)
│ ├── planning_node.py PlanningNode (LLM-backed)
│ ├── auto_reply_node.py AutoReplyNode
│ ├── research_node.py ResearchNode (Tavily search + Groq plan synthesis)
│ ├── task_node.py TaskNode
│ ├── remainder_node.py RemainderNode
│ ├── whatsapp_node.py WhatsAppNode
│ ├── failure_node.py FailureNode
│ └── constant.py Regex patterns (PHRASE, PRIORITY, NEGATION)
│
├── tools/
│ ├── calendar_tool.py Google Calendar API wrapper
│ ├── trello_tool.py Trello API wrapper
│ ├── mail_tool.py Gmail SMTP wrapper
│ └── tavily_tool.py Tavily Search API wrapper
│
├── schema/
│ ├── email_dto.py EmailDTO
│ ├── DataClassifer.py DataClassifier enum
│ ├── DecisionType.py DecisionType enum
│ ├── planning_type.py PlanningEvent enum
│ ├── mail_extractor.py ExtractionResult
│ ├── OutputClassifer.py ClassificationResult
│ └── PriorityClassifier.py PriorityClassifier enum
│
├── workflow/
│ ├── graph.py LangGraph StateGraph — nodes + conditional edges
│ └── state.py WorkflowState TypedDict
│
└── config/
└── settings.py Pydantic BaseSettings — loads all .env keys
All inference uses Groq with meta-llama/llama-4-scout-17b-16e-instruct.
| Agent | When LLM is called | Fallback |
|---|---|---|
ClassifierAgent |
Rule-based confidence < 0.7 | — (rule result used directly above threshold) |
ExtractionAgent |
Always | None — required |
PlanningAgent |
AUTO_EXECUTE decisions only |
Rule-based map of category → plan_type |
ResearchNode |
After AutoReply, before Task/Reminder | Skipped gracefully if Tavily key missing |
LLM clients are instantiated lazily via a @property on each agent and reused across calls.
Note:
ResearchNodeusestemperature=0.3(slightly creative) while classification and extraction nodes usetemperature=0(deterministic) for consistency.