Skip to main content
Glama
README.md
# Thaddeus
[![M8ven Live Monitored](https://m8ven.ai/badge/mcp/baothach10-thaddeus-ncodw4)](https://m8ven.ai/mcp/baothach10-thaddeus-ncodw4)

A local, JARVIS-style personal assistant, developed by Thach Ngo (Thomas Ngo).
Its mission is to run fully locally and boost working efficiency and
productivity. A Python orchestrator drives a local, tool-calling Qwen3
model — via MLX on Apple Silicon (recommended, faster) or via Ollama
everywhere else (the default) — which calls out to custom MCP servers over
stdio for memory, calendar/reminders, weather, Notion task tracking, Claude
Code delegation (planning/implementation/review), web search, IELTS
grading/practice question generation, and running (Strava data + a local
race tracker).

## Privacy / network boundary

Most of this runs fully locally — LLM inference (MLX or Ollama, see below),
SQLite memory, macOS Calendar/Reminders access via AppleScript. Several
servers are explicit, accepted exceptions that reach the network:

- **weather** — plain HTTPS GET to open-meteo.com, no API key, no personal
  data (just coordinates or a city name).
- **notion** — talks to the Notion API with your integration token, to read
  and manage your task database.
- **claude** — the one server that reaches an LLM in the cloud (Anthropic's
  API via the `claude` CLI). Every call runs with `--permission-mode plan`
  for read-only tasks or `acceptEdits` + an explicit git push/commit
  denylist for `implement_from_plan` — verified live, never commits or
  pushes regardless of what's asked.
- **web_search** — queries a self-hosted SearXNG instance (see `searxng/`).
  No API key or account of yours reaches a third party, but SearXNG itself
  still queries upstream engines (Google, Bing, etc.) over the network to
  gather results, as any metasearch aggregator does. Two alternatives were
  tried and rejected: Brave Search API requires a credit card on file even
  for its free tier, and Google's Custom Search API is closed to new
  signups as of 2025.

`notion` and `claude` degrade gracefully — if their `.env` secret isn't
set, that one server is skipped with a warning and everything else still
starts fine. `web_search` needs no secret, but does need the SearXNG
container actually running.

`strava` is fully local — it reads your own free personal data export
(activities.csv) rather than calling Strava's live API, since that now
costs $11.99/month for Standard-tier access (confirmed June 2026 policy
change). Not real-time; re-export periodically from Strava's account
settings to refresh. Its race tracker and `find_races` (web search for
candidate upcoming races, since Strava's API never had race data even
before the paywall) work with no Strava export at all.

### Strava export automation

Requesting the export itself needs a live logged-in Strava browser session,
so that part can't be automated — but everything after you download the
ZIP can be. A LaunchAgent (`scripts/install_strava_automation.sh`) runs
`scripts/strava_automation.py` daily, which:
- watches `~/Downloads` for a Strava export ZIP (detected by checking for
  `activities.csv` inside it, not by guessing Strava's exact filename) and
  auto-extracts it to `data/strava_export/` — no manual unzip/move step
- creates a Reminders.app reminder every ~14 days nudging you to go
  re-request the export, since that step still needs you

Both halves are idempotent (tracked via marker files in `data/`), so the
daily run is a no-op on days there's nothing to do. Uninstall:
`launchctl bootout gui/$(id -u)/com.thachngo.thaddeus.strava-automation`
then remove the plist from `~/Library/LaunchAgents/`.

## Choosing a model backend: MLX vs Ollama

The orchestrator talks to whichever local inference engine
`MODEL_BACKEND` in `.env` names — `orchestrator/backend.py` is the small
factory that switches between them, and every MCP tool/server works
identically either way since the backend only affects how the LLM itself
is served.

- **macOS (recommended): MLX.** Built natively for Metal/unified memory
  rather than going through llama.cpp (what Ollama uses under the hood),
  and confirmed empirically faster on this hardware — same Qwen3 model
  family, thinking disabled on both sides:
  - Plain question: 3.6s (MLX) vs ~8-9s (Ollama native API)
  - Tool call: 1.05s warm (MLX, helped by prompt caching) vs 8.0s (Ollama)
  - Multi-step Notion workflow (`list_connected_databases` →
    `get_database_schema` → `create_task`): Qwen3-8B on MLX completed this
    correctly across 4/4 trials in 12.6-15.4s each; Qwen3-14B on MLX got
    the right result on its one trial but skipped the mandatory
    schema-check step (succeeded on a lucky guess) in 45.2s — the 8B
    build was both faster and more rule-compliant, which is why it's the
    default (`MLX_MODEL` in `.env`) over the larger 14B.
  - Apple Silicon only — this is why it isn't the cross-platform default.
- **Everything else (default): Ollama.** Runs on any platform Ollama
  supports; also the fallback if you're on a Mac but haven't set up MLX
  yet. `MODEL_BACKEND` defaults to `ollama` if unset.

Things worth knowing about MLX before relying on it day to day:
- Qwen3 has the same thinking-mode latency trap on MLX as it does on
  Ollama (on by default, burns the token budget on invisible reasoning) —
  disabled the same way here (`MLX_THINK`/`chat_template_kwargs`) as
  Ollama's native-API `think` field.
- Both the 8B and 14B MLX builds carry Qwen3's native 40,960-token context
  window with no cap applied by `mlx_lm.server` itself, unlike Ollama's
  `OLLAMA_NUM_CTX=16384` here — worth an equivalent cap at the
  `mlx_lm.server` layer if you're tight on memory, to avoid growing the
  KV-cache footprint (and eviction risk) unnecessarily.
- All reliability testing so far used one fixed benchmark phrase
  ("Create a task on Notion for buying ice cream") — worth re-testing with
  varied phrasing/tasks before trusting the pattern beyond that.

## Setup

1. Set up your model backend:
   - **MLX (macOS, recommended):** `pip install -e ".[mlx]"`, then in a
     separate terminal run
     `mlx_lm.server --model mlx-community/Qwen3-8B-4bit --port 8082 --chat-template-args '{"enable_thinking":false}'`
     (first run downloads the model, a few GB). Set `MODEL_BACKEND=mlx` in
     `.env` (step 4 below).
   - **Ollama (default, any platform):** `ollama serve` (if not already
     running), then pull a tool-calling-capable build (see `OLLAMA_MODEL`
     in `.env` for the exact tag in use, currently `qwen3:14b`). Leave
     `MODEL_BACKEND` unset or `ollama` in `.env`.
2. `python3 -m venv .venv && source .venv/bin/activate`
3. `pip install -e ".[dev]"`
4. `cp .env.example .env` and fill in `MODEL_BACKEND` (per step 1),
   `WEATHER_DEFAULT_LAT`/`WEATHER_DEFAULT_LON`
5. `python scripts/sanity_check_sdk.py` — confirms the installed `mcp` SDK's
   actual API surface before anything else is built against it
6. `python scripts/init_db.py`
7. `cd searxng && docker compose up -d && cd ..` — starts the local search
   backend (one-time; it stays running across restarts)
8. Optional, for running tools beyond the race tracker: request a Strava
   data export (account Settings > My Account > Download or Delete Your
   Account > Request Your Archive), extract it to `data/strava_export/`
   once it arrives by email, then `python scripts/inspect_strava_export.py`
   to sanity-check the real column names/units against what
   `strava_server.py` assumes
9. `python -m orchestrator.main`