πŸŽ‰ 75% of content is free forever β€” Unlock Premium from $10/mo β†’
CW
Search courses…
πŸ’Ό Servicesℹ️ Aboutβœ‰οΈ ContactView Pricing Plansfrom $10

Productivity Agent with Gmail & Calendar

AI AgentsEmail and Calendar Agent🟒 Free Lesson

Advertisement

Productivity Agent with Gmail & Calendar

Productivity AgentGmail APICalendar APITask ExtractorSchedulerEmail ClassifierResponse GeneratorProductivity Orchestrator

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

Productivity Agent ArchitectureGmail ClientRead/Write emailsCalendar ClientEvents/AvailabilityLLM ProcessorClassification/ResponseTask ExtractorResponse DrafterSchedulerProductivity Orchestrator

Tools & Setup

ToolVersionPurpose
Python3.11+Core language
google-api-python-client2.0+Gmail/Calendar APIs
google-auth-oauthlib1.0+OAuth2 authentication
openai1.0+LLM backbone
pydantic2.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

Architecture Diagram
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

MetricValueNotes
Email Classification Accuracy90%+Urgency detection
Task Extraction Rate85%+Action item identification
Response Draft Quality8/10Human evaluation
Calendar Query Time1-2sGoogle API latency
Daily Processing Capacity500+ emailsWith 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

PitfallSolution
OAuth token expiryImplement refresh token handling
API rate limitsUse batch operations and caching
Incorrect classificationFine-tune with domain-specific examples
Calendar conflictsImplement conflict detection before scheduling
Privacy concernsProcess 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

Need Expert AI Agents Help?

Get personalized tutoring, project support, or professional consulting.

Advertisement