An AI agent that turns your Garmin wearable data into a personal running coach — powered by a LangGraph orchestration layer, MCP tool integration, and mem0 long-term memory.
Not a chatbot with generic fitness tips. A stateful agent that reasons over your actual training history and personal context before answering.
You: Am I overtraining this week?
Coach: Your training load hit 297 this week — up 34% from last week's 221. Your recovery score has been below 50% for 3 consecutive days, and your average heart rate on easy runs is trending 8bpm higher than your baseline. Given the knee tightness you mentioned after Tuesday's run, I'd recommend keeping tomorrow easy — a 30–40 min zone 2 run or a rest day. Back off before your body forces you to.
Every number came from live Garmin data. The knee context came from mem0 — remembered automatically from a previous conversation.
This isn't a wrapper around the Claude API. The interesting engineering is in the layer between the user and the LLM:
- Tool orchestration — LangGraph agent dynamically decides which Garmin MCP tools to call based on the question, retries on failure, and branches on partial results
- Long-term memory — mem0 automatically extracts and stores personal context (injuries, goals, subjective feel) across conversations, retrieved semantically at query time
- Context assembly — agent combines live Garmin data + mem0 memories into a focused context window, avoiding token waste
- Observability — every agent decision, tool call, and token traced in LangSmith
You ask a question
↓
FastAPI receives your prompt
↓
mem0 retrieves your personal context (injuries, goals, how you felt)
↓
Claude reads your message + memories, then decides which tools it needs:
└── Garmin MCP → queries your local SQLite database (garmin.db)
↓
Claude combines the tool results + your personal context into an answer
↓
Advice based on YOUR data AND your history — not generic templates
flowchart TB
subgraph Nodes["Agent Nodes"]
N1["retrieve_memories"]
N2["call_model"]
N3["tools"]
N4["save_memories"]
end
User["User (Browser)"] -- types message --> UI["Streamlit UI<br>ui/app.py"]
UI -- POST /chat/stream --> API["FastAPI Backend<br>api/main.py"]
API --> Graph["LangGraph Agent<br>agent/graph.py"]
Graph --> N1
N1 --> N2
N2 -- has tool calls --> N3
N3 -- tool results --> N2
N2 -- done --> N4
N2 <-- generate response --> Claude["Claude Sonnet<br>(Anthropic API)"]
N1 <-- search --> Memory["MemoryManager<br>memory/client.py"]
N4 <-- save --> Memory
Memory <--> Mem0["mem0 / SimpleMemory"]
N3 <-- fetch Garmin data --> MCP["Garmin MCP Server<br>garmin-givemydata/run_mcp.py"]
MCP <--> Garmin["Garmin SQLite DB"]
| Layer | Tool | Purpose |
|---|---|---|
| LLM | Claude API | Reasoning and response generation |
| Agent | LangGraph | Multi-step, stateful agent orchestration |
| Garmin data | garmin-givemydata | Browser-based Garmin Connect extraction into local SQLite |
| Garmin MCP | FastMCP (44 tools) | Exposes SQLite data as MCP tools for the agent |
| Custom tools | LangChain tools | Training trend, personal records, week comparison |
| Memory | mem0 | Long-term personal memory across conversations |
| Backend | FastAPI | REST API layer |
| UI | Streamlit | Chat interface |
| Observability | LangSmith | Agent trace monitoring |
- Natural language queries — ask anything about your training in plain English
- Local Garmin data — no cloud dependency, all data stored in a local SQLite database (47 tables, 10+ years of history)
- 44 Garmin MCP tools — activities, sleep, HRV, body battery, training load, VO2max, race predictions, and more
- Persistent personal memory — mem0 automatically remembers your injuries, goals, and how you felt after runs — across every conversation, zero manual setup
- Stateful agent — LangGraph decides which tools to call, retries on failure, branches on partial results
- Full observability — every agent decision traceable in LangSmith
On top of the 44 Garmin MCP tools, this project adds:
| Tool | What It Does |
|---|---|
calculate_training_trend |
Summarizes load trend over N weeks |
extract_personal_records |
Best performances by distance bucket |
compare_weeks |
Side-by-side current vs last week |
garmin-training-coach/
├── agent/
│ ├── graph.py # LangGraph agent graph definition
│ ├── nodes.py # Node functions (retrieve_memories, call_model, save_memories)
│ ├── router.py # Two-stage tool routing (Haiku classifier → tool subset)
│ └── tools.py # Custom analysis tools
├── api/
│ └── main.py # FastAPI app — connects MCP server at startup
├── memory/
│ └── client.py # mem0 3-tier setup: hosted → local → JSON fallback
├── ui/
│ ├── app.py # Streamlit chat interface
│ └── Dockerfile
├── garmin-givemydata/ # Local Garmin data pipeline
│ ├── garmin.db # SQLite database (47 tables, your full Garmin history)
│ ├── garmin_mcp/
│ │ ├── server.py # 44 MCP tools over SQLite
│ │ ├── sync.py # Incremental sync (today + yesterday)
│ │ └── db.py # SQLite connection layer
│ ├── run_mcp.py # MCP server entry point
│ └── Dockerfile
├── tests/
│ ├── test_router.py # Layer 1 evals — router picks correct tool categories
│ └── test_tool_calls.py # Layer 2 evals — model picks correct tool per question
├── .github/workflows/
│ └── test.yml # CI — runs the eval suite on relevant pushes/PRs
├── Dockerfile # API service image
├── docker-compose.yml # Wires api + mcp + ui containers together
├── datasette-metadata.yaml # Table browser config for garmin.db
├── start.bat # One-command launcher (backend + frontend)
└── .env.example
- Python 3.12+
- uv package manager
git clone https://github.com/SamWangTW/AI-Training-Coach
cd AI-Training-Coachuv synccp .env.example .envEdit .env:
ANTHROPIC_API_KEY=your_key_here
MEM0_API_KEY=your_key_here # get from app.mem0.ai, or leave blank for local
LANGSMITH_API_KEY=your_key_here # optional but recommendedInstall dependencies for the Garmin data pipeline:
cd garmin-givemydata
bash setup.sh # macOS/Linux
setup.bat # WindowsAdd your Garmin credentials to garmin-givemydata/.env:
GARMIN_EMAIL=your@email.com
GARMIN_PASSWORD=yourpasswordFetch your full Garmin history (first run takes ~30 min for 10 years of data):
venv/Scripts/python.exe garmin_givemydata.py # Windows
venv/bin/python garmin_givemydata.py # macOS/Linux.\start.bat # Windows — opens backend + frontend in two terminalsOr manually:
# Terminal 1 — backend
.venv\Scripts\uvicorn api.main:app --reload
# Terminal 2 — frontend
.venv\Scripts\streamlit run ui/app.pyOpen http://localhost:8501 and start asking questions about your training.
The API, Garmin MCP server, and UI each run in their own container (Dockerfile, garmin-givemydata/Dockerfile, ui/Dockerfile), wired together with docker-compose.yml. The API talks to the MCP server over the network (streamable-http) instead of spawning it as a local subprocess, and garmin.db is mounted as a volume so it reads your real, already-synced data.
Prerequisites: Docker Desktop running, .env set up (same as step 3 above), and Garmin data already synced at least once locally (step 4 above) — the containers read the existing garmin-givemydata/garmin.db, they don't fetch new data themselves.
docker compose up --buildOpen http://localhost:8501.
Notes:
- Auto-sync on startup is disabled in the containerized API (
ENABLE_AUTO_SYNC=false), since it depends on Selenium/browser automation that's intentionally left out of that image to keep it small. Sync new activities locally as usual (see below) — sincegarmin.dbis a live-mounted volume, the containers immediately see the updated data. - To stop everything:
docker compose down.
After a run, sync your latest data in one of two ways:
Via the chat UI:
"Sync my latest Garmin data"
Via the terminal:
cd garmin-givemydata
venv/Scripts/python.exe -m garmin_mcp.syncThe sync fetches today and yesterday from Garmin Connect, skips activities already in the database, and completes in under a minute once the browser session is established.
Two layers of evals validate the agent's tool-selection behavior against real Claude/Haiku calls, not fixed mock outputs:
| File | What it checks |
|---|---|
tests/test_router.py |
The Haiku classifier (agent/router.py) maps each question to the correct tool categories |
tests/test_tool_calls.py |
The main model (agent/nodes.py's call_model) picks the correct Garmin tool for a given question — tested directly against the node function, so no real garmin.db is needed |
Run locally:
uv run pytest tests/ -vA GitHub Actions workflow runs both suites automatically on any push or pull request to main that touches agent/, tests/, or pyproject.toml — scoped to relevant changes only, since unrelated commits (docs, UI tweaks) can't affect routing behavior. Requires an ANTHROPIC_API_KEY repository secret configured in GitHub, since these tests call the real API rather than mocks.
To explore the raw SQLite database in a web UI (similar to Supabase):
datasette garmin-givemydata/garmin.db --metadata datasette-metadata.yamlOpen http://localhost:8001 — browse all 47 tables, filter rows, and run SQL queries.
Alternatively, install the SQLite Viewer extension in VS Code and click garmin.db directly.
mem0 automatically builds your personal profile from conversations — no manual setup needed.
Just chat naturally:
You: "My left knee felt tight around km 7 today"
↓
mem0 automatically stores:
→ "experiences left knee tightness on longer efforts"
You (3 days later): "Should I do a long run today?"
↓
mem0 retrieves: knee history + your half marathon goal
↓
Coach: "Given your recent knee tightness, I'd cap today
at 8km and keep it easy."
The agent gets smarter the more you use it — without you doing anything extra. View stored memories at app.mem0.ai.
Why LangGraph over a simple LangChain chain? The agent needs to loop. If Claude decides it needs multiple tools to answer a question, LangGraph's graph model handles the tool → model → tool cycle cleanly. A linear chain can't branch or retry based on what data comes back.
Why local SQLite over a cloud database? Health data (sleep, HRV, stress, location) is sensitive. Keeping it local means it never leaves your machine. The MCP server exposes a read-only query interface so the agent can't modify data. If you want multi-device access or to build a multi-user product, migrating to Supabase (same schema, Postgres) is a straightforward next step.
Why mem0 over a custom RAG pipeline? RAG is the right tool for unstructured documents. Your Garmin data is structured and queryable — SQL is more precise than embedding similarity search for "what was my training load last week." mem0 is used only for the unstructured personal context that comes out of conversation: injuries, goals, how you felt. It extracts and stores these automatically, which is the key difference from a manual note-taking approach.
Why a local MCP server over the npm Garmin package? Garmin deployed aggressive bot detection in March 2026 that broke all Python HTTP libraries. The local MCP server (garmin-givemydata) uses a browser-based approach to bypass this, stores data in a local SQLite database, and exposes it via 44 MCP tools. This gives the agent precise, structured access to 10+ years of history without re-fetching from Garmin on every query.
- Evaluation suite — test agent correctness on known coaching scenarios, wired into CI
- Interval training analysis — detect and compare track/interval sessions
- Automatic post-run analysis triggered after each sync
- Weekly training summary pushed to email
- Training plan generation with week-by-week structure
Garmin data pipeline powered by garmin-givemydata — browser-based Garmin Connect extraction with 44 MCP tools and a local SQLite database.
Memory layer powered by mem0 — open source universal memory for AI agents.
MIT