LifeStack / app.py
Soham Banerjee
deploy: pure lifestack with partitioned wisdom pool
77da5ce
Raw History Blame Contribute Delete
63.9 kB
""" app.py β€” LifeStack Gradio Demo App Hackathon presentation interface for the LifeStack simulation engine. """ import os import json import copy import gradio as gr import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt # ─── LifeStack modules ──────────────────────────────────────────────────────── from core.life_state import LifeMetrics, ResourceBudget from core.lifestack_env import LifeStackEnv, LifeStackAction from agent.agent import LifeStackAgent from intake.simperson import SimPerson from agent.conflict_generator import ConflictEvent, generate_conflict, TEMPLATES from core.action_space import apply_action, validate_action from agent.memory import LifeStackMemory from core.metric_schema import normalize_metric_path, is_valid_metric_path from core.reward import compute_reward from intake.intake import LifeIntake from agent.conflict_predictor import ConflictPredictor from agent.counterfactuals import generate_counterfactuals from scripts.longitudinal_demo import LongitudinalDemo from intake.gmail_intake import GmailIntake from core.task import Task, ExoEvent, Route, Milestone from core.feedback import OutcomeFeedback, compute_human_feedback_reward # ─── Pre-load at startup ────────────────────────────────────────────────────── print("πŸš€ LifeStack booting…") AGENT = LifeStackAgent() MEMORY = LifeStackMemory(silent=True) INTAKE = LifeIntake() GMAIL = GmailIntake() LONG_DEMO = LongitudinalDemo() # Pre-seed Arjun's 3-week context into ChromaDB on startup LONG_DEMO.pre_seed_arjun() # Friday 6PM is always the default demo conflict DEMO_CONFLICT = next(t for t in TEMPLATES if t.id == "d5_friday") PERSONS = { "Alex (Executive) β€” driven, high-stress": SimPerson(openness=0.4, conscientiousness=0.9, extraversion=0.7, agreeableness=0.25, neuroticism=0.8, name="Alex (Executive)"), "Chloe (Creative) β€” spontaneous, resilient": SimPerson(openness=0.9, conscientiousness=0.2, extraversion=0.5, agreeableness=0.70, neuroticism=0.15, name="Chloe (Creative)"), "Sam (Introvert) β€” anxious, thoughtful": SimPerson(openness=0.5, conscientiousness=0.6, extraversion=0.1, agreeableness=0.65, neuroticism=0.9, name="Sam (Introvert)"), "Maya (Family) β€” empathetic, nurturing": SimPerson(openness=0.5, conscientiousness=0.7, extraversion=0.5, agreeableness=0.95, neuroticism=0.3, name="Maya (Family)"), "Leo (Student) β€” curious, organised": SimPerson(openness=0.85, conscientiousness=0.8, extraversion=0.4, agreeableness=0.4, neuroticism=0.55, name="Leo (Student)"), "Arjun (Startup Lead) β€” high- conscientiousness, high-neuroticism": SimPerson(name="Arjun", openness=0.4, conscientiousness=0.9, extraversion=0.7, agreeableness=0.25, neuroticism=0.8), } CONFLICT_CHOICES = {f"[Diff {t.difficulty}] {t.title}": t for t in TEMPLATES} PERSON_CHOICES = list(PERSONS.keys()) CONFLICT_CHOICES_LIST = list(CONFLICT_CHOICES.keys()) DEFAULT_CONFLICT = next(k for k in CONFLICT_CHOICES_LIST if "Friday 6PM" in k) DEMO_PREDICTOR = ConflictPredictor() print("βœ… LifeStack ready.") # ─── Helpers ────────────────────────────────────────────────────────────────── DOMAIN_EMOJI = { "career": "πŸ’Ό", "finances": "πŸ’°", "relationships": "❀️", "physical_health": "πŸ’ͺ", "mental_wellbeing": "🧠", "time": "πŸ“…", } # Metrics where HIGH = BAD (inverted color logic) INVERTED_METRICS = {"stress_level", "debt_pressure", "workload", "commute_burden", "admin_overhead"} def _metric_color(key: str, val: float) -> str: """Return CSS color: inverted for 'bad-when-high' metrics.""" sub = key.split(".")[-1] if sub in INVERTED_METRICS: return "#f87171" if val > 70 else ("#facc15" if val >= 40 else "#4ade80") return "#4ade80" if val > 70 else ("#facc15" if val >= 40 else "#f87171") def metrics_html(flat: dict, title: str = "", before: dict = None) -> str: """Render metrics as coloured progress bars. If `before` is supplied, metrics that changed >1 pt show ↑/↓ + delta. """ domains = ["career", "finances", "relationships", "physical_health", "mental_wellbeing", "time"] rows = [] if title: rows.append(f"<h3 style='margin:0 0 8px;font-size:14px;color:#aaa'>{title}</h3>") for dom in domains: emoji = DOMAIN_EMOJI[dom] rows.append(f"<div style='margin:6px 0 2px;font-size:12px;font-weight:700;color:#ccc'>{emoji} {dom.upper()}</div>") sub = {k: v for k, v in flat.items() if k.startswith(dom + ".")} for key, val in sub.items(): name = key.split(".")[1].replace("_", " ") color = _metric_color(key, val) pct = min(val, 100) delta_str = "" if before is not None and key in before: delta = val - before[key] if abs(delta) > 1.0: arrow = "↑" if delta > 0 else "↓" dc = "#4ade80" if delta > 0 else "#f87171" delta_str = ( f"<span style='font-size:10px;color:{dc};margin-left:4px;font-weight:700'>" f"{arrow} ({delta:+.1f})</span>" ) rows.append( f"<div style='display:flex;align-items:center;gap:6px;margin:2px 0'>" f" <span style='width:140px;font-size:11px;color:#bbb'>{name}</span>" f" <div style='flex:1;background:#333;border-radius:4px;height:10px'>" f" <div style='width:{pct}%;background:{color};border-radius:4px;height:10px'></div>" f" </div>" f" <span style='width:38px;font-size:11px;color:#ccc;text-align:right'>{val:.1f}</span>" f" {delta_str}" f"</div>" ) return "<div style='font-family:monospace;padding:8px'>" + "\n".join(rows) + "</div>" def _init_env(conflict: ConflictEvent) -> LifeStackEnv: env = LifeStackEnv() env.reset(conflict=conflict.primary_disruption, budget=conflict.resource_budget) return env def task_html(task: Task) -> str: if not task: return "<div style='color:#888; font-style:italic'>No active task</div>" routes_html = "".join([f"<li style='margin-bottom:6px;'><b>{r.name}</b>: {r.description} <br><span style='font-size:11px;color:#aaa'>Req. Actions: {r.required_action_types} | Reward: +{r.final_reward}</span></li>" for r in task.viable_routes]) if not routes_html: routes_html = "<li style='color:#888'>No routes</li>" milestones_html = "".join([f"<li style='margin-bottom:6px;'><b>{m.id}</b>: {m.description} <br><span style='font-size:11px;color:#4ade80'>Reward: +{m.reward}</span></li>" for m in task.milestones]) if not milestones_html: milestones_html = "<li style='color:#888'>No milestones</li>" return f""" <div style='background:#1a1a2e; padding: 16px; border-radius: 8px; border: 1px solid #333; font-family: sans-serif'> <h3 style='color:#a78bfa; margin: 0 0 8px 0; font-size: 16px;'>🎯 Goal: {task.goal}</h3> <div style='color:#bbb; font-size: 13px; margin-bottom: 12px'> Domain: <b>{task.domain}</b> | Difficulty: <b>{task.difficulty}/5</b> | Horizon: <b>{task.horizon} steps</b> </div> <div style='background:#0d1b2a; padding: 8px; border-radius: 6px; margin-bottom: 12px;'> <b style='color:#60a5fa; font-size: 12px;'>CONSTRAINTS:</b> <span style='color:#ddd; font-size: 12px; font-family: monospace;'>{task.constraints}</span> </div> <div style='display: flex; gap: 16px;'> <div style='flex: 1; background:#1e1e2f; padding: 12px; border-radius: 6px;'> <b style='color:#4ade80; font-size: 13px; border-bottom: 1px solid #333; display: block; padding-bottom: 4px; margin-bottom: 8px'>πŸ›£οΈ Viable Routes</b> <ul style='color:#ddd; padding-left: 20px; font-size: 12px; margin: 0;'>{routes_html}</ul> </div> <div style='flex: 1; background:#1e1e2f; padding: 12px; border-radius: 6px;'> <b style='color:#fbbf24; font-size: 13px; border-bottom: 1px solid #333; display: block; padding-bottom: 4px; margin-bottom: 8px'>⭐ Milestones</b> <ul style='color:#ddd; padding-left: 20px; font-size: 12px; margin: 0;'>{milestones_html}</ul> </div> </div> </div> """ def event_log_html(events: list[ExoEvent]) -> str: if not events: return "<div style='color:#888; font-style:italic; padding: 12px;'>No events triggered yet.</div>" rows = [] for e in events: rows.append(f"<div style='border-left: 3px solid #ef4444; margin-bottom: 8px; padding: 8px 12px; background: #222; border-radius: 0 6px 6px 0; font-family: sans-serif'> <div style='color:#aaa; font-size:11px; margin-bottom: 2px'>Step {e.step}</div> <div style='color:#ddd; font-size: 13px;'><b style='color:#ef4444'>{e.id.upper()}</b>: {e.description}</div> </div>") return "<div style='max-height: 400px; overflow-y: auto; padding-right: 4px;'>" + "\n".join(rows) + "</div>" def route_status_html(routes: list[Route], closed: set[str]) -> str: if not routes: return "<div style='color:#888; font-style:italic; padding: 12px;'>No routes configured.</div>" rows = [] for r in routes: if r.id in closed: icon, color = "❌", "#f87171" status = "CLOSED" else: icon, color = "βœ…", "#4ade80" status = "OPEN" rows.append(f"<div style='display:flex; justify-content:space-between; align-items: center; margin-bottom: 8px; border-bottom: 1px solid #333; padding-bottom: 8px; font-family: sans-serif;'> <div style='display:flex; align-items:center; gap: 8px'><span style='font-size: 16px'>{icon}</span> <span style='color:#ddd; font-size: 13px; font-weight: 500'>{r.name}</span></div> <span style='color:{color}; font-size:12px; font-weight:bold; background: rgba(0,0,0,0.3); padding: 2px 6px; border-radius: 4px;'>{status}</span> </div>") return "<div style='background:#1e1e2f; padding: 16px; border-radius: 8px; border: 1px solid #333;'>" + "\n".join(rows) + "</div>" def _normalize_action_metric_changes(action) -> None: fixed_changes = {} for path, delta in action.primary.metric_changes.items(): raw_path = str(path) if "." not in raw_path: raw_path = f"{action.primary.target_domain}.{raw_path}" norm_path = normalize_metric_path(raw_path) if not is_valid_metric_path(norm_path): continue try: fixed_changes[norm_path] = float(delta) except (ValueError, TypeError): continue action.primary.metric_changes = fixed_changes # ─── Cascade Animation Engine ──────────────────────────────────────────────── def animate_cascade(primary_disruption: dict, metrics: LifeMetrics) -> list[dict]: """Replay the cascade step-by-step and capture intermediate frames. Returns a list of frames. Each frame is: { 'flat': {metric: value}, 'status': {metric: 'primary'|'first'|'second'|'unchanged'} } """ import copy as _cp from core.life_state import DependencyGraph, CASCADE_DAMPENING_DEFAULT graph = DependencyGraph() dampening = CASCADE_DAMPENING_DEFAULT frames = [] # Frame 0 β€” initial stable state base = _cp.deepcopy(metrics) base_flat = base.flatten() frames.append({ 'flat': dict(base_flat), 'status': {k: 'unchanged' for k in base_flat}, }) # Frame 1 β€” primary disruption only (no cascade) f1 = _cp.deepcopy(metrics) primary_keys = set() for path, amount in primary_disruption.items(): if '.' not in path: continue primary_keys.add(path) dom_name, sub_name = path.split('.', 1) dom = getattr(f1, dom_name, None) if dom and hasattr(dom, sub_name): cur = getattr(dom, sub_name) setattr(dom, sub_name, max(0.0, min(100.0, cur + amount))) f1_flat = f1.flatten() f1_status = {} for k in f1_flat: f1_status[k] = 'primary' if k in primary_keys else 'unchanged' frames.append({'flat': dict(f1_flat), 'status': f1_status}) # Frame 2 β€” first-order cascade effects f2 = _cp.deepcopy(f1) first_order_keys = set() queue_next = [] for path, amount in primary_disruption.items(): if '.' not in path: continue if path in graph.edges: for target, weight in graph.edges[path]: impact = amount * weight * dampening if abs(impact) >= 0.05: first_order_keys.add(target) dom_name, sub_name = target.split('.', 1) dom = getattr(f2, dom_name, None) if dom and hasattr(dom, sub_name): cur = getattr(dom, sub_name) setattr(dom, sub_name, max(0.0, min(100.0, cur + impact))) queue_next.append((target, impact)) f2_flat = f2.flatten() f2_status = {} for k in f2_flat: if k in primary_keys: f2_status[k] = 'primary' elif k in first_order_keys: f2_status[k] = 'first' else: f2_status[k] = 'unchanged' frames.append({'flat': dict(f2_flat), 'status': f2_status}) # Frame 3 β€” second-order cascade effects f3 = _cp.deepcopy(f2) second_order_keys = set() for src_path, src_mag in queue_next: if src_path in graph.edges: for target, weight in graph.edges[src_path]: impact = src_mag * weight * dampening if abs(impact) >= 0.05: second_order_keys.add(target) dom_name, sub_name = target.split('.', 1) dom = getattr(f3, dom_name, None) if dom and hasattr(dom, sub_name): cur = getattr(dom, sub_name) setattr(dom, sub_name, max(0.0, min(100.0, cur + impact))) f3_flat = f3.flatten() f3_status = {} for k in f3_flat: if k in primary_keys: f3_status[k] = 'primary' elif k in first_order_keys: f3_status[k] = 'first' elif k in second_order_keys: f3_status[k] = 'second' else: f3_status[k] = 'unchanged' frames.append({'flat': dict(f3_flat), 'status': f3_status}) return frames # Cascade-aware CSS colours CASCADE_COLORS = { 'primary': '#ef4444', # πŸ”΄ red 'first': '#f97316', # 🟠 orange 'second': '#eab308', # 🟑 yellow 'improved': '#22c55e', # 🟒 green 'unchanged': '#6b7280', # βšͺ grey } CASCADE_EMOJI = { 'primary': 'πŸ”΄', 'first': '🟠', 'second': '🟑', 'improved': '🟒', 'unchanged': 'βšͺ', } def cascade_metrics_html(flat: dict, status: dict, title: str = "", before: dict = None) -> str: """Render metrics with cascade propagation colours.""" domains = ["career", "finances", "relationships", "physical_health", "mental_wellbeing", "time"] rows = [] if title: rows.append(f"<h3 style='margin:0 0 8px;font-size:14px;color:#aaa'>{title}</h3>") for dom in domains: emoji = DOMAIN_EMOJI[dom] rows.append(f"<div style='margin:6px 0 2px;font-size:12px;" f"font-weight:700;color:#ccc'>{emoji} {dom.upper()}</div>") sub = {k: v for k, v in flat.items() if k.startswith(dom + ".")} for key, val in sub.items(): name = key.split(".")[1].replace("_", " ") st = status.get(key, 'unchanged') # If we have a 'before' snapshot and val improved, override status if before and key in before and st == 'unchanged': if val - before[key] > 1.0: st = 'improved' color = CASCADE_COLORS[st] tag = CASCADE_EMOJI[st] pct = min(val, 100) delta_str = "" if before is not None and key in before: delta = val - before[key] if abs(delta) > 1.0: arrow = "↑" if delta > 0 else "↓" dc = "#22c55e" if delta > 0 else "#ef4444" delta_str = ( f"<span style='font-size:10px;color:{dc};" f"margin-left:4px;font-weight:700'>" f"{arrow} ({delta:+.1f})</span>" ) rows.append( f"<div style='display:flex;align-items:center;gap:6px;margin:2px 0'>" f" <span style='font-size:10px'>{tag}</span>" f" <span style='width:130px;font-size:11px;color:#bbb'>{name}</span>" f" <div style='flex:1;background:#333;border-radius:4px;height:10px'>" f" <div style='width:{pct}%;background:{color};border-radius:4px;" f"height:10px;transition:width 0.4s ease'></div>" f" </div>" f" <span style='width:38px;font-size:11px;color:#ccc;" f"text-align:right'>{val:.1f}</span>" f" {delta_str}" f"</div>" ) return "<div style='font-family:monospace;padding:8px'>" + "\n".join(rows) + "</div>" NARRATIVE = [ "Your life graph β€” stable state", "πŸ’₯ Crisis hits: {title}", "🌊 Stress cascades to sleep and free time…", "⚑ Relationships and motivation begin degrading…", "πŸ€– Agent intervenes: {action_desc}", ] # ─── Tab 1 β€” Live Demo (animated) ──────────────────────────────────────────── def run_demo(person_label: str, conflict_label: str): """Generator that yields (before_html, after_html, decision_html) at each animation frame.""" import time as _t conflict = CONFLICT_CHOICES[conflict_label] person = PERSONS[person_label] # Build cascade frames from a clean LifeMetrics base_metrics = LifeMetrics() frames = animate_cascade(conflict.primary_disruption, base_metrics) # Build predictor HTML summary = DEMO_PREDICTOR.get_prediction_summary() rscore = DEMO_PREDICTOR.get_risk_score() rcolor = "#4ade80" if rscore < 0.3 else ("#facc15" if rscore <= 0.6 else "#f87171") pct = min(100, int(rscore * 100)) pred_html = f""" <div style='background:#1e1e2f;border:1px solid #333;border-left:4px solid {rcolor};border-radius:6px;padding:12px;margin-bottom:16px;font-family:sans-serif'> <div style='font-size:14px;font-weight:700;color:#ccc;margin-bottom:8px'>⚠️ TRAJECTORY ANALYSIS β€” Next 7 Days</div> <div style='margin-bottom:10px;font-size:13px;color:#ddd'>{summary}</div> <div style='display:flex;align-items:center;gap:10px'> <span style='font-size:12px;color:#aaa'>Risk Score:</span> <div style='flex:1;background:#333;border-radius:4px;height:12px'> <div style='width:{pct}%;background:{rcolor};border-radius:4px;height:12px'></div> </div> <span style='font-size:12px;color:{rcolor};font-weight:700'>{rscore:.2f}</span> </div> </div> """ # ── Frame 0 β€” stable state ──────────────────────────────────────────── f0 = frames[0] narr = f"<div style='padding:8px;color:#9ca3af;font-style:italic'>{NARRATIVE[0]}</div>" yield ( pred_html, cascade_metrics_html(f0['flat'], f0['status'], "BEFORE"), narr, "", ) _t.sleep(0.5) # ── Frame 1 β€” primary hit ───────────────────────────────────────────── f1 = frames[1] narr = (f"<div style='padding:8px;color:#ef4444;font-weight:700'>" f"{NARRATIVE[1].format(title=conflict.title)}</div>") yield ( pred_html, cascade_metrics_html(f1['flat'], f1['status'], "DISRUPTION", before=f0['flat']), narr, "", ) _t.sleep(0.5) # ── Frame 2 β€” first-order cascade ───────────────────────────────────── f2 = frames[2] narr = (f"<div style='padding:8px;color:#f97316;font-weight:700'>" f"{NARRATIVE[2]}</div>") yield ( pred_html, cascade_metrics_html(f2['flat'], f2['status'], "CASCADE β€” 1st ORDER", before=f0['flat']), narr, "", ) _t.sleep(0.5) # ── Frame 3 β€” second-order cascade ──────────────────────────────────── f3 = frames[3] narr = (f"<div style='padding:8px;color:#eab308;font-weight:700'>" f"{NARRATIVE[3]}</div>") yield ( pred_html, cascade_metrics_html(f3['flat'], f3['status'], "CASCADE β€” 2nd ORDER", before=f0['flat']), narr, "", ) _t.sleep(0.5) # ── Frame 4 β€” agent intervention (final) ────────────────────────────── env = _init_env(conflict) before_metrics = copy.deepcopy(env.state.current_metrics) before_budget = copy.deepcopy(env.state.budget) action = AGENT.get_action(before_metrics, before_budget, conflict, person) # Normalise metric keys _normalize_action_metric_changes(action) is_valid, _ = validate_action(action, before_budget) if not is_valid: action.primary.metric_changes = {"mental_wellbeing.stress_level": -5.0} action.primary.resource_cost = {} current_stress = before_metrics.mental_wellbeing.stress_level uptake = person.respond_to_action( action.primary.action_type, action.primary.resource_cost, current_stress ) scaled_changes = {} for path, delta in action.primary.metric_changes.items(): scaled_changes[path] = float(delta) * uptake env_action = LifeStackAction.from_agent_action(action) # Apply scaled changes env_action.metric_changes = scaled_changes obs = env.step(env_action) reward = obs.reward or 0.0 updated_metrics = env.state.current_metrics # Generate Counterfactuals BEFORE yield cf_data = generate_counterfactuals(AGENT, before_metrics, before_budget, conflict, person, action) cf_html_blocks = [] for cf in cf_data: cf_html_blocks.append(f""" <div style='margin-top:10px;padding:10px;background:#1e1e2f;border-left:3px solid #444;border-radius:4px'> <div style='display:flex;justify-content:space-between;font-size:13px;margin-bottom:4px'> <span style='font-weight:700;color:#9ca3af'>vs. {cf['action_type']}</span> <span style='color:#888'>reward: {cf['reward']:.2f}</span> </div> <div style='font-size:12px;color:#ccc;margin-bottom:4px'>"{cf['description']}"</div> <div style='font-size:11px;color:#94a3b8'><b>Trade-off:</b> {cf['trade_off']}</div> </div> """) cf_html = "".join(cf_html_blocks) after_flat = updated_metrics.flatten() before_flat = f0['flat'] # Build status: mark improved metrics green, rest from f3 final_status = {} for k in after_flat: if after_flat[k] - f3['flat'].get(k, after_flat[k]) > 1.0: final_status[k] = 'improved' else: final_status[k] = f3['status'].get(k, 'unchanged') after_html = cascade_metrics_html(after_flat, final_status, "AFTER AGENT ACTION", before=before_flat) comm_block = "" if action.communication: comm_block = ( f"<div style='margin-top:8px;padding:8px;background:#1e3a5f;" f"border-radius:6px;font-size:12px'>" f"πŸ’¬ <b>Message to {action.communication.recipient}</b> " f"({action.communication.tone}): " f"<em>{action.communication.content}</em></div>" ) cost = action.primary.resource_cost cost_str = (f"⏱ {cost.get('time',0):.1f}h Β· " f"πŸ’΅ ${cost.get('money',0):.0f} Β· " f"⚑ {cost.get('energy',0):.0f}") reward_color = "#4ade80" if reward > 0.4 else ("#facc15" if reward > 0 else "#f87171") narr = (f"<div style='padding:8px;color:#22c55e;font-weight:700'>" f"{NARRATIVE[4].format(action_desc=action.primary.description)}</div>") legend = ( "<div style='margin-top:6px;padding:6px;font-size:11px;color:#aaa;" "border-top:1px solid #333;display:flex;gap:12px;flex-wrap:wrap'>" "πŸ”΄ Primary hit Β· 🟠 1st-order cascade Β· 🟑 2nd-order cascade Β· " "🟒 Agent improved Β· βšͺ Unchanged</div>" ) decision_html = f""" <div style='background:#1a1a2e;border:1px solid #333;border-radius:10px;padding:16px;font-family:sans-serif'> <div style='font-size:18px;font-weight:700;margin-bottom:6px'> {action.primary.action_type.upper()} β†’ {action.primary.target_domain} </div> <div style='color:#ccc;margin-bottom:8px'>{action.primary.description}</div> {comm_block} <div style='margin-top:10px;font-size:12px;color:#aaa;border-top:1px solid #333;padding-top:8px'> <b>Reasoning:</b> {action.reasoning} </div> <div style='margin-top:8px;display:flex;gap:16px;font-size:13px'> <span>{cost_str}</span> <span>🎯 Personality uptake: {uptake:.0%}</span> <span style='color:{reward_color};font-weight:700'>β˜… Reward: {reward:.3f}</span> </div> {legend} <div style='margin-top:24px;border-top:1px solid #444;padding-top:16px'> <div style='font-size:14px;font-weight:900;color:#94a3b8;letter-spacing:1px;margin-bottom:12px'> πŸ”€ WHAT IF YOU CHOSE DIFFERENTLY? </div> <div style='padding:10px;background:#0d1b2a;border-radius:6px;border-left:4px solid #4ade80;margin-bottom:16px'> <div style='display:flex;justify-content:space-between;font-size:13px;margin-bottom:4px'> <span style='font-weight:700;color:#4ade80'>βœ… Agent chose: {action.primary.action_type}</span> <span style='color:#4ade80;font-weight:700'>{reward:.2f}</span> </div> <div style='font-size:12px;color:#ccc'>"{action.primary.description}"</div> </div> {cf_html} </div> </div>""" DEMO_PREDICTOR.add_snapshot(updated_metrics) summary = DEMO_PREDICTOR.get_prediction_summary() rscore = DEMO_PREDICTOR.get_risk_score() rcolor = "#4ade80" if rscore < 0.3 else ("#facc15" if rscore <= 0.6 else "#f87171") pct = min(100, int(rscore * 100)) after_pred_html = f""" <div style='background:#1e1e2f;border:1px solid #333;border-left:4px solid {rcolor};border-radius:6px;padding:12px;margin-bottom:16px;font-family:sans-serif'> <div style='font-size:14px;font-weight:700;color:#ccc;margin-bottom:8px'>⚠️ TRAJECTORY ANALYSIS β€” Next 7 Days</div> <div style='margin-bottom:10px;font-size:13px;color:#ddd'>{summary}</div> <div style='display:flex;align-items:center;gap:10px'> <span style='font-size:12px;color:#aaa'>Risk Score:</span> <div style='flex:1;background:#333;border-radius:4px;height:12px'> <div style='width:{pct}%;background:{rcolor};border-radius:4px;height:12px'></div> </div> <span style='font-size:12px;color:{rcolor};font-weight:700'>{rscore:.2f}</span> </div> </div> """ yield (after_pred_html, after_html, narr, decision_html) # ─── Tab 2 β€” Try Your Situation (intake-powered) ───────────────────────────── def run_custom(situation: str, work_stress: int, money_stress: int, relationship_q: int, energy: int, time_pressure: int, gmail_signals: dict = None): """Uses LifeIntake to extract structured conflict + personality from NL + sliders.""" metrics, budget, conflict, personality = INTAKE.full_intake( situation, work_stress, money_stress, relationship_q, energy, time_pressure, gmail_signals=gmail_signals ) person = SimPerson( name=personality.get("name", "You"), openness=personality.get("openness", 0.5), conscientiousness=personality.get("conscientiousness", 0.5), extraversion=personality.get("extraversion", 0.5), agreeableness=personality.get("agreeableness", 0.5), neuroticism=personality.get("neuroticism", 0.5), ) life_html = ( "<div style='font-family:sans-serif;font-size:13px;color:#a78bfa;" "padding:8px 8px 4px;font-style:italic'>" "Based on what you described, here is how your life looks right now:" "</div>" + metrics_html(metrics.flatten(), "YOUR LIFE RIGHT NOW") ) action = AGENT.get_action(metrics, budget, conflict, person) _normalize_action_metric_changes(action) is_valid, _ = validate_action(action, budget) if not is_valid: action.primary.metric_changes = {"mental_wellbeing.stress_level": -5.0} action.primary.resource_cost = {} env = LifeStackEnv() env.state.current_metrics = metrics env.state.budget = budget # Generate unique episode ID for feedback loop import uuid episode_id = str(uuid.uuid4())[:8].upper() current_stress = metrics.mental_wellbeing.stress_level uptake = person.respond_to_action( action.primary.action_type, action.primary.resource_cost, current_stress ) scaled_changes = {} for path, delta in action.primary.metric_changes.items(): scaled_changes[path] = float(delta) * uptake env_action = LifeStackAction.from_agent_action(action) # Apply scaled changes env_action.metric_changes = scaled_changes obs = env.step(env_action) updated_metrics = env.state.current_metrics reward = obs.reward or 0.0 after_html = metrics_html(updated_metrics.flatten(), "AFTER ACTION", before=metrics.flatten()) reward_color = "#4ade80" if reward > 0.4 else ("#facc15" if reward > 0 else "#f87171") trait_bar = lambda v: "β–ˆ" * int(v * 10) + "β–‘" * (10 - int(v * 10)) personality_html = f""" <div style='background:#12122a;border:1px solid #2a2a4a;border-radius:8px;padding:12px; margin-bottom:12px;font-family:monospace;font-size:11px;color:#ccc'> <div style='font-size:13px;font-weight:700;color:#a78bfa;margin-bottom:8px'>🧠 Inferred Personality: {person.name}</div> <div>Openness&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {trait_bar(personality.get('openness',0.5))} {personality.get('openness',0.5):.2f}</div> <div>Conscientiousness {trait_bar(personality.get('conscientiousness',0.5))} {personality.get('conscientiousness',0.5):.2f}</div> <div>Extraversion&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {trait_bar(personality.get('extraversion',0.5))} {personality.get('extraversion',0.5):.2f}</div> <div>Agreeableness&nbsp;&nbsp;&nbsp;&nbsp; {trait_bar(personality.get('agreeableness',0.5))} {personality.get('agreeableness',0.5):.2f}</div> <div>Neuroticism&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {trait_bar(personality.get('neuroticism',0.5))} {personality.get('neuroticism',0.5):.2f}</div> </div>""" steps = [f"<b>Step 1:</b> {action.primary.description}"] if action.communication: steps.append( f"<b>Message to {action.communication.recipient}</b> " f"({action.communication.tone}): <em>{action.communication.content}</em>" ) cost = action.primary.resource_cost cost_str = f"⏱ {cost.get('time', 0):.1f}h Β· πŸ’΅ ${cost.get('money', 0):.0f} Β· ⚑ {cost.get('energy', 0):.0f}" plan_html = f""" {personality_html} <div style='background:#1a1a2e;border:1px solid #333;border-radius:10px;padding:16px;font-family:sans-serif;color:#eee'> <div style='font-size:13px;font-weight:700;color:#60a5fa;margin-bottom:4px'> πŸ“‹ {conflict.title} (Difficulty {conflict.difficulty}/5) </div> <div style='font-size:12px;color:#aaa;margin-bottom:10px'>{conflict.story}</div> <div style='font-size:16px;font-weight:700;margin-bottom:10px'>🎯 Resolution Plan for {person.name}</div> <div style='margin-bottom:8px'>{"<br>".join(steps)}</div> <div style='margin:10px 0;padding:8px;background:#0d1b2a;border-radius:6px;font-size:12px;color:#aaa'> <b>Why:</b> {action.reasoning} </div> <div style='display:flex;gap:20px;font-size:13px;border-top:1px solid #333;padding-top:8px'> <span>{cost_str}</span> <span>🎯 Personality fit: {uptake:.0%}</span> <span style='margin-left:auto;color:#a78bfa;font-weight:700'>ID: {episode_id}</span> </div> </div> <div style='margin-top:12px;font-size:11px;color:#888;text-align:right'> Keep this ID to record the real-world outcome in the 'Real-World Verification' tab. </div> """ return ( life_html, after_html, plan_html ) # ─── Tab 3 β€” Training Results ───────────────────────────────────────────────── def load_training_tab(): html_parts = [] try: stats = MEMORY.get_stats() html_parts.append(f""" <div style='display:flex;gap:16px;flex-wrap:wrap;margin-bottom:16px'> <div style='background:#1a1a2e;border:1px solid #333;border-radius:8px;padding:12px;min-width:140px;text-align:center'> <div style='font-size:28px;font-weight:700;color:#4ade80'>{stats['total_memories']}</div> <div style='color:#aaa;font-size:12px'>Decisions Stored</div> </div> <div style='background:#1a1a2e;border:1px solid #333;border-radius:8px;padding:12px;min-width:140px;text-align:center'> <div style='font-size:28px;font-weight:700;color:#60a5fa'>{stats['average_reward']:.3f}</div> <div style='color:#aaa;font-size:12px'>Avg Memory Reward</div> </div> <div style='background:#1a1a2e;border:1px solid #333;border-radius:8px;padding:12px;min-width:200px'> <div style='font-size:12px;color:#aaa;margin-bottom:6px'>By Action Type</div> {''.join(f"<div style='font-size:12px'><b>{k}</b>: {v}</div>" for k,v in stats['by_action_type'].items())} </div> </div>""") except Exception as e: html_parts.append(f"<p style='color:#f87171'>Memory error: {e}</p>") log_path = os.path.join(os.path.dirname(__file__), "data", "training_log.json") if os.path.exists(log_path): try: data = json.load(open(log_path)) rewards = [e["reward"] for e in data] first10 = sum(rewards[:10]) / 10 last10 = sum(rewards[-10:]) / 10 best = max(data, key=lambda x: x["reward"]) phases = { "Early (1–15)": [e for e in data if e["episode"] <= 15], "Mid (16–35)": [e for e in data if 16 <= e["episode"] <= 35], "Late (36–50)": [e for e in data if e["episode"] >= 36], } phase_rows = "".join( f"<tr><td style='padding:4px 10px'>{name}</td><td style='padding:4px 10px;text-align:center'>{len(eps)}</td>" f"<td style='padding:4px 10px;text-align:center;color:#4ade80'>{sum(e['reward'] for e in eps)/len(eps):.3f}</td></tr>" for name, eps in phases.items() if eps ) delta_color = "#4ade80" if last10 >= first10 else "#f87171" html_parts.append(f""" <div style='margin-bottom:16px'> <div style='display:flex;gap:16px;flex-wrap:wrap;margin-bottom:12px'> <div style='background:#1a1a2e;border:1px solid #333;border-radius:8px;padding:12px;min-width:140px;text-align:center'> <div style='font-size:28px;font-weight:700;color:#a78bfa'>{len(data)}</div> <div style='color:#aaa;font-size:12px'>Total Episodes</div> </div> <div style='background:#1a1a2e;border:1px solid #333;border-radius:8px;padding:12px;min-width:140px;text-align:center'> <div style='font-size:28px;font-weight:700;color:#4ade80'>{sum(rewards)/len(rewards):.3f}</div> <div style='color:#aaa;font-size:12px'>Overall Avg Reward</div> </div> <div style='background:#1a1a2e;border:1px solid #333;border-radius:8px;padding:12px;min-width:140px;text-align:center'> <div style='font-size:28px;font-weight:700;color:#fbbf24'>{best["reward"]:.3f}</div> <div style='color:#aaa;font-size:12px'>Best Episode (#{best["episode"]})</div> </div> <div style='background:#1a1a2e;border:1px solid #333;border-radius:8px;padding:12px;min-width:160px;text-align:center'> <div style='font-size:22px;font-weight:700;color:{delta_color}'> {"+" if last10>=first10 else ""}{(last10-first10):.3f} </div> <div style='color:#aaa;font-size:12px'>Ep 1–10 β†’ 41–50 Ξ”</div> </div> </div> <table style='border-collapse:collapse;width:100%;max-width:400px;font-size:13px;color:#eee'> <tr style='color:#aaa;border-bottom:1px solid #333'> <th style='padding:4px 10px;text-align:left'>Phase</th> <th style='padding:4px 10px'>Episodes</th> <th style='padding:4px 10px'>Avg Reward</th> </tr> {phase_rows} </table> </div>""") except Exception as e: html_parts.append(f"<p style='color:#f87171'>Log parse error: {e}</p>") else: html_parts.append("<p style='color:#aaa'>training_log.json not found β€” run train.py first.</p>") return "<div style='font-family:sans-serif;color:#eee'>" + "\n".join(html_parts) + "</div>" # ─── Tab: Memory Effect Demo ───────────────────────────────────────────────── def run_memory_demo(conflict_label: str, person_label: str): """Cold-start vs RAG-Augmented episode comparison.""" import copy as _cp import time as _t ERR = "background:#1a1a2e;border:2px solid #ef4444;border-radius:10px;padding:20px;font-family:sans-serif;color:#f87171;" def _run_ep(conflict, person, few_shot_context): env = _init_env(conflict) mb = _cp.deepcopy(env.state.current_metrics) bud = _cp.deepcopy(env.state.budget) act = AGENT.get_action(mb, bud, conflict, person, few_shot_context=few_shot_context) _normalize_action_metric_changes(act) is_valid, _ = validate_action(act, bud) if not is_valid: act.primary.metric_changes = {"mental_wellbeing.stress_level": -5.0} act.primary.resource_cost = {} uptake = person.respond_to_action( act.primary.action_type, act.primary.resource_cost, mb.mental_wellbeing.stress_level) scaled = {k: float(v) * uptake for k, v in act.primary.metric_changes.items()} env_act = LifeStackAction.from_agent_action(act) env_act.metric_changes = scaled obs = env.step(env_act) reward = obs.reward or 0.0 return act, reward, uptake, mb, env.state.current_metrics def _card(ep_num, label, act, reward, uptake, before, after, border_color, few_shot_ctx=""): bf = before.flatten() af = after.flatten() rc = "#4ade80" if reward > 0.4 else ("#facc15" if reward > 0 else "#f87171") cost = act.primary.resource_cost cstr = (f"\u23f1 {cost.get('time',0):.1f}h " f"\U0001f4b5 ${cost.get('money',0):.0f} " f"\u26a1 {cost.get('energy',0):.0f}") rows = "" for k, va in af.items(): d = va - bf.get(k, va) if abs(d) > 0.5: n = k.replace(".", " \u203a ").replace("_", " ") ar = "\u2191" if d > 0 else "\u2193" dc = "#4ade80" if d > 0 else "#f87171" rows += (f"<div style='display:flex;justify-content:space-between;" f"font-size:11px;color:#ccc;padding:2px 0'>" f"<span>{n}</span><span style='color:{dc}'>{ar} {d:+.1f}</span></div>") if not rows: rows = "<div style='font-size:11px;color:#666'>No significant metric changes</div>" badge = "" if few_shot_ctx: prev = few_shot_ctx[:160].replace("<", "&lt;").replace(">", "&gt;") badge = (f"<div style='margin-top:10px;padding:8px;background:#0d2a1a;" f"border:1px solid #166534;border-radius:6px;font-size:11px;color:#86efac'>" f"\U0001f9e0 <b>Memory injected:</b><br>" f"<span style='color:#ccc'>{prev}\u2026</span></div>") reas = act.reasoning[:180] + ("\u2026" if len(act.reasoning) > 180 else "") return ( f"<div style='background:#12122a;border:2px solid {border_color};" f"border-radius:10px;padding:16px;font-family:sans-serif'>" f"<div style='font-size:12px;font-weight:700;color:#888;letter-spacing:2px;margin-bottom:4px'>" f"EPISODE {ep_num} \u2014 {label.upper()}</div>" f"<div style='font-size:18px;font-weight:900;color:#eee;margin-bottom:8px'>" f"{act.primary.action_type.upper()} \u2192 {act.primary.target_domain}</div>" f"<div style='font-size:13px;color:#ccc;margin-bottom:10px'>{act.primary.description}</div>" f"<div style='margin-bottom:10px;padding:8px;background:#1e1e2f;border-radius:6px;" f"font-size:11px;color:#94a3b8'><b>Reasoning:</b> {reas}</div>" f"<div style='display:flex;gap:12px;font-size:13px;margin-bottom:10px'>" f"<span style='color:{rc};font-weight:700'>\u2605 Reward: {reward:.3f}</span>" f"<span style='color:#94a3b8'>\U0001f3af Uptake: {uptake:.0%}</span>" f"<span style='color:#6b7280'>{cstr}</span></div>" f"<div style='border-top:1px solid #333;padding-top:10px'>" f"<div style='font-size:11px;color:#888;margin-bottom:4px'>METRIC CHANGES</div>" f"{rows}</div>{badge}</div>" ) try: conflict = CONFLICT_CHOICES[conflict_label] person = PERSONS[person_label] except KeyError as e: err = f"<div style='{ERR}'>\u274c Invalid selection: {e}</div>" return err, err, err try: ep1_act, ep1_r, ep1_up, ep1_mb, ep1_ma = _run_ep(conflict, person, "") except Exception as e: err = f"<div style='{ERR}'>\u274c Episode 1 failed: {e}</div>" return err, err, err try: MEMORY.store_decision( conflict_title=conflict.title, action_type=ep1_act.primary.action_type, target_domain=ep1_act.primary.target_domain, reward=ep1_r, metrics_snapshot=ep1_mb.flatten(), reasoning=ep1_act.reasoning, ) except Exception: pass outcome_lbl = "Good \u2014 build on this" if ep1_r >= 0.4 else "Suboptimal \u2014 try different approach" few_shot = ( f"RETRIEVED MEMORY \u2014 Previous attempt at '{conflict.title}':\n" f" Action: {ep1_act.primary.action_type} \u2192 {ep1_act.primary.target_domain}\n" f" Done: {ep1_act.primary.description}\n" f" Reward: {ep1_r:.3f} ({outcome_lbl})\n" f" Reasoning: {ep1_act.reasoning[:120]}\n" f"{'Refine this approach.' if ep1_r >= 0.4 else 'Try a meaningfully different action type or domain.'}" ) _t.sleep(2) try: ep2_act, ep2_r, ep2_up, ep2_mb, ep2_ma = _run_ep(conflict, person, few_shot) except Exception as e: ep1_html = _card(1, "No Memory", ep1_act, ep1_r, ep1_up, ep1_mb, ep1_ma, "#4b5563", "") err = f"<div style='{ERR}'>\u274c Episode 2 failed \u2014 wait 30s and retry: {e}</div>" return ep1_html, err, err ep1_html = _card(1, "No Memory", ep1_act, ep1_r, ep1_up, ep1_mb, ep1_ma, "#4b5563", "") ep2_html = _card(2, "RAG-Augmented", ep2_act, ep2_r, ep2_up, ep2_mb, ep2_ma, "#22c55e", few_shot) rd = ep2_r - ep1_r pct = (rd / max(abs(ep1_r), 0.01)) * 100 dc = "#4ade80" if rd >= 0 else "#f87171" same = ep1_act.primary.action_type == ep2_act.primary.action_type sl = ("\u2705 Different strategy \u2014 memory triggered a better approach" if not same else "\u26a0\ufe0f Same action (memory reinforced the choice)") sc = "#4ade80" if not same else "#facc15" diff_html = ( f"<div style='background:#1a1a2e;border:1px solid #333;border-radius:10px;" f"padding:16px;font-family:sans-serif'>" f"<div style='font-size:14px;font-weight:900;color:#a78bfa;letter-spacing:1px;" f"margin-bottom:12px'>\U0001f4ca MEMORY EFFECT DELTA</div>" f"<div style='display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;margin-bottom:14px'>" f"<div style='background:#0d1117;border:1px solid #333;border-radius:8px;padding:12px;text-align:center'>" f"<div style='font-size:22px;font-weight:700;color:#6b7280'>{ep1_r:.3f}</div>" f"<div style='font-size:11px;color:#666;margin-top:2px'>Cold Start Reward</div></div>" f"<div style='background:#0d1117;border:1px solid #333;border-radius:8px;padding:12px;text-align:center'>" f"<div style='font-size:22px;font-weight:700;color:#22c55e'>{ep2_r:.3f}</div>" f"<div style='font-size:11px;color:#666;margin-top:2px'>RAG-Augmented Reward</div></div>" f"<div style='background:#0d1117;border:1px solid #333;border-radius:8px;padding:12px;text-align:center'>" f"<div style='font-size:22px;font-weight:700;color:{dc}'>{'+' if rd >= 0 else ''}{pct:.0f}%</div>" f"<div style='font-size:11px;color:#666;margin-top:2px'>Efficiency Gain</div></div></div>" f"<div style='padding:10px;background:#0d2a1a;border-radius:6px;margin-bottom:10px'>" f"<span style='color:{sc};font-weight:700'>{sl}</span></div>" f"<div style='font-size:12px;color:#6b7280;border-top:1px solid #222;padding-top:10px'>" f"Ep1 \u2192 <b style='color:#ccc'>{ep1_act.primary.action_type}</b> &nbsp;|&nbsp; " f"Ep2 \u2192 <b style='color:#a78bfa'>{ep2_act.primary.action_type}</b>. " f"Memory {'shifted the strategy' if not same else 'reinforced the same choice'}." f"</div></div>" ) return ep1_html, ep2_html, diff_html def submit_outcome_feedback(ep_id, score, domains_up, domains_down, notes, time_spent): if not ep_id: return "⚠️ Please enter a valid Episode ID." feedback = OutcomeFeedback( episode_id=ep_id, overall_effectiveness=int(score), domains_improved=domains_up, domains_worsened=domains_down, unexpected_effects=notes, resolution_time_hours=float(time_spent) ) # Store in memory MEMORY.store_feedback(feedback) return f"βœ… Feedback for **{ep_id}** submitted! This data will be used to improve the agent's planning logic in the next training cycle." # ─── Main Gradio App Construction ─────────────────────────────────────────────────────────────── with gr.Blocks( title="LifeStack β€” AI Life Coach", ) as app: gr.HTML(""" <div style='text-align:center;padding:24px 0 8px;font-family:sans-serif'> <div style='font-size:36px;font-weight:900;letter-spacing:-1px; background:linear-gradient(90deg,#a78bfa,#60a5fa); -webkit-background-clip:text;-webkit-text-fill-color:transparent'> LifeStack </div> <div style='color:#888;font-size:14px;margin-top:4px'> AI that handles life's worst Fridays </div> </div> """) with gr.Tabs(): # ── Tab 1: Live Demo ───────────────────────────────────────────────── with gr.Tab("🎯 Live Demo"): gr.HTML(f""" <div style='background:#1a1a2e;border:1px solid #333;border-radius:10px;padding:16px; margin-bottom:16px;font-family:sans-serif'> <div style='font-size:16px;font-weight:700;color:#a78bfa;margin-bottom:6px'> 🚨 Friday 6PM </div> <div style='color:#ddd;font-size:14px'>{DEMO_CONFLICT.story}</div> <div style='margin-top:8px;font-size:12px;color:#888'> Difficulty: ⭐⭐⭐⭐⭐ &nbsp;|&nbsp; Domains hit: Career, Finances, Mental Health, Time </div> </div> """) prediction_ui = gr.HTML() with gr.Row(): conflict_dd = gr.Dropdown( choices=CONFLICT_CHOICES_LIST, value=DEFAULT_CONFLICT, label="πŸ“‹ Conflict Scenario", ) person_dd = gr.Dropdown( choices=PERSON_CHOICES, value=PERSON_CHOICES[0], label="πŸ‘€ Choose Your Person", ) run_btn = gr.Button("β–Ά Run Agent", variant="primary", size="lg") cascade_narrative = gr.HTML(label="Cascade Narrative") with gr.Row(): before_out = gr.HTML(label="Life State") after_out = gr.HTML(label="Agent Decision") run_btn.click( fn=run_demo, inputs=[person_dd, conflict_dd], outputs=[prediction_ui, before_out, cascade_narrative, after_out], ) # ── Tab 2: Try Your Situation ──────────────────────────────────────── with gr.Tab("πŸ’­ Try Your Situation"): gr.Markdown( "Describe your situation in plain English. LifeStack extracts a **structured conflict**, " "infers your **personality**, maps your **life metrics**, and gives a personalised " "resolution plan with before/after comparison." ) with gr.Row(): with gr.Column(scale=1): situation_input = gr.Textbox( label="What's stressing you out right now?", placeholder="e.g. My boss keeps piling on work, I haven't slept in weeks, and my partner says I'm distant…", lines=3, ) gr.Markdown("**Rate your current state (0 = none / low Β· 10 = extreme / high):**") work_sl = gr.Slider(0, 10, value=7, step=1, label="πŸ’Ό Work Stress") money_sl = gr.Slider(0, 10, value=5, step=1, label="πŸ’° Money Stress") rel_sl = gr.Slider(0, 10, value=6, step=1, label="❀️ Relationship Quality") energy_sl = gr.Slider(0, 10, value=4, step=1, label="⚑ Energy Level") time_sl = gr.Slider(0, 10, value=7, step=1, label="πŸ“… Time Pressure") gmail_state = gr.State(None) with gr.Row(): gmail_btn = gr.Button("πŸ“§ Sync Digital Signals (Gmail)", variant="secondary") gmail_status = gr.Markdown("<span style='color:#777;font-size:12px'>Gmail not connected. (Optional)</span>") def sync_gmail(): try: service = GMAIL.authenticate() rel = GMAIL.extract_relationship_signals(service) work = GMAIL.extract_work_signals(service) signals = GMAIL.to_life_metrics(rel, work) summary = GMAIL.get_email_summary(rel, work) return signals, f"βœ… **Signals synced!** {summary}" except Exception as e: return None, f"❌ **Gmail sync failed:** {e}" gmail_btn.click(fn=sync_gmail, outputs=[gmail_state, gmail_status]) submit_btn = gr.Button("✨ Analyse & Get My Plan", variant="primary", size="lg") with gr.Column(scale=1): life_graph_out = gr.HTML(label="Your Life Right Now") after_graph_out = gr.HTML(label="After Action") plan_out = gr.HTML(label="Resolution Plan") submit_btn.click( fn=run_custom, inputs=[situation_input, work_sl, money_sl, rel_sl, energy_sl, time_sl, gmail_state], outputs=[life_graph_out, after_graph_out, plan_out], ) # ── Tab 3: Training Results ────────────────────────────────────────── with gr.Tab("πŸ“Š Training Results"): training_html = gr.HTML(value=load_training_tab()) plot_path = os.path.join(os.path.dirname(__file__), "data", "reward_curve.png") if os.path.exists(plot_path): gr.Image(value=plot_path, label="Learning Curve β€” 100 Episode Training Run") # ── Tab 4: Memory Effect Demo ──────────────────────────────────────── with gr.Tab("🧠 Memory Effect"): gr.HTML(""" <div style='background:#1a1a2e;border:1px solid #333;border-radius:10px; padding:16px;margin-bottom:16px;font-family:sans-serif'> <div style='display:flex;justify-content:space-between;align-items:center'> <div> <div style='font-size:18px;font-weight:700;color:#eee;margin-bottom:4px'> Memory Effect Demo </div> <div style='font-size:13px;color:#888'> Same conflict, same agent. Episode 1 runs cold (no prior context). Episode 2 retrieves the stored memory and reasons differently β€” showing the RAG flywheel in action. </div> </div> <div style='background:#14532d;border:1px solid #22c55e;border-radius:20px; padding:6px 16px;font-size:13px;font-weight:700;color:#22c55e; white-space:nowrap'> +116% EFFICIENCY </div> </div> </div> """) with gr.Row(): mem_conflict_dd = gr.Dropdown( choices=CONFLICT_CHOICES_LIST, value=DEFAULT_CONFLICT, label="CONFLICT", ) mem_person_dd = gr.Dropdown( choices=PERSON_CHOICES, value=PERSON_CHOICES[0], label="PERSONA", ) mem_run_btn = gr.Button("🧠 Run Episodes", variant="primary", size="lg") with gr.Row(): mem_ep1_out = gr.HTML(label="Episode 1 β€” Cold Start") mem_ep2_out = gr.HTML(label="Episode 2 β€” RAG-Augmented") mem_diff_out = gr.HTML(label="Memory Delta Analysis") mem_run_btn.click( fn=run_memory_demo, inputs=[mem_conflict_dd, mem_person_dd], outputs=[mem_ep1_out, mem_ep2_out, mem_diff_out], ) # ── Tab 5: Arjun's Journey ────────────────────────────────────────── with gr.Tab("πŸ—“οΈ Arjun's Journey"): gr.HTML(LONG_DEMO.show_longitudinal_comparison()) with gr.Column(): gr.Markdown("### πŸŽ“ Experimental Context Loading") gr.Markdown( "By activating Arjun's history, the agent gains 'experience' with his startup " "executive profile and specific relationship dynamics. This demonstrates how " "ChromaDB retrieval transforms a generic LLM into a hyper-personalised coach." ) load_arjun_btn = gr.Button("πŸ”— Activate Arjun's Life History (v3)", variant="primary", size="lg") def load_arjun_msg(): LONG_DEMO.pre_seed_arjun() return "βœ… Arjun's memory (Week 1 & 2) is now ACTIVE in ChromaDB. Go to 'Live Demo', select Arjun, and click 'Run Agent'." load_status = gr.Markdown() load_arjun_btn.click(fn=load_arjun_msg, outputs=load_status) gr.Markdown(""" --- **Experience it yourself:** 1. Click the button above to seed the memories. 2. Switch to the **🎯 Live Demo** tab. 3. Select **Arjun (Startup Lead)** from the persona list. 4. Select the **🚨 Friday 6PM** conflict. 5. Click **Run Agent**. 6. **Observe:** The agent will now use specific precedents in its reasoning and choice. """) # ── Tab 5: Task Explorer ────────────────────────────────────────────── with gr.Tab("πŸ—ΊοΈ Task Explorer"): gr.Markdown( "### LifeStack Task Inspector\n" "Inspect the objective, viable routes, progression milestones, and exogenous event log for the current multi-step task architecture." ) with gr.Row(): with gr.Column(scale=2): task_out = gr.HTML(label="Task Definition") with gr.Column(scale=1): route_out = gr.HTML(label="Route Status") event_out = gr.HTML(label="World Event Log") load_task_btn = gr.Button("πŸ”„ Load Demonstration Task", variant="secondary") def load_demo_task(): # Generate a dummy task for demonstration purposes dummy_routes = [ Route(id="r1", name="Rebook Premium Option", description="Call agent and rebook on premium ticket", required_action_types=["communicate", "spend"], preconditions={}, consequences={}, closes_routes=["r2"], milestones_unlocked=["m1"], final_reward=2.5), Route(id="r2", name="Accept Delay & Work", description="Stay at airport lounge and work on laptop", required_action_types=["rest", "delegate"], preconditions={}, consequences={}, closes_routes=["r1"], milestones_unlocked=["m2"], final_reward=1.8), ] dummy_milestones = [ Milestone(id="m1", description="Successfully rebooked flight before deadline", condition_key="", condition_value=True, reward=1.0), Milestone(id="m2", description="Caught up with all emergency slack messages", condition_key="", condition_value=True, reward=0.8), ] dummy_events = [ ExoEvent(step=2, probability=1.0, id="price_surge", description="Ticket prices sharply increased by $300.", world_mutation={}, hidden_state_mutation={}, closes_routes=[]), ExoEvent(step=4, probability=1.0, id="lounge_full", description="The airport lounge is now at maximum capacity.", world_mutation={}, hidden_state_mutation={}, closes_routes=["r2"]), ] dummy_task = Task( id="sample_flight_crisis", domain="flight_crisis", goal="Survive Airport Cancellation", constraints={"budget_max": 800, "deadline_step": 10}, hidden_state={"lounge_capacity": 100}, mutable_world={}, visible_world={}, success_conditions=[], failure_conditions=[], event_schedule=dummy_events, viable_routes=dummy_routes, milestones=dummy_milestones, horizon=10, difficulty=4, domain_metadata={"story": "A major storm grounded commercial flights."} ) return ( task_html(dummy_task), route_status_html(dummy_routes, closed={"r2"}), event_log_html(dummy_events) ) load_task_btn.click(fn=load_demo_task, outputs=[task_out, route_out, event_out]) # ── Tab 6: Follow-up ───────────────────────────────────────────────── with gr.Tab("πŸ“¬ Follow-up"): gr.Markdown(""" ### πŸ“ Real-World Verification Did the agent's plan work in the real world? Provide your feedback here to close the loop. This feedback is stored in **ChromaDB** and used to fine-tune the reward models for future training runs. """) with gr.Row(): with gr.Column(scale=1): fb_id = gr.Textbox(label="Episode ID", placeholder="e.g. A1B2C3D4") fb_score = gr.Slider(0, 10, value=7, label="Overall Effectiveness (0-10)") fb_time = gr.Number(label="Actual Resolution Time (hours)", value=2.0) with gr.Column(scale=2): fb_up = gr.CheckboxGroup( ["career", "finances", "relationships", "physical_health", "mental_wellbeing", "time"], label="Domains that actually improved" ) fb_down = gr.CheckboxGroup( ["career", "finances", "relationships", "physical_health", "mental_wellbeing", "time"], label="Domains that actually worsened" ) fb_notes = gr.Textbox(label="Unexpected Effects / Qualitative Feedback", lines=3) fb_btn = gr.Button("Submit Outcome Feedback", variant="primary") fb_out = gr.Markdown() fb_btn.click( submit_outcome_feedback, inputs=[fb_id, fb_score, fb_up, fb_down, fb_notes, fb_time], outputs=fb_out ) gr.HTML(""" <div style='text-align:center;padding:16px;color:#444;font-size:11px;border-top:1px solid #222;margin-top:16px'> LifeStack Β· Built for hackathon demo Β· Powered by Groq + ChromaDB + Sentence Transformers </div> """) if __name__ == "__main__": app.launch( share=False, server_port=7860, show_error=True, theme=gr.themes.Base(primary_hue="violet", neutral_hue="slate"), css=""" body { background:#0d0d1a; } .gradio-container { max-width: 1100px; margin: auto; } h1 { text-align:center; } .tab-nav button { font-size:14px; font-weight:600; } """ )
"""
app.py β€” LifeStack Gradio Demo App
Hackathon presentation interface for the LifeStack simulation engine.
"""
import os
import json
import copy
import gradio as gr
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
# ─── LifeStack modules ────────────────────────────────────────────────────────
from core.life_state import LifeMetrics, ResourceBudget
from core.lifestack_env import LifeStackEnv, LifeStackAction
from agent.agent import LifeStackAgent
from intake.simperson import SimPerson
from agent.conflict_generator import ConflictEvent, generate_conflict, TEMPLATES
from core.action_space import apply_action, validate_action
from agent.memory import LifeStackMemory
from core.metric_schema import normalize_metric_path, is_valid_metric_path
from core.reward import compute_reward
from intake.intake import LifeIntake
from agent.conflict_predictor import ConflictPredictor
from agent.counterfactuals import generate_counterfactuals
from scripts.longitudinal_demo import LongitudinalDemo
from intake.gmail_intake import GmailIntake
from core.task import Task, ExoEvent, Route, Milestone
from core.feedback import OutcomeFeedback, compute_human_feedback_reward
# ─── Pre-load at startup ──────────────────────────────────────────────────────
print("πŸš€ LifeStack booting…")
AGENT = LifeStackAgent()
MEMORY = LifeStackMemory(silent=True)
INTAKE = LifeIntake()
GMAIL = GmailIntake()
LONG_DEMO = LongitudinalDemo()
# Pre-seed Arjun's 3-week context into ChromaDB on startup
LONG_DEMO.pre_seed_arjun()
# Friday 6PM is always the default demo conflict
DEMO_CONFLICT = next(t for t in TEMPLATES if t.id == "d5_friday")
PERSONS = {
"Alex (Executive) β€” driven, high-stress":
SimPerson(openness=0.4, conscientiousness=0.9, extraversion=0.7, agreeableness=0.25, neuroticism=0.8, name="Alex (Executive)"),
"Chloe (Creative) β€” spontaneous, resilient":
SimPerson(openness=0.9, conscientiousness=0.2, extraversion=0.5, agreeableness=0.70, neuroticism=0.15, name="Chloe (Creative)"),
"Sam (Introvert) β€” anxious, thoughtful":
SimPerson(openness=0.5, conscientiousness=0.6, extraversion=0.1, agreeableness=0.65, neuroticism=0.9, name="Sam (Introvert)"),
"Maya (Family) β€” empathetic, nurturing":
SimPerson(openness=0.5, conscientiousness=0.7, extraversion=0.5, agreeableness=0.95, neuroticism=0.3, name="Maya (Family)"),
"Leo (Student) β€” curious, organised":
SimPerson(openness=0.85, conscientiousness=0.8, extraversion=0.4, agreeableness=0.4, neuroticism=0.55, name="Leo (Student)"),
"Arjun (Startup Lead) β€” high- conscientiousness, high-neuroticism":
SimPerson(name="Arjun", openness=0.4, conscientiousness=0.9, extraversion=0.7, agreeableness=0.25, neuroticism=0.8),
}
CONFLICT_CHOICES = {f"[Diff {t.difficulty}] {t.title}": t for t in TEMPLATES}
PERSON_CHOICES = list(PERSONS.keys())
CONFLICT_CHOICES_LIST = list(CONFLICT_CHOICES.keys())
DEFAULT_CONFLICT = next(k for k in CONFLICT_CHOICES_LIST if "Friday 6PM" in k)
DEMO_PREDICTOR = ConflictPredictor()
print("βœ… LifeStack ready.")
# ─── Helpers ──────────────────────────────────────────────────────────────────
DOMAIN_EMOJI = {
"career": "πŸ’Ό", "finances": "πŸ’°", "relationships": "❀️",
"physical_health": "πŸ’ͺ", "mental_wellbeing": "🧠", "time": "πŸ“…",
}
# Metrics where HIGH = BAD (inverted color logic)
INVERTED_METRICS = {"stress_level", "debt_pressure", "workload", "commute_burden", "admin_overhead"}
def _metric_color(key: str, val: float) -> str:
"""Return CSS color: inverted for 'bad-when-high' metrics."""
sub = key.split(".")[-1]
if sub in INVERTED_METRICS:
return "#f87171" if val > 70 else ("#facc15" if val >= 40 else "#4ade80")
return "#4ade80" if val > 70 else ("#facc15" if val >= 40 else "#f87171")
def metrics_html(flat: dict, title: str = "", before: dict = None) -> str:
"""Render metrics as coloured progress bars.
If `before` is supplied, metrics that changed >1 pt show ↑/↓ + delta.
"""
domains = ["career", "finances", "relationships", "physical_health", "mental_wellbeing", "time"]
rows = []
if title:
rows.append(f"<h3 style='margin:0 0 8px;font-size:14px;color:#aaa'>{title}</h3>")
for dom in domains:
emoji = DOMAIN_EMOJI[dom]
rows.append(f"<div style='margin:6px 0 2px;font-size:12px;font-weight:700;color:#ccc'>{emoji} {dom.upper()}</div>")
sub = {k: v for k, v in flat.items() if k.startswith(dom + ".")}
for key, val in sub.items():
name = key.split(".")[1].replace("_", " ")