I Built 9 AI Advisors That Argue About My Life Data

A multi-persona LLM pattern with programmatic trigger conditions, agreement detection, and memory distillation, for personal dashboards that interpret data, not just display it.

I Built 9 AI Advisors That Argue About My Life Data

My personal dashboard has 22 data collectors, 51 database models, and 240 MCP tools. It could tell me my sleep was bad, my mood was down, and my habits were slipping. What it couldn't do was interpret any of that. Until I gave it nine different personalities to argue with each other.

The Advisory Council pattern is a multi-persona LLM architecture where each AI advisor has its own personality, domain expertise, trigger conditions, cooldown rules, and persistent memory. It’s not “ask ChatGPT to pretend to be 9 people.” Each advisor fires independently based on real data thresholds, and their disagreements are often more valuable than any single alert.

The Perspective Problem

Most dashboards are single-voice systems. One algorithm, one interpretation, one set of alerts. A sleep score of 65 means the same thing whether you’re optimizing for athletic performance, emotional wellbeing, or career output. The problem isn’t data collection. It’s interpretation diversity.

Consider: your sleep dropped 20% this week. A wellness coach sees a health risk. A strategist sees a goal-stall predictor. A devil’s advocate asks whether your obsession with sleep metrics is itself the problem. Each interpretation is valid. A single-perspective dashboard picks one and discards the rest.

The advisory council pattern gives you all of them.

9 Advisors, 9 Perspectives

Each advisor is a frozen Python dataclass with an ID, name, title, role description, voice sample, accent color, and list of watched domains:

@dataclass(frozen=True)
class AdvisorProfile:
    id: str              # "alistair-vance"
    name: str            # "Alistair Vance"
    title: str           # "The Strategist"
    role: str            # One-sentence role description
    voice_sample: str    # Example quote for LLM voice calibration
    accent_color: str    # For UI avatar circles
    domains: list[str]   # ["goals", "mood", "sleep"]
    trigger_descriptions: list[str]

The voice_sample field is the key design choice. It’s not just metadata. It’s injected into the LLM system prompt when generating commentary. Each advisor literally speaks differently. Here are the personalities:

AdvisorTitleVoiceWatches
Alistair VanceThe Strategist“The data suggests a rather compelling pattern here. Shall we examine it?”Goals, cross-domain correlations
Sister MiriamThe Moral Compass“Before we look at what’s missing, let’s acknowledge what you’ve built.”Low mood, social isolation, gratitude
Jax ThorneThe Fixer“Three things you can do right now. Pick one. Go.”Habit decline, stalled goals
Coach MillerThe Wellness Coach“Your body is keeping score even when you’re not. Let’s check in.”Sleep, health metrics, streaks
Bramwell FinchThe Devil’s Advocate“I see everyone agrees. Excellent. Let me offer the counterpoint.”Complacency, journal rationalization
Birdie MaloneThe Wildcard“You’ve been staring at this dashboard for a while. Maybe go outside?”Hyperfocus, audiobook binges
Professor PenhaligonThe Historian“Interesting. This time last month, the trajectory looked quite different.”Trends, seasonal patterns
Sloane SterlingThe Perception Architect“The way you frame this matters more than the data itself.”Career, professional positioning
Vivienne LeClairThe Flame Keeper“Romance isn’t spontaneous. It’s intentional.”Date nights, intimacy, love languages

These aren’t categories. They’re characters. Alistair speaks in measured, analytical prose. Jax gives you a numbered list and tells you to move. Bramwell asks the uncomfortable question nobody else will. The voice differences aren’t cosmetic. They change how the same data gets interpreted.

How It Works: Trigger → Commentary → Rating → Memory

The critical design choice: advisors don’t fire because you asked them to. They fire because programmatic trigger conditions detected something worth commenting on.

Trigger Conditions Are Data-Driven

Here’s what actual trigger evaluation looks like (simplified from production code):

# Alistair Vance: Goal stalled 7+ days
stalled = [g for g in goals if g.days_since_update > 7]
if stalled and ("alistair-vance", "goal_stalled") not in recent_set:
    triggered.append({
        "advisor_id": "alistair-vance",
        "trigger_type": "goal_stalled",
        "context_summary": f"Goals stalled 7+ days: {names}",
        "commentary": _alistair_stalled(stalled),
    })

# Bramwell Finch: Complacency check (everything too good)
if (all(s >= mood_p75 for s in mood_scores[-3:])
    and all(s.hours >= sleep_p75 for s in sleep_7d[-3:])):
    triggered.append({
        "advisor_id": "bramwell-finch",
        "trigger_type": "complacency_check",
        "commentary": "I see everyone agrees. Everything is wonderful. "
                       "Shall I be the one to ask what you're avoiding?",
    })

# Birdie Malone: Hyperfocus (50+ commits, barely any music)
if commit_count >= 50 and music_count + reading_count < 10:
    triggered.append({
        "advisor_id": "birdie-malone",
        "trigger_type": "hyperfocus_detected",
        "commentary": f"{commit_count} commits and barely a song. "
                       "When was the last time you looked up?",
    })

A crucial detail: triggers use personal baselines (30-day rolling percentiles), not hardcoded thresholds. “Low mood” means below your p25, not some universal number.

baselines = await get_baselines(session)
mood_low_threshold = mood_bl.p25 if mood_bl and not mood_bl.is_default else 4

Cooldowns Prevent Advisor Spam

Without cooldowns, the advisors would be insufferable. Three scope levels control how often each advisor can fire:

SCOPE_COOLDOWNS = {
    "daily":   (timedelta(hours=20), timedelta(hours=48)),
    "weekly":  (timedelta(days=6),   timedelta(days=10)),
    "monthly": (timedelta(days=25),  timedelta(days=35)),
}

Each advisor+trigger combination has an independent cooldown. The result: each comment feels like a considered observation, not a nagging alert.

The Novel Parts

Agreement Detection

This is the highest-signal feature. When 2+ advisors independently flag overlapping domains within a time window, that’s a consensus signal:

# Group recent comments by domain
domain_map: dict[str, list] = {}
for comment in recent_comments:
    for domain in comment.domains:
        domain_map.setdefault(domain, []).append(comment)

# Find domains where 3+ different advisors commented
for domain, comments in domain_map.items():
    unique_advisors = {c.advisor_id for c in comments}
    if len(unique_advisors) >= 3:
        agreements.append({
            "type": "domain_consensus",
            "key": domain,
            "advisors": list(unique_advisors),
        })

Bramwell Finch (the Devil’s Advocate) is specifically excluded from agreements: he’s the contrarian by design, and including him would dilute the signal.

When agreement is detected, priority escalates automatically:

ESCALATION_PRIORITY = {
    1: {"P1": "P1", "P2": "P2", "P3": "P3"},  # single advisor
    2: {"P1": "P1", "P2": "P1", "P3": "P2"},  # 2 advisors agree
    3: {"P1": "P1", "P2": "P1", "P3": "P1"},  # 3+ advisors agree
}

If The Strategist, The Fixer, and The Wellness Coach all flag “goals” in the same week, a routine P3 observation becomes a P1 action item. Created automatically, with a webhook to my AI agent team.

Memory Distillation

When you rate an advisor comment ≥ 4/5 or mark it as “acted on,” it becomes a memory candidate. An LLM call extracts a reusable insight:

# Memory types:
# - observation: "User responds well to data-backed advice"
# - preference: "Prefers morning routines over evening"
# - effective_strategy: "3-item action lists get acted on"
# - ineffective_strategy: "Generic encouragement rated poorly"

# Injected into future prompts:
"""
--- Your Memories (learned from past interactions) ---
- [Effective Strategy] Concrete 3-item action lists get acted on
- [Observation] Mood dips correlate with low social contact
- [Preference] Prefers direct language over gentle suggestions
--- End Memories ---
"""

The loop closes: advisor speaks → user rates → memory extracted → injected into future prompts → advisor speaks better. This is reinforcement learning through human feedback, but for persona calibration rather than model weights.

After a few months, Jax learned that 3-item action lists get acted on, so he always gives exactly 3 items now. Miriam learned that gratitude framing lands better than “you should be grateful.” These aren’t hallucinations. They’re genuine adaptations.

The Full Trigger Catalog

The system currently has 34 trigger conditions across 9 advisors:

AdvisorTriggerCondition
Alistair Vancegoal_stalledNo progress in 7+ days
Alistair Vanceoutput_input_imbalance30+ commits but < 5 mood/sleep logs
Sister Miriammood_dropBelow personal p25 for 3+ days
Sister Miriamsocial_isolationNo interactions in 5+ days
Sister Miriamrelationship_neglectPast contact cadence deadline
Jax Thornehabit_declineCompletion below personal p25
Jax Thornekeystone_missedKeystone habit missed, cascade risk
Coach Millersleep_decline> 1 std dev below baseline
Coach Millerbaby_sleep_disruption3+ baby wake-ups AND parent < 6h
Coach Millerovercommitment6+ meeting hours, score ≥ 7
Coach Millerburnout_riskTrajectory predicts threshold in <10 days
Bramwell Finchcomplacency_checkAll metrics above p75 for 3+ days
Bramwell Finchpillar_recessionBelow target for 3+ consecutive days
Birdie Malonehyperfocus_detected50+ commits with < 10 songs + articles
Vivienne LeClairromantic triggersDate gaps, intimacy frequency, love language balance

Triggers respect adoption: social isolation only fires if you’ve actually logged social events before.

What It’s Like to Live With

I’ve been running this daily for three months. The advisors catch patterns raw dashboards miss:

  • Bramwell’s complacency check fired during a week where every metric looked great. Turned out I was avoiding a hard conversation and the “great metrics” were just distraction-driven productivity.
  • Birdie’s hyperfocus detection caught a 3-day coding sprint where I hadn’t logged mood, sleep, or eaten a real meal. 87 commits, zero songs played.
  • Sister Miriam’s relationship neglect surfaced a friendship that had gone silent for two months, not because of conflict, just drift.
  • Coach Miller’s baby_sleep_disruption combined my son’s wake-up data with my own sleep quality to say “this isn’t just bad sleep. It’s unsustainable parenting load. Ask for help.”

The agreement system is the highest-signal feature. One week, Coach Miller flagged sleep decline, Sister Miriam flagged low mood, and Alistair flagged goal stall. Independently, on different days. The system detected domain consensus on “health,” escalated to P1, and created an action item: “Protect recovery time.” I rated it 5/5. The distilled memory: “When sleep, mood, and goals all decline together, the root cause is usually overcommitment, not any single domain.”

That’s the kind of insight you don’t get from a chart.

Build Your Own (Starter Pattern)

You don’t need 9 advisors and 34 triggers. Here’s a minimal version:

# advisors.py — Minimal 3-advisor setup
ADVISORS = {
    "optimist": {
        "name": "The Optimist",
        "voice": "Focus on what's working. Amplify the positive signal.",
        "domains": ["mood", "habits", "goals"],
    },
    "realist": {
        "name": "The Realist",
        "voice": "Look at the numbers. What do they actually say?",
        "domains": ["sleep", "health", "habits"],
    },
    "contrarian": {
        "name": "The Contrarian",
        "voice": "Everyone agrees? Then someone isn't thinking hard enough.",
        "domains": ["goals", "mood"],
    },
}

# triggers.py — 5 starter triggers
async def evaluate(data: dict) -> list[dict]:
    triggered = []

    if data["days_since_goal_update"] > 7:
        triggered.append({
            "advisor": "realist",
            "trigger": "goal_stalled",
            "context": f"No progress in {data['days_since_goal_update']} days",
        })

    if data["mood_avg"] > 7 and data["sleep_avg"] > 7:
        triggered.append({
            "advisor": "contrarian",
            "trigger": "complacency",
            "context": "All metrics above threshold for 3+ days",
        })

    if data["mood_avg"] < 4:
        triggered.append({
            "advisor": "optimist",
            "trigger": "mood_low",
            "context": f"Mood averaging {data['mood_avg']:.1f} over 3 days",
        })

    return triggered

The architecture scales linearly: add more advisors, add more triggers, and the cooldown + agreement systems handle coordination automatically.

Dashboards That Think in Stereo

The advisory council pattern works anywhere you have data that needs interpretation, not just alerting. Team health dashboards, infrastructure monitoring, financial tracking: anywhere a single perspective flattens nuance, multiple personas restore it.

The memory distillation loop is what separates this from a rule engine. Over months, the advisors genuinely learn your preferences, your patterns, and your blind spots. It’s not AGI. It’s 9 frozen dataclasses, 34 threshold checks, and a feedback loop. But it caught a burnout trajectory I missed, surfaced relationships I was neglecting, and asked uncomfortable questions about weeks I thought were going great.

The best advice I’ve gotten this year didn’t come from a person. It came from nine fictional characters arguing about my sleep data.