Backend-first MVP that connects to Gmail, ingests emails, triages and drafts replies via a swappable LLM adapter, supports approve & send, tracks activity, and provides simple reporting boards.
- Gmail OAuth connect, unread fetch with normalization, send via Gmail API
- Triage + Draft via pluggable LLM adapter (Architect by default)
- Tickets with status flow: NEW → NEEDS_ACTION → WAITING_OTHERS → DONE
- Approve & Send with activity log and follow-up scheduling
- Reporting summary and four boards (server-rendered Jinja pages)
- FastAPI, SQLAlchemy (async), SQLite (dev) / Postgres (prod)
- Jinja2 for simple pages, structlog for JSON logs
- Python 3.11+ recommended
app/
api/ # FastAPI app + routers
adapters/ # Gmail + LLM (Architect) adapters
services/ # triage, drafting, sync, templates
models/ # SQLAlchemy entities + Pydantic schemas
core/ # config, db, logging
ui/templates/ # Jinja pages
data/templates.yaml # seed templates
scripts/
tests/
- Python 3.11+
- Google Cloud OAuth2 client (Web app), Gmail API enabled
- Architect LLM service running locally at
LLM_URL(OpenAI-chat-like endpoint)
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
Create .env in the repo root:
ENV=local
# Database (dev)
DB_URL=sqlite+aiosqlite:///./dev.db
# LLM
LLM_PROVIDER=architect
LLM_URL=http://localhost:8000/v1/chat
LLM_MODE=owner
# Increase if your model generates slowly (seconds)
LLM_TIMEOUT_S=180
# Gmail OAuth
GMAIL_CLIENT_ID=your_client_id.apps.googleusercontent.com
GMAIL_CLIENT_SECRET=your_client_secret
GMAIL_REDIRECT_URI=http://localhost:5000/auth/gmail/callback
GMAIL_SCOPES=https://www.googleapis.com/auth/gmail.readonly,https://www.googleapis.com/auth/gmail.modify,https://www.googleapis.com/auth/gmail.send
# Optional
#SYNC_ENABLED=true
#SYNC_INTERVAL_S=300
#GMAIL_TOKEN_PATH=.secrets/gmail_token.json
Secrets and tokens are ignored by .gitignore. Do not commit credentials.
uvicorn app.api.main:app --host 0.0.0.0 --port 5000 --reload
Health check: GET /health
- Initiate:
POST /auth/gmail/initiate→ copyauthorization_urland open in browser. - Consent; Google redirects to
GET /auth/gmail/callback?state=...&code=...→ returns{"status":"ok"}. - A token is stored at
.secrets/gmail_token.json(ignored by Git).
Tip: use the interactive docs at http://localhost:5000/docs to run these.
- Pull unread and persist:
POST /sync/run(returns counts) - List tickets:
GET /tickets - Ticket detail + email body:
GET /tickets/{id} - Boards (server-rendered):
- New:
GET /boards/new - Needs My Action:
GET /boards/needs - Waiting on Others:
GET /boards/waiting - Done:
GET /boards/done
- New:
- Summary:
GET /report/summaryandGET /report/summary.html
Prereq: Ensure your Local-LLM service is running at LLM_URL.
- Draft a reply:
POST /tickets/{id}/draft
{
"template_id": 1,
"tone": "neutral-professional"
}
- Approve & send:
POST /tickets/{id}/approve_send
Other actions:
- Assign:
POST /tickets/{id}/assign?owner_user_id=me - Tag:
POST /tickets/{id}/tag?tag=priority - Status:
POST /tickets/{id}/status?status=NEEDS_ACTION
Enable continuous polling by setting in .env:
SYNC_ENABLED=true
SYNC_INTERVAL_S=300
Restart the server after changes.
pytest -q
- Method Not Allowed on
/auth/gmail/initiate: it’s a POST; use the docs UI orcurl -X POST. Gmail not authenticated: run the OAuth initiate step above and complete consent.- Import conflicts: ensure you run
uvicorn app.api.main:app ...(top-levelapp.pyis legacy; main app is underapp/). - Architect connection: verify
LLM_URLpoints to your local service and it accepts chat-style JSON.
- Do not commit OAuth client JSON or tokens;
.gitignoreis configured to ignore common secret paths. - If secrets were ever committed, rotate them in Google Cloud and purge history with
git filter-repoor BFG.