An intelligent email assistant that learns your preferences over time, triages your inbox, drafts responses, and schedules meetingsβall with human-in-the-loop oversight.
Attach a video demo or GIF here showcasing the LangGraph workflow in action.
The demo should illustrate the complete email processing flow: triage classification, human review via Agent Inbox, email response drafting, and how the system learns from user feedback to improve future interactions.
Managing email is a time sink. This project tackles that problem by building a production-grade email assistant using LangGraphβa framework for building stateful, multi-actor AI applications with cycles, controllability, and persistence.
Traditional LLM chains are linear and stateless. Email management, however, requires:
- Stateful workflows: Remembering conversation context across multiple interactions
- Conditional routing: Different emails need different handling (ignore, notify, respond)
- Human-in-the-loop control: Critical actions like sending emails require human approval
- Persistent memory: Learning user preferences over time to improve triage and response quality
- Interruptible execution: Pausing workflows for human review and resuming with feedback
LangGraph provides all of these capabilities through its graph-based architecture, making it the ideal choice for this use case.
- Multi-stage triage with adaptive learning: The system classifies emails into three categories and learns from corrections
- Nested graph composition: A response agent subgraph handles the actual email drafting
- Bidirectional HITL: Humans can approve, edit, provide feedback, or ignore at multiple decision points
- Memory profiles: Three separate memory stores (triage, response, calendar) that evolve with usage
- Real Gmail integration: Not just a demoβconnects to actual Gmail/Google Calendar APIs
- Production deployment: Docker support, cron jobs, and LangGraph Platform deployment
| Feature | Description |
|---|---|
| Smart Email Triage | Classifies incoming emails as ignore, notify, or respond based on configurable rules |
| Human-in-the-Loop | All critical actions (sending emails, scheduling meetings) require human approval |
| Adaptive Memory | Learns from user feedback to improve triage decisions and response quality over time |
| Gmail Integration | Full integration with Gmail API for fetching, sending, and marking emails as read |
| Calendar Management | Checks availability and schedules meetings via Google Calendar API |
| Agent Inbox Compatible | Works with Agent Inbox for a polished human review experience |
| Automated Ingestion | Cron jobs for continuous email processing on hosted deployments |
| Stateful Execution | Checkpointing enables pause/resume workflows across sessions |
- StateGraph: Typed state management with Pydantic schemas
- Command-based routing: Dynamic edge traversal with
Commandobjects - Interrupts:
interrupt()for human-in-the-loop checkpoints - Subgraph composition: Response agent as a nested, reusable graph
- BaseStore integration: Persistent memory with namespace-based organization
- Conditional edges: Runtime routing based on state and LLM decisions
The email assistant is built as a hierarchical graph with two main components:
| Component | Responsibility |
|---|---|
| Triage Router | Classifies emails using an LLM with structured output (RouterSchema) |
| Triage Interrupt Handler | Presents notifications to humans, handles feedback for reclassification |
| Response Agent | Drafts emails, checks calendars, schedules meetings using bound tools |
| Interrupt Handler | HITL checkpoint for send_email, schedule_meeting, and Question tools |
| Memory System | Three namespaced stores for triage, response, and calendar preferences |
State: {
email_input: dict # Raw email data from Gmail
messages: List[Message] # Conversation history with LLM
classification_decision: # "ignore" | "notify" | "respond"
}
State flows through the graph, accumulating messages and decisions. The Command pattern enables dynamic routing based on classification results and human feedback.
| Category | Technology |
|---|---|
| Agent Framework | LangGraph 1.0+ |
| LLM Integration | LangChain with OpenAI/Gemini via OpenRouter |
| Schema Validation | Pydantic v2 |
| Email API | Gmail API (google-api-python-client) |
| Calendar API | Google Calendar API |
| Testing | Pytest with LangSmith integration |
| Deployment | Docker, LangGraph Platform |
| Human Review | Agent Inbox |
| Package Management | uv / pip with pyproject.toml |
Email-Agent-Using-Langgraph/
βββ src/
β βββ email_assistant/
β βββ __init__.py
β βββ email_assistant.py # Basic agent workflow
β βββ email_assistant_hitl.py # + Human-in-the-loop
β βββ email_assistant_hitl_memory.py # + Memory/learning
β βββ email_assistant_hitl_memory_gmail.py # + Gmail integration (main)
β βββ prompts.py # System prompts and templates
β βββ schemas.py # Pydantic models (State, RouterSchema)
β βββ configuration.py # Runtime configuration
β βββ cron.py # Scheduled ingestion graph
β βββ utils.py # Helper functions
β βββ eval/
β β βββ email_dataset.py # Test email corpus with ground truth
β β βββ prompts.py # Evaluation prompts
β βββ tools/
β βββ __init__.py
β βββ base.py # Tool registry and loading
β βββ default/
β β βββ email_tools.py # write_email, Done, Question
β β βββ calendar_tools.py # schedule_meeting, check_calendar
β β βββ prompt_templates.py # Tool descriptions for prompts
β βββ gmail/
β βββ gmail_tools.py # Gmail API wrappers
β βββ setup_gmail.py # OAuth setup script
β βββ run_ingest.py # Email ingestion CLI
β βββ setup_cron.py # Cron job configuration
β βββ README.md # Gmail-specific docs
βββ tests/
β βββ conftest.py # Pytest fixtures
β βββ test_response.py # Response quality tests
β βββ run_all_tests.py # Test runner
βββ pyproject.toml # Dependencies and metadata
βββ langgraph.json # LangGraph deployment config
βββ Dockerfile # Container definition
βββ README.md
| Directory | Purpose |
|---|---|
src/email_assistant/ |
Core agent implementation with progressive feature additions |
src/email_assistant/tools/ |
Modular tool implementations (default mock + Gmail API) |
src/email_assistant/eval/ |
Evaluation dataset with 16 diverse email scenarios |
tests/ |
Pytest-based test suite with LangSmith integration |
- Python 3.13+
- A Gmail account (for full functionality)
- OpenRouter API key or OpenAI API key
git clone https://github.com/yourusername/Email-Agent-Using-Langgraph.git
cd Email-Agent-Using-Langgraph
# Using uv (recommended)
uv sync
# Or using pip
pip install -e .Create a .env file in the project root:
# LLM Provider (OpenRouter)
OPENROUTER_PERSONAL_BILLED_KEY=your_openrouter_key
# Or use OpenAI directly
OPENAI_API_KEY=your_openai_key
# LangSmith (optional, for tracing)
LANGCHAIN_API_KEY=your_langsmith_key
LANGCHAIN_TRACING_V2=true
LANGCHAIN_PROJECT=email-assistantFor real Gmail functionality, follow the detailed setup in src/email_assistant/tools/gmail/README.md:
# 1. Create secrets directory
mkdir -p src/email_assistant/tools/gmail/.secrets
# 2. Download OAuth credentials from Google Cloud Console
# Save as: src/email_assistant/tools/gmail/.secrets/secrets.json
# 3. Run OAuth flow
python src/email_assistant/tools/gmail/setup_gmail.py# Start the development server
langgraph dev
# In another terminal, ingest emails
python src/email_assistant/tools/gmail/run_ingest.py \
--email your@email.com \
--minutes-since 1000Open Agent Inbox and connect:
- Deployment URL:
http://127.0.0.1:2024 - Graph ID:
email_assistant_hitl_memory_gmail
The workflow consists of these key nodes:
def triage_router(state: State, store: BaseStore) -> Command[...]:
"""Classifies emails into ignore/notify/respond using structured LLM output."""
# Fetches triage preferences from memory store
# Uses RouterSchema for structured classification
# Returns Command with goto and state updatedef triage_interrupt_handler(state: State, store: BaseStore) -> Command[...]:
"""HITL checkpoint for 'notify' classifications."""
# Calls interrupt() to pause execution
# Waits for human response via Agent Inbox
# Updates memory based on human decisiondef llm_call(state: State, store: BaseStore):
"""Invokes LLM with tools to generate responses."""
# Fetches calendar and response preferences from memory
# Uses tool_choice="required" to enforce tool usagedef interrupt_handler(state: State, store: BaseStore) -> Command[...]:
"""HITL for send_email, schedule_meeting, and Question tools."""
# Different handling for each tool type
# Supports: accept, edit, ignore, respond actions
# Updates memory when user edits or provides feedbackThe graph uses both static and conditional edges:
# Static edges
agent_builder.add_edge(START, "llm_call")
agent_builder.add_edge("mark_as_read_node", END)
# Conditional edges with Command pattern
def should_continue(state: State, store: BaseStore) -> Literal[...]:
"""Routes based on tool calls in last message."""
if tool_call["name"] == "Done":
return "mark_as_read_node"
else:
return "interrupt_handler"State is managed through MessagesState (built-in message accumulation) extended with custom fields:
class State(MessagesState):
email_input: dict # Raw email
classification_decision: Literal["ignore", "respond", "notify"]- Triage Decision: LLM classifies β routes to different branches
- HITL at Notify: Human decides to respond or ignore
- Tool Execution: Non-HITL tools execute immediately
- HITL at Critical Tools: send_email, schedule_meeting, Question pause for approval
- Memory Updates: User edits/feedback update relevant memory profiles
Input Email:
From: Sarah Johnson <sarah.j@partner.com>
Subject: Joint presentation next month
Body: Could we schedule 60 minutes to collaborate on slides?
Step-by-Step Execution:
-
Triage Router
- Fetches triage preferences from memory
- LLM classifies as
respond(direct question requiring action) - Routes to
response_agent
-
LLM Call (Response Agent)
- Fetches calendar preferences (30-min meetings preferred)
- Fetches response preferences (verify calendar, propose times)
- LLM calls
check_calendar_toolwith dates
-
Tool Execution (No HITL)
- Calendar check executes immediately
- Returns: "Available: 10:00 AM - 12:00 PM, 2:00 PM - 4:00 PM"
-
LLM Call (Second Pass)
- LLM calls
schedule_meeting_toolwith proposed time - Routes to
interrupt_handler
- LLM calls
-
Interrupt Handler (HITL)
- Pauses execution, sends to Agent Inbox
- Human reviews meeting details
- Human edits duration from 30 to 60 minutes
- Memory updated with new calendar preference
-
Tool Execution
- Meeting scheduled with edited parameters
- LLM drafts confirmation email
-
Interrupt Handler (HITL for Email)
- Human approves email draft
- Email sent via Gmail API
-
Mark as Read
- Original email marked as read in Gmail
Different aspects of email handling require different learning:
- Triage Preferences: Which senders/subjects to prioritize
- Response Preferences: Tone, structure, acknowledgment patterns
- Calendar Preferences: Meeting duration, time-of-day preferences
Separating these prevents unrelated feedback from corrupting other preferences.
Command objects provide:
- Atomic state updates with routing decisions
- Cleaner code than separate edge functions
- Type-safe routing with
Literaltypes
The response agent is reusable:
- Called from both triage_router and triage_interrupt_handler
- Encapsulates the LLM loop with HITL
- Could be swapped for different response strategies
- Access to multiple model providers (Google, Anthropic, OpenAI)
- Easy model switching for cost/performance optimization
- Provider fallback capabilities
This architecture pattern applies beyond email:
| Use Case | Application |
|---|---|
| Customer Support | Triage tickets, draft responses, escalate when needed |
| Legal Document Review | Classify documents, extract key terms, flag issues |
| Sales Pipeline | Qualify leads, draft outreach, schedule demos |
| Content Moderation | Classify content, take action, learn from corrections |
| IT Helpdesk | Categorize requests, suggest solutions, route to specialists |
The core patternβtriage β decide β act with oversight β learnβis universal for any domain where AI assists human decision-making.
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Write tests for your changes
- Ensure all tests pass (
pytest tests/) - Submit a Pull Request
For major changes, please open an issue first to discuss what you would like to change.
This project is open source. See the LICENSE file for details.
Built with LangGraph π¦π
Star this repo if you found it useful!

