← Projects

fitness-agent

The Problem

Modern training data is everywhere and useless. WHOOP knows your overnight recovery, Strava knows what you ran, your lifting notebook is on paper, the weather app is a separate tab, and your training plan lives in your head. None of them talk to each other — and none of them tell you what to do today. The actual question an athlete wakes up with — given how I slept, how my body recovered, what I ran yesterday, and what’s on the plan, what should this session actually look like? — isn’t answered by any one app.

What I Did

I built a personal AI coach that lives in Discord and answers exactly that question. The moment my overnight WHOOP recovery record lands, the bot fuses it with a 7-day physiology trend, recent Strava activity, weather and air quality, and a RAG knowledge base of running, strength, recovery, nutrition, and periodization literature — then prescribes a specific session: pace, HR ceiling, lift template, fueling notes. Not a generic recommendation; a call grounded in the actual state of the athlete that morning.

Lift logging happens conversationally — type “bench 3x10 at 145” in Discord and it parses the set, writes it to SQLite, mirrors it to a five-database Notion training journal, and flags PRs. After every run, a post-workout debrief fuses Strava pace and zone data with WHOOP strain to read whether the session matched intent. A slash-command tree (/recovery, /load, /debrief, /plan, /cost…) covers quick views without free-form prompts.

I owned the full scope from problem definition to deployment. It runs as a systemd service on a small Ubuntu VPS, ingests WHOOP and Strava via webhooks (with a polling fallback), and redeploys on push to main via GitHub Actions. Source at github.com/dylanglatt/fitness-agent.

Data Sources

WHOOP API Overnight recovery, sleep, HRV, and strain — the primary physiological signal. Trigger for the morning brief and the basis for every prescription.
Strava API Activity history with pace, heart-rate zones, and distance — the performance side of the ledger, fused with WHOOP for the post-workout debrief.
Open-Meteo Free, no-key weather and air-quality feed — the environmental layer that shifts an outdoor session’s pace target or routes it indoors.

Stack

Python 3.11 + asyncio Single-process event loop hosting the Discord client, scheduler, and webhook server — one runtime, no broker, no cron.
discord.py User-facing surface — morning brief, conversational chat, slash-command tree, and lift logging by free-form message.
Anthropic Claude Sonnet for the morning brief and post-workout debrief; Haiku for fast conversational chat. Grounded in live physiology + RAG retrieval for every call.
ChromaDB + sentence-transformers Local RAG store over a curated markdown knowledge base — running, strength, recovery, nutrition, periodization, Stoic philosophy — embedded once and retrieved per query.
Notion API (5 databases) Background-written training journal: Schedule, Lifts, Lift Sets, Runs, Daily Log — relational, queryable, and visible to me as a normal Notion page.
SQLite via aiosqlite Local source of truth for lifts and notes — async, file-based, zero-ops, survives Notion outages.
aiohttp webhook server Co-hosted in the bot’s event loop — receives Strava and WHOOP push events behind Caddy + TLS, with a polling fallback if the public endpoint is down.
systemd + GitHub Actions Runs as a systemd service on a small Ubuntu VPS; zero-downtime redeploy on push to main.