Building a Council of AI Advisors for My Personal Dashboard
Eight AI personas with distinct voices monitor my life data and speak up when they have something worth saying. Here's how I built a proactive advisory system with rule-based triggers, scope-aware cooldowns, and LLM-powered chat.
Most personal dashboards stop at data entry. You log your mood, track your habits, record your sleep, and the dashboard shows it back to you in charts. Useful, but passive. I wanted something that actually talks back.
Life Hub is my self-hosted personal dashboard. It aggregates everything: habits, mood, sleep, journal entries, goals, Garmin health data, GitHub commits, reading stats, music listening, infrastructure monitors, and finance. But the feature that changed how I use it wasn't another chart. It was the Council of Advisors: eight AI personas that monitor my life data and speak up when they have something worth saying.
The Concept
The idea came from a simple observation: raw data doesn't change behavior. Seeing that your mood has been declining for three days is information. Having someone say "Hey, your mood's been low, and your sleep dropped an hour this week. Those usually correlate for you" is insight. Making that someone a distinct personality with a consistent voice makes it memorable.
So I built eight advisors, each with a different role:
- Alistair Vance, The Strategist: Pattern recognition across life domains. Connects the dots between sleep trends, goal progress, and mood shifts.
- Birdie Malone, The Wildcard: Levity and unconventional perspectives. Calls out overthinking and hyperfocus.
- Sister Miriam, The Moral Compass: Values alignment, gratitude, recovery principles. Speaks up during prolonged low moods.
- Jax Thorne, The Fixer: Actionable steps, removing blockers. Shows up when habit completion craters or goals stall.
- Professor Penhaligon, The Historian: Historical context and trend analysis. Month-over-month comparisons and seasonal patterns.
- Sloane Sterling, The Perception Architect: Professional positioning and career narrative. Watches GitHub patterns and work-related goals.
- Coach Miller, The Wellness Coach: Sleep optimization, health metrics, sustainable habits. Celebrates streak milestones.
- Bramwell Finch, The Devil's Advocate: Challenges assumptions and stress-tests decisions. When everyone agrees, Bramwell dissents.
They're defined as frozen dataclasses in code: immutable personality profiles with a name, title, role description, voice sample for LLM calibration, accent color for the UI, and a list of domains they care about.
How Triggers Work
Advisors don't post randomly. Each one has specific trigger conditions that are evaluated after every data collection run. The collector pipeline (which runs on a cron schedule) hits a POST /api/v1/council/evaluate-triggers endpoint that queries the last 7 days of life data and checks each advisor's conditions:
# From advisor_triggers.py — simplified
async def evaluate_triggers(session):
today = date.today()
week_ago = (today - timedelta(days=7)).isoformat()
mood_7d = await get_moods(session, since=week_ago)
sleep_7d = await get_sleep(session, since=week_ago)
habits_7d = await get_habits(session, since=week_ago)
goals = await get_active_goals(session)
triggered = []
# Alistair Vance: goal stalled 7+ days
stalled = [g for g in goals
if g.status == "active"
and g.updated_at < week_ago]
if stalled:
triggered.append(build_comment(
advisor="alistair-vance",
trigger_type="goal_stalled",
context=f"Goals stalled 7+ days: {names}",
commentary=alistair_speaks_about(stalled),
))
# Sister Miriam: mood <= 4 for 3+ consecutive days
# Coach Miller: sleep dropping >1h between week halves
# Birdie Malone: 50+ commits but <10 leisure activities
# ... and so on for each advisor
return triggeredThe conditions are concrete and data-driven: no vague heuristics. Alistair fires when a goal hasn't been updated in 7 days. Sister Miriam fires when mood scores are at or below 4 for three consecutive days. Coach Miller fires when sleep hours drop by more than an hour between the first and second half of the week. Birdie calls out hyperfocus when there are 50+ GitHub commits but fewer than 10 music or reading activities.
The Cooldown System
Without rate limiting, advisors would repeat themselves every collection cycle. Nobody wants Jax Thorne nagging about stalled goals every 2 hours. So each trigger has a scope-based cooldown:
# Scope-based cooldowns
DAILY_COOLDOWN = 20 hours # Won't re-fire same trigger within 20h
WEEKLY_COOLDOWN = 6 days # Weekly-scoped triggers cool down for 6 days
# Auto-expiry: comments disappear after their scope window
daily triggers → expire after 48 hours
weekly triggers → expire after 10 days
monthly triggers → expire after 35 daysThe scope is encoded in the trigger type itself. A trigger named daily_mood_check gets daily cooldown rules; weekly_habit_review gets weekly rules. Unscoped triggers like goal_stalled default to weekly cooldown. The system deduplicates on the (advisor_id, trigger_type) tuple: same advisor, same trigger type, same cooldown window = no duplicate post.
Comments also auto-expire. A daily observation disappears after 48 hours. A weekly theme lasts 10 days. This keeps the council feed fresh without manual cleanup.
Advisor Chat
Pop-in comments are one-way: the advisor speaks, you read. But sometimes you want to ask a follow-up. The council page has a slide-out chat panel for each advisor, powered by Claude Haiku.
The system prompt for each conversation includes:
- The advisor's role description and voice sample (for style calibration)
- Current life data context: today's mood, sleep, habits, active goals, recent trends
- Conversation history
- Explicit instructions: stay in character, 2-4 sentences typical, reference life data when relevant
The voice sample is key. Each advisor has a characteristic quote that anchors the LLM's tone:
# Alistair Vance
"The data suggests a rather compelling pattern here — shall we examine it?"
# Birdie Malone
"You've been staring at this dashboard for a while. Maybe go outside?"
# Sister Miriam
"Before we look at what's missing, let's acknowledge what you've built."
# Jax Thorne
"Three goals are stuck. Pick one. Ship it. Then we'll talk about the rest."This produces surprisingly distinct responses. Alistair is measured and analytical. Birdie is blunt and funny. Sister Miriam grounds you. Jax cuts through paralysis. The voice sample gives the LLM enough style information to stay in character without extensive prompt engineering.
Each conversation is a threaded UUID: you can have multiple ongoing conversations with the same advisor, and pick up old threads from the history panel.
Bramwell Finch: The Consensus Breaker
Bramwell Finch has a unique role. He's the devil's advocate, and the system gives him special tooling for it. There's a dedicated detect_advisor_agreements() function that analyzes recent comments from all other advisors, looking for consensus patterns:
- Multiple advisors commenting on the same domain
- Multiple advisors using the same trigger type
- Temporal clustering (several advisors firing in the same window)
When the system detects that three advisors are all saying "great job, keep it up," Bramwell's complacency check fires: "Everyone's celebrating. What are you not seeing?"
This isn't contrarianism for its own sake. Confirmation bias is real, and when all your data points look positive, that's exactly when you stop paying attention to the edges.
The Briefing System
External AI tools (like Claude Code via MCP) can also interact with the council. The get_council_briefing() MCP tool returns a comprehensive snapshot of everything an advisor would need to decide whether to speak:
- Today's mood, sleep, and habits
- 7-day trends with averages and direction (improving/declining/stable)
- Active goals with progress and last-updated timestamps
- Activity counts by source (commits, listens, reads)
- Habit streaks for all tracked habits
- Recent advisor comments (for deduplication)
- Per-advisor cooldown status
- All 8 advisor profiles with their trigger descriptions
This means Claude Code can look at the briefing data, decide which advisors should comment, write commentary in their voice, and post it through the MCP tools, all without any custom code on the dashboard side. The MCP server handles auth, cooldown enforcement, and auto-expiry.
The UI
Council comments appear in two places:
Pop-ins on the Today page and Digest pages. A small HTMX-powered section shows 2-3 relevant advisor comments, auto-refreshing every 5 minutes. Comments are filtered by domain: the Today page shows mood/sleep/habit-related comments; the weekly digest shows weekly-scoped triggers. Each pop-in card has the advisor's avatar, name, accent color border, and a truncated commentary.
The Council page. This is the full view: active advisors with their latest comments, a history toggle for older ones, dismiss buttons, and the slide-out chat panel. Advisors with no recent comments are shown in a "Silent Advisors" grid, still accessible for chat. The whole thing is server-rendered HTML with HTMX for interactivity: no frontend build step, no React, no JavaScript framework.
Architecture Decisions
A few decisions that shaped the implementation:
Advisor definitions in code, not the database. The 8 advisor profiles are frozen dataclasses. The database only stores comments and messages that reference advisors by their slug ID. This means I can refactor advisor roles, adjust voice samples, or add trigger descriptions without data migration. The personality layer is code; the interaction layer is data.
Triggers are rule-based, not LLM-generated. The conditions for when an advisor speaks are deterministic Python: query the data, check thresholds, return true or false. The content of triggered comments can be either templated or LLM-generated, but the decision to fire is always rules. This keeps the system predictable and debuggable. I know exactly why Jax spoke up: because habit completion dropped below 50%.
Soft deletes via auto-expiry. No comments are hard-deleted. Dismissing a comment sets expires_at to now. Time-scoped comments expire naturally. Every query filters on expires_at IS NULL OR expires_at > now. This preserves audit history and means accidental dismissals are recoverable.
MCP as the integration layer. The council is fully accessible through the MCP server: briefings, comments, chat, agreements, dismissals. This means Claude Code (my primary coding tool) can read advisor commentary, post new observations, and participate in conversations, all through the same protocol it uses for everything else in Life Hub.
What Actually Changed
The honest answer: I check Life Hub more often now. Before the council, the dashboard was a place I went to record data. Now it's a place where data talks back. Seeing Coach Miller celebrate a 15-day meditation streak feels different than seeing the number 15 on a chart. Having Alistair point out that my mood dips every time a goal stalls for a week is an insight I wouldn't have surfaced myself, not because the data wasn't there, but because I wasn't looking at it that way.
The advisors don't replace introspection. But they create moments of reflection that I'd otherwise skip. And because each one has a distinct voice, the observations feel less like system alerts and more like perspectives from people who know you.
The whole system runs on a single SQLite database, a single FastAPI process, and a cron job. No vector database. No fine-tuned models. No agent framework. Just structured data, rule-based triggers, and a single LLM call when you want to chat. Sometimes the simplest architecture is the right one.