Skip to content

krMaynard/automation

Repository files navigation

Email Assistant

Watches your Gmail inbox, uses Gemini to detect schedulable events and to draft replies, then creates Google Calendar events and Gmail drafts automatically. Also generates a daily morning standup email draft addressed to your key stakeholders. Runs on Google Cloud Run, triggered by Cloud Scheduler.

How it works

Email processing (every 5 minutes)

  1. Cloud Scheduler sends a POST / request to the Cloud Run service every 5 minutes
  2. The service fetches unread Gmail messages that haven't been processed yet
  3. Each email is analyzed by Gemini, which returns two things:
    • Whether the email contains a schedulable event (meeting, appointment, deadline, booking confirmation, etc.) and, if so, the event details
    • Whether the email needs a reply and, if so, one or more concise draft bodies
  4. When an event is detected, a Google Calendar event is created and the Gmail message is labeled event
  5. When a reply is needed, a Gmail draft is created on the original thread. If the email could reasonably be answered in two distinct ways (e.g., accept vs. decline an invitation), both drafts are created so you can pick the one you want to send and discard the other
  6. Processed message IDs are saved to GCS so emails are never analyzed twice

The endpoint responds with a JSON summary like {"processed": 2, "events_created": 1, "drafts_created": 1}.

Daily standup (weekdays at 8 am)

  1. Cloud Scheduler sends a POST /standup request once each weekday morning
  2. The service scans 30 days of sent/received mail to discover key stakeholders, using Gemini to rank them by importance
  3. It fetches today's calendar events, open tasks, recently completed tasks, and recent email thread context per stakeholder
  4. Gemini composes a four-section standup email (Today's Schedule, Open Tasks, Completed Yesterday, Stakeholder Updates)
  5. The draft is saved to Gmail — addressed to all discovered stakeholders — for you to review and send
  6. A GCS record (last_standup_date.json) prevents duplicate drafts on the same day

The endpoint responds with {"draft_url": "https://mail.google.com/mail/#drafts/...", "date": "YYYY-MM-DD"}.

AI Engineering

LLM call design

Each email triggers up to two Gemini calls:

Call Model Purpose Max output
analyze_email gemini-3.5-flash Classify event/reply/action items, signal context-worthiness 1,024 tokens
update_user_context gemini-2.0-flash-lite Extract personal facts into persistent profile dynamic (estimated tokens + 512, range 1,024–4,096)

The context update call only fires when analyze_email returns has_context_update=true — a signal the first call sets when the email contains facts worth remembering (new contact details, family news, stated preferences) versus newsletters, receipts, or automated notifications. This keeps the second call from running on ~50–80% of inbox traffic.

Persistent user context

user_context.json in GCS stores a freeform JSON profile that grows automatically as facts are extracted from emails. It is injected into the analyze_email prompt so drafts are personalized. The profile survives Cloud Run restarts and can be manually edited at any time:

gsutil cat gs://your-bucket-name/user_context.json   # inspect
gsutil cp user_context.json gs://your-bucket-name/   # overwrite/seed

Initial seed example:

{
  "name": "Kyle Maynard",
  "preferred_tone": "friendly but professional",
  "preferred_sign_off": "Thanks, Kyle",
  "family": {},
  "important_contacts": {},
  "notes": []
}

Token cost controls

Several layers prevent runaway billing:

  • Output caps: analyze_email capped at 1,024 output tokens; update_user_context uses a dynamic cap (max(1024, profile_chars / 3 + 512)) bounded at 4,096. Without caps, unconstrained generation was the root cause of a 15× billing spike.
  • Conditional context updates: has_context_update field on EmailAnalysis lets the first LLM call gate the second. Junk email never reaches update_user_context.
  • Input token reduction: quoted reply chains are stripped from email bodies before they reach the model (threaded emails can have 2–3× more quoted history than new content). User context is serialized as compact JSON (no indentation). The context update prompt truncates the email body to 1,500 chars — fact extraction doesn't need the full body.
  • Model tiering: email analysis uses the full-quality model; context extraction (a simpler task) uses gemini-2.0-flash-lite (~10× cheaper per token).
  • Token logging: every Gemini response logs prompt/output/total token counts tagged by call type. Query in Cloud Logging:
    resource.type="cloud_run_revision"
    textPayload=~"tokens \["
    
    Output looks like:
    tokens [analyze_email] 'Lunch Thursday' — prompt: 847, output: 312, total: 1159
    tokens [update_user_context] 'Lunch Thursday' — prompt: 423, output: 198, total: 621
    

Architecture

Cloud Scheduler (*/5 * * * *)   → POST /         → email processing pipeline
Cloud Scheduler (0 8 * * 1-5)  → POST /standup  → daily standup draft

Cloud Run (Flask)
  ├── Gmail API      (read emails, apply labels, create drafts)
  ├── Gemini API     (extract event details, draft replies, compose standup)
  ├── Calendar API   (create events, read today's schedule)
  ├── Tasks API      (create and read action items)
  └── GCS            (token.json, processed_ids.json, user_context.json,
                       last_standup_date.json)

Setup

1. Google Cloud project

Create a project at console.cloud.google.com signed in with the Gmail account you want to monitor.

Enable the required APIs:

gcloud services enable run.googleapis.com cloudbuild.googleapis.com \
  cloudscheduler.googleapis.com storage.googleapis.com gmail.googleapis.com \
  calendar-json.googleapis.com tasks.googleapis.com

2. OAuth credentials

  1. Go to APIs & Services → OAuth consent screen — set up an external app and add your Gmail as a test user
  2. Go to APIs & Services → Credentials → Create Credentials → OAuth 2.0 Client ID
  3. Choose Desktop app, download the JSON, save it as credentials.json

The OAuth scopes used are Gmail modify (read, label, create drafts) and Calendar events.

3. GCS bucket

gsutil mb gs://your-bucket-name

4. Generate the OAuth token locally

python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env  # fill in GEMINI_API_KEY, GCS_BUCKET_NAME, GOOGLE_CREDENTIALS_FILE
python main.py        # browser opens for Google consent

Upload the generated token to GCS:

gsutil cp token.json gs://your-bucket-name/

5. Connect GitHub to Cloud Build

In the Cloud Build console, go to Triggers → Connect Repository and authorize krMaynard/automation. Create a trigger pointing at the main branch using cloudbuild.yaml.

The Cloud Build service account needs two IAM roles:

  • Cloud Run Admin on the project
  • Service Account User on [PROJECT_NUMBER]-compute@developer.gserviceaccount.com

Push to main to trigger the first build and deploy. The Cloud Run service is named assistant (see cloudbuild.yaml).

6. Set environment variables on Cloud Run

gcloud run services update assistant \
  --region us-central1 \
  --set-env-vars GEMINI_API_KEY=AIza...,GCS_BUCKET_NAME=your-bucket-name,CALENDAR_ID=primary

7. Create the Cloud Scheduler jobs

# Create a service account for the scheduler
gcloud iam service-accounts create cloud-scheduler-invoker

gcloud run services add-iam-policy-binding assistant \
  --region=us-central1 \
  --member="serviceAccount:cloud-scheduler-invoker@YOUR_PROJECT.iam.gserviceaccount.com" \
  --role="roles/run.invoker"

SERVICE_URL=$(gcloud run services describe assistant --region us-central1 --format='value(status.url)')

# Email processing: runs every 5 minutes
gcloud scheduler jobs create http email-assistant-poll \
  --schedule="*/5 * * * *" \
  --uri="$SERVICE_URL" \
  --http-method=POST \
  --oidc-service-account-email=cloud-scheduler-invoker@YOUR_PROJECT.iam.gserviceaccount.com \
  --location=us-central1

# Daily standup: runs at 8 am Monday–Friday
gcloud scheduler jobs create http daily-standup \
  --schedule="0 8 * * 1-5" \
  --uri="$SERVICE_URL/standup" \
  --http-method=POST \
  --oidc-service-account-email=cloud-scheduler-invoker@YOUR_PROJECT.iam.gserviceaccount.com \
  --location=us-central1 \
  --time-zone="America/Los_Angeles"

Verifying it works

In Cloud Run → Logs tab, a successful run looks like:

[INFO] Loaded 12 previously processed message ID(s).
[INFO] Processing 2 new message(s)...
[INFO] Analyzing: 'Lunch meeting Thursday'
[INFO] Event detected: 'Lunch with Sarah' at 2026-05-15T12:30:00
[INFO] Calendar event created: https://www.google.com/calendar/event?eid=...
[INFO] Draft reply created: https://mail.google.com/mail/u/0/#drafts/...

To test end-to-end, send yourself an email like:

Subject: Lunch meeting Thursday Body: Let's catch up for lunch this Thursday at 12:30pm at The Market on Main. Should wrap up by 2pm. Does that work for you?

Then click Run now in Cloud Scheduler and check Google Calendar and your Gmail drafts.

Local development

source .venv/bin/activate
python main.py  # runs the Flask server on port 8080

Trigger a run manually:

curl -X POST http://localhost:8080/         # email processing
curl -X POST http://localhost:8080/standup  # standup draft

Environment variables

Variable Description
GEMINI_API_KEY API key from aistudio.google.com
GCS_BUCKET_NAME GCS bucket for token.json, processed_ids.json, user_context.json, last_standup_date.json
CALENDAR_ID Calendar to read/write events (primary = default)
TASKS_LIST_ID Google Tasks list for action items (@default = My Tasks)
GEMINI_MODEL Gemini model for all AI tasks (default gemini-3.5-flash)
STANDUP_LOOKBACK_DAYS Days of mail history to scan for stakeholder discovery (default 30)
STANDUP_THREAD_DAYS Days of thread context per stakeholder included in standup (default 7)
GOOGLE_CREDENTIALS_FILE Path to OAuth client secrets (local only)
GOOGLE_TOKEN_FILE Path to OAuth token file (local only)
BROWSER_PATH Optional path to a specific browser for OAuth consent

About

Watches your Gmail inbox, uses Gemini to detect schedulable events, and automatically creates Google Calendar events. Runs on Google Cloud Run, triggered every 5 minutes by Cloud Scheduler.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors