Project - AI Email Processing Agent (LangChain + Gmail + Express)
Project - AI Email Processing Agent (LangChain + Gmail + Express)
Hey everyone! Today we are going to understand a complete AI Agent that automatically reads customer order emails from Gmail, processes them, creates orders in the database, and sends confirmation emails back — all by itself, no human needed!
This is a real-world example of how LangChain tool-calling agents work.
What we will cover:
- What does this project do? (Big Picture)
- The 3 parts of the system
- What is a LangChain Agent?
- How the Agent processes one email (step by step)
- The Agent Loop (mainAgent.py)
- Tools - how the agent talks to your API
- Gmail Integration - fetch, send, mark as read
- Cron Job - automatic email checking
- System Prompt - the agent's brain
- Database schema
- How to run it
What does this project do?
Imagine a customer sends an email like this:
From: shreyeshk@iconnectsolutions.com Subject: Need laptops for new team members Hi, I'm looking to order 2 Laptops (productId: 1) Recipient: John Doe Address: 123 Main St, City Phone: 1234567890
Your AI agent will:
1. Read this email from Gmail (automatically, every 2 minutes) 2. Understand: "Oh, this person wants 2 Laptops" 3. Find the customer in the database by their email 4. Find the product (Laptop, id: 1) in the database 5. Create an order (2 laptops for this customer) 6. Send a confirmation email back: "Your order #42 is confirmed!" 7. Mark the original email as read ALL AUTOMATICALLY. No human clicks anything.
The 3 Parts of the System
┌───────────────────────────────────────────────────────────────┐ │ YOUR SYSTEM │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ main-agent │ │ parcel-backend│ │ client │ │ │ │ (FastAPI) │ │ (Express) │ │ (React) │ │ │ │ Port 8000 │ │ Port 3000 │ │ Port 5173 │ │ │ │ │ │ │ │ │ │ │ │ - AI Agent │───>│ - REST API │ │ - Chat UI │ │ │ │ - Gmail API │ │ - Database │ │ (for testing)│ │ │ │ - Cron Job │ │ - Orders │ │ │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ │ │ │ │ │ │ │ Gmail API PostgreSQL │ │ (fetch/send) (customers, products, │ │ orders, order_items) │ └───────────────────────────────────────────────────────────────┘
| Part | What it does | Technology |
|---|---|---|
| main-agent | The AI brain. Reads emails, decides what to do, calls tools, sends replies | FastAPI + LangChain + OpenAI GPT-4o + Gmail API |
| parcel-backend | The database API. Stores customers, products, orders | Express.js + Sequelize + PostgreSQL |
| client | Chat UI for testing. You can paste email content manually | React + Vite |
Q: What is a LangChain Agent?
A: Think of it like this — a normal program follows fixed steps (if this, do that). An agent is different. You give the AI tools and instructions, and it decides on its own what to do.
NORMAL PROGRAM (hardcoded): ============================ 1. Parse email with regex 2. Extract customer name 3. Look up product ID 4. Create order 5. Send email Problem: What if the email format changes? What if product name is written differently? What if there are 3 products? BREAKS! LANGCHAIN AGENT (AI decides): =============================== 1. Give AI the email text 2. Give AI a list of tools it CAN use 3. AI reads the email and THINKS: "I see an email from shreyesh@... asking for 2 Laptops" "Let me first find this customer..." "Now let me find the product..." "Now let me create the order..." "Done! Let me send confirmation..." The AI figures out the steps ITSELF. It handles different formats, edge cases, everything.
Q: What are "tools"?
A: Tools are just Python functions that the AI can call. Like:
Tools available to the AI:
===========================
- find_customer_by_email("john@email.com") → returns customer data
- get_product_by_id(1) → returns product data
- find_product("Laptop") → search product by name
- create_order(customer_id=5, product_id=1) → creates order in DB
- send_gmail(to, subject, body) → sends email via Gmail
- get_all_customers() → list all customers
- get_all_products() → list all products
- get_all_orders() → list all orders
The AI CHOOSES which tools to call and in what order.
You don't write if-else logic. The AI decides.
How the Agent Processes ONE Email (Full Trace)
Let's trace exactly what happens when this email arrives:
From: sarah.miller@designagency.com Subject: Office supplies order Hi, we need: - 3x Wireless Keyboard - 2x Wireless Mouse Shipping: Sarah Miller, 456 Oak Avenue, LA
STEP 1: Cron job fetches this unread email from Gmail
=======================================================
cron_job.py runs every 2 minutes
→ Calls gmail_service.fetch_unread_emails()
→ Gets the email: from, subject, body
→ Formats it as a string:
"From: sarah.miller@designagency.com\nSubject: Office supplies\n\nHi, we need..."
STEP 2: Passes email text to mainAgent()
==========================================
mainAgent(query="From: sarah.miller@...\n\nHi, we need: 3x Keyboard...")
Messages sent to GPT-4o:
[SystemPrompt, UserMessage(email text)]
STEP 3: AI reads the email and THINKS
=======================================
AI: "This is an order email. I see 2 products.
Customer email is sarah.miller@designagency.com
Let me find the customer first."
AI DECIDES TO CALL: find_customer_by_email("sarah.miller@designagency.com")
STEP 4: Tool executes, result comes back
==========================================
find_customer_by_email calls Express API:
GET http://localhost:3000/api/customers?email=sarah.miller@designagency.com
Returns: {id: 8, name: "Sarah Miller", email: "sarah.miller@..."}
Tool result added to messages. AI sees: "Found customer, ID is 8"
STEP 5: AI processes FIRST product (Wireless Keyboard)
========================================================
AI: "Now I need to find 'Wireless Keyboard' product"
AI CALLS: find_product("Wireless Keyboard")
→ GET http://localhost:3000/api/products?name=Wireless+Keyboard
→ Returns: {id: 3, name: "Wireless Keyboard", price: 49.99, stock: 50}
AI: "Found it! ID 3. Now create order for 3 units."
AI CALLS: create_order(customer_id=8, product_id=3)
→ POST http://localhost:3000/api/customers/8/orders
→ Body: {productId: 3, quantity: 3}
→ Returns: {orderId: 15, totalAmount: 149.97}
STEP 6: AI processes SECOND product (Wireless Mouse)
======================================================
AI: "Now the second item — Wireless Mouse"
AI CALLS: find_product("Wireless Mouse")
→ Returns: {id: 7, name: "Wireless Mouse", price: 29.99, stock: 100}
AI CALLS: create_order(customer_id=8, product_id=7)
→ Returns: {orderId: 16, totalAmount: 59.98}
STEP 7: AI sends confirmation email
=====================================
AI: "Both orders created! Let me send confirmation."
AI CALLS: send_gmail(
to="sarah.miller@designagency.com",
subject="Order Confirmation - Orders #15, #16",
body="Dear Sarah, your orders have been confirmed!
Order #15: 3x Wireless Keyboard - $149.97
Order #16: 2x Wireless Mouse - $59.98
Total: $209.95"
)
STEP 8: Email marked as read
==============================
cron_job.py calls: mark_as_read(message_id)
→ Removes UNREAD label from Gmail
DONE! Customer gets confirmation email. Orders are in the database.
Notice: the AI made 6 tool calls in this one run. It decided each step on its own!
The Agent Loop (mainAgent.py)
This is the heart of the entire system. It's surprisingly short:
File: main-agent/mainAgent.py
==============================
def mainAgent(query, history=None):
# 1. Build messages
messages = [SystemMessage(SYSTEM_PROMPT)]
if history:
messages.extend(history) # previous conversation
messages.append(HumanMessage(query)) # the email text
# 2. Create LLM with tools
llm = ChatOpenAI(model="gpt-4o")
llm_with_tools = llm.bind_tools(ALL_TOOLS)
# 3. THE LOOP (max 10 iterations)
for i in range(10):
response = llm_with_tools.invoke(messages)
messages.append(response)
# If AI wants to call tools
if response.tool_calls:
for tool_call in response.tool_calls:
tool_name = tool_call["name"]
tool_args = tool_call["args"]
# Execute the tool
result = TOOL_MAP[tool_name].invoke(tool_args)
# Add result back to messages
messages.append(ToolMessage(content=str(result), tool_call_id=...))
# If AI is done (no more tool calls)
else:
break
return {"response": response.content, "history": messages}
Q: Why a loop?
A: Because the AI might need to call MULTIPLE tools. Each iteration:
Iteration 1: AI calls find_customer_by_email → gets result Iteration 2: AI calls find_product → gets result Iteration 3: AI calls create_order → gets result Iteration 4: AI calls send_gmail → gets result Iteration 5: AI says "Done! Orders created." → NO tool calls → loop breaks
Max 10 iterations = safety limit so it doesn't loop forever.
Q: What does bind_tools() do?
A: It tells GPT-4o "hey, here are functions you CAN call". GPT-4o then returns structured tool_calls instead of just text.
WITHOUT bind_tools:
AI response: "I would like to find the customer by email sarah@..."
(just TEXT — useless, we can't execute text)
WITH bind_tools:
AI response.tool_calls: [
{
"name": "find_customer_by_email",
"args": {"customer_email": "sarah.miller@designagency.com"}
}
]
(STRUCTURED — we can execute this!)
Tools - How the Agent talks to your API
Each tool is a Python function with the @tool decorator. Here's one example:
File: main-agent/tools/customer_tools.py
==========================================
from langchain_core.tools import tool
import httpx
API_URL = "http://localhost:3000/api"
@tool
def find_customer_by_email(customer_email: str) -> str:
"""Find a customer by their email address.
Use this when you have the customer's email from the email header."""
response = httpx.get(f"{API_URL}/customers", params={"email": customer_email})
return response.text
That's it! The tool just calls your Express API and returns the result. The AI reads the result and decides what to do next.
How tools connect to the database: =================================== AI calls tool Tool calls Express API Express queries PostgreSQL ───────────── ────────────────────── ───────────────────────── find_customer → GET /api/customers?email= → SELECT * FROM customers get_product_by_id → GET /api/products/1 → SELECT * FROM products create_order → POST /api/customers/8/orders → INSERT INTO orders + order_items send_gmail → Gmail API (not Express) → Sends real email
All 13 Tools
| Tool | File | What it does |
|---|---|---|
| find_customer | customer_tools.py | Search customer by name |
| find_customer_by_email | customer_tools.py | Search customer by email |
| get_all_customers | customer_tools.py | List all customers |
| get_customer_by_id | customer_tools.py | Get customer by ID |
| find_product | product_tools.py | Search product by name |
| get_product_by_id | product_tools.py | Get product by ID |
| get_all_products | product_tools.py | List all products |
| create_product | product_tools.py | Create new product |
| update_product | product_tools.py | Update product details |
| delete_product | product_tools.py | Delete product |
| create_order | order_tools.py | Create order for a customer |
| get_all_orders | order_tools.py | List all orders |
| send_gmail | gmail_tools.py | Send email via Gmail API |
Gmail Integration
Q: How does the app access Gmail?
A: Using Google OAuth2. One-time setup:
ONE-TIME SETUP:
================
1. You visit: http://localhost:8000/auth/google
2. Browser redirects to Google login page
3. You click "Allow" (grant gmail.readonly, gmail.send, gmail.modify)
4. Google sends back a code
5. Your app exchanges code for tokens
6. Tokens saved to token.json
After this, your app can read/send emails WITHOUT your password!
token.json contains:
{
"token": "ya29.a0ARrdaM...", ← access token (expires in 1 hour)
"refresh_token": "1//0eXyz...", ← refresh token (lasts forever)
"expiry": "2025-01-15T10:30:00Z"
}
When access token expires → app uses refresh token to get a new one.
Automatic! No re-login needed.
File: main-agent/gmail_service.py has 3 key functions:
fetch_unread_emails(max_results=1) =================================== → Calls Gmail API: messages.list(q="is:unread") → Gets message IDs → For each: messages.get(id) → extracts From, Subject, Body → Returns list of email dicts send_email(to, subject, body) ============================== → Builds MIME message → Calls Gmail API: messages.send(body=raw_message) → Email sent from YOUR Gmail account mark_as_read(message_id) ========================== → Calls Gmail API: messages.modify(removeLabelIds=["UNREAD"]) → Email no longer shows as unread
Cron Job - Automatic Email Checking
File: main-agent/cron_job.py
What is a cron job?
====================
A function that runs AUTOMATICALLY at a set interval.
Like an alarm clock — every 2 minutes, it wakes up and checks Gmail.
How it works:
==============
from apscheduler.schedulers.background import BackgroundScheduler
scheduler = BackgroundScheduler()
@scheduler.scheduled_job('interval', minutes=2)
def process_unread_emails():
# 1. Fetch unread emails
emails = fetch_unread_emails(max_results=5)
if not emails:
print("No new emails")
return
# 2. Process each email
for email in emails:
text = f"From: {email['from']}\nSubject: {email['subject']}\n\n{email['body']}"
# 3. Send to AI agent
result = mainAgent(query=text, history=None)
# 4. Mark as read (so we don't process it again)
mark_as_read(email['id'])
print(f"Processed: {email['subject']}")
Timeline:
==========
00:00 - Cron runs → no emails → sleeps
00:02 - Cron runs → 1 new email → processes it → marks as read
00:04 - Cron runs → no emails (already read) → sleeps
00:06 - Cron runs → 2 new emails → processes both → marks as read
...runs forever
Q: When does the cron job start?
A: When FastAPI starts! In main.py:
@asynccontextmanager
async def lifespan(app):
# App starting up
scheduler.start() ← cron job begins
yield
# App shutting down
scheduler.shutdown() ← cron job stops
System Prompt - The Agent's Brain
File: main-agent/prompts/system_prompt.py
This is the instruction manual for the AI. It tells the AI EXACTLY how to process orders:
You are an order processing assistant. Process customer order emails.
Step 1: Extract customer details (name, email, phone, address)
Step 2: Extract order details (product ID or name, quantity)
Step 3: Use find_customer_by_email to find customer
Step 4: Use get_product_by_id or find_product to verify product
Step 5: Use create_order with customer ID and product ID
Step 6: If info missing → report what's missing
Step 7: If product not found → inform user
Step 8: For multiple products → process each separately
Step 9: Return Order ID(s) or error messages
Step 10: On success → send_gmail with confirmation
(Subject: "Order Confirmation - Order #ID")
Step 11: On failure → send_gmail with error notification
(Subject: "Action Required - Issue With Your Order Request")
Without this prompt, the AI wouldn't know what to do with an email. The prompt is what makes it an order processing agent vs a general chatbot.
Database Schema
PostgreSQL Database: parcel_db
┌──────────────┐ ┌──────────────┐
│ customers │ │ products │
├──────────────┤ ├──────────────┤
│ id (PK) │ │ id (PK) │
│ name │ │ name │
│ email (UNIQUE)│ │ description │
│ phone │ │ price │
│ address │ │ stock │
└──────┬───────┘ └──────┬───────┘
│ │
│ 1:M │ 1:M
│ │
┌──────┴───────┐ ┌──────┴───────┐
│ orders │ │ order_items │
├──────────────┤ ├──────────────┤
│ id (PK) │──1:M──│ id (PK) │
│ customerId(FK)│ │ orderId (FK) │
│ totalAmount │ │ productId(FK)│
│ status │ │ quantity │
│ (pending, │ │ price │
│ processing, │ └──────────────┘
│ shipped, │
│ delivered, │
│ cancelled) │
└──────────────┘
Q: What happens when an order is created?
POST /api/customers/8/orders {productId: 3, quantity: 3}
Inside orderController.js (with DATABASE TRANSACTION):
=======================================================
1. BEGIN TRANSACTION
2. Find customer (id=8) → exists? ✓
3. Find product (id=3) → exists? ✓
4. Check stock: product.stock (50) >= quantity (3)? ✓
5. Calculate: price = 49.99 * 3 = 149.97
6. INSERT INTO orders (customerId=8, totalAmount=149.97, status='pending')
7. INSERT INTO order_items (orderId=15, productId=3, quantity=3, price=49.99)
8. UPDATE products SET stock = stock - 3 WHERE id = 3
9. COMMIT TRANSACTION
If anything fails → ROLLBACK (nothing changes)
File Structure
D:\AI-agenet/
│
├── main-agent/ ← AI Agent (FastAPI, port 8000)
│ ├── main.py ← Entry point, /chat endpoint, starts cron
│ ├── mainAgent.py ← THE AGENT LOOP (core logic)
│ ├── config.py ← Loads .env variables
│ ├── .env ← API keys (OpenAI, Google OAuth)
│ ├── requirements.txt ← Python packages
│ ├── token.json ← Gmail OAuth tokens
│ │
│ ├── prompts/
│ │ └── system_prompt.py ← Agent instructions
│ │
│ ├── tools/
│ │ ├── __init__.py ← ALL_TOOLS list + TOOL_MAP
│ │ ├── customer_tools.py ← 4 customer tools
│ │ ├── product_tools.py ← 6 product tools
│ │ ├── order_tools.py ← 2 order tools
│ │ └── gmail_tools.py ← 1 send_gmail tool
│ │
│ ├── gmail_service.py ← Gmail API (fetch, send, mark_as_read)
│ ├── auth_routes.py ← Google OAuth2 login flow
│ ├── cron_job.py ← Background job (every 2 min)
│ │
│ └── template/ ← Example emails for testing
│ ├── email1.txt
│ ├── email2.txt
│ └── email3.txt
│
├── parcel-backend/ ← REST API (Express, port 3000)
│ ├── index.js ← Server entry point
│ ├── .env ← DB credentials
│ ├── config/database.js ← PostgreSQL connection
│ ├── models/ ← Customer, Product, Order, OrderItem
│ ├── controllers/ ← Business logic (transactions!)
│ └── routes/ ← API route definitions
│
└── client/ ← Chat UI (React, port 5173)
└── src/App.jsx ← Simple chat interface for testing
How to Run
STEP 1: Start PostgreSQL ========================= Make sure PostgreSQL is running with database "parcel_db" STEP 2: Start Express Backend =============================== cd D:\AI-agenet\parcel-backend npm install npm start → Running on http://localhost:3000 STEP 3: Start FastAPI Agent ============================= cd D:\AI-agenet\main-agent pip install -r requirements.txt uvicorn main:app --reload → Running on http://localhost:8000 → Cron job starts automatically! STEP 4: Gmail Authentication (one time only) ============================================== Open browser: http://localhost:8000/auth/google Login with Google → Allow permissions → token.json is saved. Done forever. STEP 5 (Optional): Start React UI for testing ================================================ cd D:\AI-agenet\client npm install npm run dev → Running on http://localhost:5173 → Paste email text in chat to test manually
Key Takeaway
What makes this an "Agent" and not just a "Script": ===================================================== Script: Fixed steps. Same logic every time. Breaks on edge cases. Agent: AI DECIDES the steps. Adapts to any email format. The SAME agent code handles: - Simple order (1 product, has ID) - Multi-item order (3 products, by name) - Corporate order (PO number, bulk) - Missing info (asks customer to clarify) - Product not found (sends error email) You didn't write if-else for each case. The AI figures it out from the system prompt + tools. That's the power of LangChain tool-calling agents!
Post a Comment