JARVIS: a deterministic-first personal assistant
- Role
- Design, build, and daily use
- Stack
- Python · systemd · Telegram · Ollama
- Status
- Running continuously
- Year
- 2026
JARVIS is the Telegram bot I actually talk to every day. It fronts the OptiPlex stack: morning briefs, Docker container control, backup monitoring, location-aware routines, and a job-search pipeline. It's a single Python service under systemd, about 2,200 lines, no framework, because a personal assistant is an operational tool first and a chat toy second.
Design principle: deterministic utility first, LLM second
The first version leaned on an agent framework and an LLM for everything. It was impressive when it worked and useless when it didn't: the model endpoint being down meant I couldn't restart a container from my phone. The rewrite inverted the hierarchy: every command I need daily is plain deterministic code: /status, /containers, /restart n8n, /backup last, and the LLM is an enhancement layered on top for open-ended questions, with graceful degradation when no model is reachable:
def pick_ollama() -> tuple[str, str] | None:
"""
Returns (url, model) for the best available Ollama endpoint.
Prefers the gaming PC (GPU) when online, falls back to local CPU.
Returns None if neither is reachable.
"""
for url, model, label in [
(OLLAMA_URL_REMOTE, OLLAMA_MODEL_REMOTE, "gaming PC"),
(OLLAMA_URL, OLLAMA_MODEL, "local"),
]:
try:
urllib.request.urlopen(f"{url}/api/tags", timeout=3)
log.info(f"Ollama: using {label} ({url}, model={model})")
return url, model
except Exception:
log.debug(f"Ollama: {label} unreachable ({url})")
return NoneThe GPU box is a gaming PC that isn't always on, so model selection is a health-checked fallback chain rather than a config value. If both endpoints are down, every operational command still works.
Try the command surface
A simulated replay: canned output, real commands, honest proportions. The deterministic layer is the part you're poking at; this is what it feels like from my phone:
What it does all day
- Ops: container list/start/stop/restart with alias resolution, log tailing, host CPU/RAM/disk status, and service reachability checks, all from my phone.
- Backup watchdog: a daily check compares the newest restic snapshot's age against a 25-hour threshold and alerts on Telegram only when something is wrong. Silence means healthy.
- Daily brief: one
/todaycommand returns a market snapshot, career prompts, project nudges, and infrastructure status. - Job-search pipeline: a weekday-morning digest of new SE/TAM openings matched to my targets, and on-demand resume tailoring: I paste a job description, and it rewrites the right resume variant from a version-controlled master in my Obsidian vault, saving a per-application copy to the tracker.
- Presence & routines: geofences fed by an iPhone Shortcut, manual check-ins as ground truth, and an adaptive routine brief that learns corrections from a
/learncommand. - Knowledge capture:
/logappends straight into the Obsidian vault inbox, which syncs across devices via CouchDB LiveSync.
Lessons that stuck
- Alert only on deviation. An early watchdog messaged me on every check. It trained me to ignore it within a week, so I removed it and rebuilt alerts to fire only when action is needed.
- Reliability beats capability. The deterministic rewrite made JARVIS strictly less clever and dramatically more useful.
- Own the data layer. Everything JARVIS knows lives in plain-text Markdown in the vault, greppable, diffable, and portable to whatever the next iteration is.
Status
Running continuously as a systemd service. The code is personal by nature (it knows my routines and my infrastructure), so it stays private, but the architecture is easy to talk through: bretdubois1@gmail.com.