Productivity Agent with Gmail & Calendar
What is an Email and Calendar Agent?
Email and calendar agents automate routine productivity tasks: reading and classifying emails, drafting responses, scheduling meetings, extracting action items, and managing calendar events. They transform the inbox from a source of overwhelm into a managed workflow.
The key capability is understanding email intent. An effective agent classifies emails by urgency, extracts tasks and deadlines, identifies meeting requests, and generates appropriate responses β all while respecting organizational norms and personal preferences.
These agents integrate with Gmail and Google Calendar APIs, using OAuth2 for authentication and providing read/write access to emails and events.
Project Overview
We will build a productivity agent that:
- Reads and classifies incoming emails (urgent, FYI, spam)
- Extracts action items and deadlines from email content
- Drafts contextually appropriate email responses
- Schedules meetings based on email requests
- Manages calendar availability
- Provides daily briefings
Expected outcome: An agent that manages your inbox and calendar automatically.
Difficulty: Advanced (requires understanding of Google APIs, OAuth2, and NLP)
Architecture
Tools & Setup
| Tool | Version | Purpose |
|---|---|---|
| Python | 3.11+ | Core language |
| google-api-python-client | 2.0+ | Gmail/Calendar APIs |
| google-auth-oauthlib | 1.0+ | OAuth2 authentication |
| openai | 1.0+ | LLM backbone |
| pydantic | 2.0+ | Data models |
Step 1: Environment Setup
python -m venv venv
source venv/bin/activate
pip install google-api-python-client google-auth-oauthlib openai pydantic
Step 2: Project Structure
productivity-agent/
βββ gmail/
β βββ __init__.py
β βββ client.py
βββ calendar/
β βββ __init__.py
β βββ client.py
βββ processing/
β βββ __init__.py
β βββ classifier.py
β βββ task_extractor.py
β βββ response_drafter.py
βββ agent.py
βββ main.py
Step 3: Gmail Client
# gmail/client.py
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from typing import List, Dict
import base64
class GmailClient:
SCOPES = ["https://mail.google.com/"]
def __init__(self, credentials_path: str = "credentials.json"):
self.service = None
self.credentials_path = credentials_path
def authenticate(self, token_path: str = "token.json") -> None:
creds = None
try:
creds = Credentials.from_authorized_user_file(token_path, self.SCOPES)
except:
pass
if not creds or not creds.valid:
flow = InstalledAppFlow.from_client_secrets_file(
self.credentials_path, self.SCOPES
)
creds = flow.run_local_server(port=0)
with open(token_path, "w") as f:
f.write(creds.to_json())
self.service = build("gmail", "v1", credentials=creds)
def get_recent_emails(self, max_results: int = 10, query: str = "") -> List[Dict]:
results = self.service.users().messages().list(
userId="me", maxResults=max_results, q=query
).execute()
messages = results.get("messages", [])
emails = []
for msg in messages:
full = self.service.users().messages().get(
userId="me", id=msg["id"]
).execute()
headers = {
h["name"]: h["value"]
for h in full["payload"].get("headers", [])
}
body = self._extract_body(full["payload"])
emails.append({
"id": msg["id"],
"subject": headers.get("Subject", ""),
"from": headers.get("From", ""),
"to": headers.get("To", ""),
"date": headers.get("Date", ""),
"snippet": full.get("snippet", ""),
"body": body[:5000],
"labels": full.get("labelIds", []),
})
return emails
def _extract_body(self, payload: dict) -> str:
if "body" in payload and payload["body"].get("data"):
return base64.urlsafe_b64decode(payload["body"]["data"]).decode("utf-8", errors="ignore")
if "parts" in payload:
for part in payload["parts"]:
if part["mimeType"] == "text/plain":
if part.get("body", {}).get("data"):
return base64.urlsafe_b64decode(part["body"]["data"]).decode("utf-8", errors="ignore")
return ""
def send_email(self, to: str, subject: str, body: str) -> Dict:
message = {
"raw": base64.urlsafe_b64encode(
f"To: {to}\nSubject: {subject}\n\n{body}".encode()
).decode()
}
return self.service.users().messages().send(
userId="me", body=message
).execute()
Step 4: Calendar Client
# calendar/client.py
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
from datetime import datetime, timedelta
from typing import List, Dict
class CalendarClient:
def __init__(self, credentials_path: str = "credentials.json"):
self.service = None
self.credentials_path = credentials_path
def authenticate(self, token_path: str = "token.json") -> None:
from google_auth_oauthlib.flow import InstalledAppFlow
SCOPES = ["https://www.googleapis.com/auth/calendar"]
creds = None
try:
creds = Credentials.from_authorized_user_file(token_path, SCOPES)
except:
pass
if not creds or not creds.valid:
flow = InstalledAppFlow.from_client_secrets_file(
self.credentials_path, SCOPES
)
creds = flow.run_local_server(port=0)
with open(token_path, "w") as f:
f.write(creds.to_json())
self.service = build("calendar", "v3", credentials=creds)
def get_events(self, days_ahead: int = 7) -> List[Dict]:
now = datetime.utcnow().isoformat() + "Z"
end = (datetime.utcnow() + timedelta(days=days_ahead)).isoformat() + "Z"
events = self.service.events().list(
calendarId="primary",
timeMin=now,
timeMax=end,
singleEvents=True,
orderBy="startTime",
).execute()
return [
{
"id": e["id"],
"summary": e.get("summary", "No title"),
"start": e["start"].get("dateTime", e["start"].get("date")),
"end": e["end"].get("dateTime", e["end"].get("date")),
"description": e.get("description", ""),
"attendees": [a["email"] for a in e.get("attendees", [])],
}
for e in events.get("items", [])
]
def get_free_slots(self, date: datetime, duration_minutes: int = 60) -> List[Dict]:
day_start = date.replace(hour=9, minute=0, second=0).isoformat() + "Z"
day_end = date.replace(hour=17, minute=0, second=0).isoformat() + "Z"
events = self.service.events().list(
calendarId="primary",
timeMin=day_start,
timeMax=day_end,
singleEvents=True,
).execute()
busy_slots = [
(
e["start"].get("dateTime", e["start"].get("date")),
e["end"].get("dateTime", e["end"].get("date")),
)
for e in events.get("items", [])
]
free_slots = []
current = datetime.fromisoformat(day_start.replace("Z", "+00:00"))
end = datetime.fromisoformat(day_end.replace("Z", "+00:00"))
while current + timedelta(minutes=duration_minutes) <= end:
slot_end = current + timedelta(minutes=duration_minutes)
is_free = all(
slot_end.isoformat() <= busy[0] or current.isoformat() >= busy[1]
for busy in busy_slots
)
if is_free:
free_slots.append({
"start": current.isoformat(),
"end": slot_end.isoformat(),
})
current += timedelta(minutes=30)
return free_slots
def create_event(
self,
summary: str,
start: str,
end: str,
description: str = "",
attendees: List[str] = None,
) -> Dict:
event = {
"summary": summary,
"start": {"dateTime": start, "timeZone": "UTC"},
"end": {"dateTime": end, "timeZone": "UTC"},
"description": description,
}
if attendees:
event["attendees"] = [{"email": a} for a in attendees]
return self.service.events().insert(
calendarId="primary", body=event, sendNotifications=True
).execute()
Step 5: Processing Modules
# processing/classifier.py
from openai import OpenAI
import json
class EmailClassifier:
def __init__(self, model: str = "gpt-4-turbo-preview"):
self.client = OpenAI()
self.model = model
def classify(self, email: dict) -> dict:
response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": """Classify email by urgency and intent.
Return JSON:
{
"urgency": "urgent|high|medium|low",
"intent": "request|information|meeting|deadline|spam|newsletter|other",
"requires_response": true/false,
"action_needed": "brief description of what's needed"
}"""},
{"role": "user", "content": f"Subject: {email['subject']}\nFrom: {email['from']}\nBody: {email['body'][:1000]}"},
],
temperature=0.0,
)
try:
return json.loads(response.choices[0].message.content)
except:
return {"urgency": "medium", "intent": "other", "requires_response": False}
# processing/task_extractor.py
from openai import OpenAI
import json
class TaskExtractor:
def __init__(self, model: str = "gpt-4-turbo-preview"):
self.client = OpenAI()
self.model = model
def extract(self, email: dict) -> list:
response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": """Extract action items from email.
Return JSON array:
[{"task": "description", "deadline": "date or null", "priority": "high|medium|low"}]"""},
{"role": "user", "content": f"Email:\n{email['body'][:2000]}"},
],
temperature=0.0,
)
try:
return json.loads(response.choices[0].message.content)
except:
return []
# processing/response_drafter.py
from openai import OpenAI
class ResponseDrafter:
def __init__(self, model: str = "gpt-4-turbo-preview"):
self.client = OpenAI()
self.model = model
def draft(self, email: dict, context: str = "", tone: str = "professional") -> str:
response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": f"""Draft email response in {tone} tone.
Be concise, clear, and action-oriented."""},
{"role": "user", "content": f"Original email:\nSubject: {email['subject']}\nFrom: {email['from']}\n{email['body'][:2000]}\n\n{context}\n\nDraft response:"},
],
temperature=0.7,
)
return response.choices[0].message.content
Step 6: Complete Agent
# agent.py
from gmail.client import GmailClient
from calendar.client import CalendarClient
from processing.classifier import EmailClassifier
from processing.task_extractor import TaskExtractor
from processing.response_drafter import ResponseDrafter
from datetime import datetime
from typing import Dict, List
class ProductivityAgent:
def __init__(self, credentials_path: str = "credentials.json"):
self.gmail = GmailClient(credentials_path)
self.calendar = CalendarClient(credentials_path)
self.classifier = EmailClassifier()
self.task_extractor = TaskExtractor()
self.drafter = ResponseDrafter()
def authenticate(self) -> None:
self.gmail.authenticate()
self.calendar.authenticate()
def get_daily_briefing(self) -> Dict:
emails = self.gmail.get_recent_emails(max_results=20)
classified = []
for email in emails:
classification = self.classifier.classify(email)
tasks = self.task_extractor.extract(email)
classified.append({
**email,
"classification": classification,
"tasks": tasks,
})
urgent = [e for e in classified if e["classification"]["urgency"] == "urgent"]
events = self.calendar.get_events(days_ahead=1)
return {
"date": datetime.now().isoformat(),
"total_emails": len(classified),
"urgent_emails": urgent,
"needs_response": [e for e in classified if e["classification"]["requires_response"]],
"upcoming_events": events,
"all_tasks": [t for e in classified for t in e["tasks"]],
}
def draft_reply(self, email_id: str) -> str:
emails = self.gmail.get_recent_emails(max_results=50)
email = next((e for e in emails if e["id"] == email_id), None)
if not email:
return "Email not found"
return self.drafter.draft(email)
def schedule_meeting(self, summary: str, date: str, duration: int = 60) -> Dict:
from datetime import datetime
dt = datetime.fromisoformat(date)
free_slots = self.calendar.get_free_slots(dt, duration)
if not free_slots:
return {"error": "No free slots available"}
slot = free_slots[0]
return self.calendar.create_event(
summary=summary,
start=slot["start"],
end=slot["end"],
)
Mathematical Foundation
Email Priority Score:
Where each parameter means:
- β urgency classification (0-1)
- β time sensitivity (deadline proximity)
- β sender importance score
Intuition: Balances urgency, time pressure, and sender importance for prioritization.
Scheduling Conflict Resolution:
Intuition: Slots with fewer conflicts score higher. The agent selects the slot minimizing total scheduling friction.
Testing & Evaluation
import pytest
from agent import ProductivityAgent
def test_classifier():
classifier = EmailClassifier()
result = classifier.classify({
"subject": "URGENT: Server down",
"from": "ops@company.com",
"body": "Production server is down. Need immediate action."
})
assert result["urgency"] == "urgent"
def test_task_extractor():
extractor = TaskExtractor()
tasks = extractor.extract({
"body": "Please review the attached document by Friday and send your feedback."
})
assert len(tasks) > 0
Performance Metrics
| Metric | Value | Notes |
|---|---|---|
| Email Classification Accuracy | 90%+ | Urgency detection |
| Task Extraction Rate | 85%+ | Action item identification |
| Response Draft Quality | 8/10 | Human evaluation |
| Calendar Query Time | 1-2s | Google API latency |
| Daily Processing Capacity | 500+ emails | With rate limiting |
Deployment
# main.py
from agent import ProductivityAgent
import json
def main():
agent = ProductivityAgent()
agent.authenticate()
print("Productivity Agent Ready\n")
while True:
cmd = input("Command (briefing/draft/schedule/quit): ").strip()
if cmd == "quit":
break
elif cmd == "briefing":
briefing = agent.get_daily_briefing()
print(json.dumps(briefing, indent=2, default=str))
elif cmd == "draft":
email_id = input("Email ID: ").strip()
draft = agent.draft_reply(email_id)
print(f"\nDraft:\n{draft}")
if __name__ == "__main__":
main()
Real-World Use Cases
- Executive Assistance: Manage executive inbox and calendar
- Sales Follow-up: Track and respond to prospect emails
- Project Management: Extract tasks from project communications
- Customer Support: Prioritize and route customer emails
- Personal Productivity: Automate email triage and scheduling
Common Pitfalls & Solutions
| Pitfall | Solution |
|---|---|
| OAuth token expiry | Implement refresh token handling |
| API rate limits | Use batch operations and caching |
| Incorrect classification | Fine-tune with domain-specific examples |
| Calendar conflicts | Implement conflict detection before scheduling |
| Privacy concerns | Process emails locally when possible |
Summary with Key Takeaways
- Email classification enables automatic prioritization and routing
- Task extraction transforms unstructured emails into actionable items
- Response drafting saves time while maintaining personalization
- Calendar integration enables automated scheduling
- Daily briefings provide actionable summaries of important items