|
| 1 | +"""Smoke: VADER → user_mood EMA hook + MOOD_CONGRUENT_RERANK delta. |
| 2 | +
|
| 3 | +End-to-end signal path on a stub store (no live PG required): |
| 4 | +
|
| 5 | + 1. remember() with a positive-valence message → user_mood drifts positive |
| 6 | + 2. remember() with a negative-valence message → user_mood drifts back |
| 7 | + 3. mood_congruent_rerank with the EMA-driven mood produces score deltas |
| 8 | + vs the same call with user_mood=None |
| 9 | + 4. CORTEX_ABLATE_MOOD_CONGRUENT_RERANK=1 short-circuits (identity) |
| 10 | +
|
| 11 | +Run: |
| 12 | + uv run python tasks/smoke_user_mood_ema.py |
| 13 | +""" |
| 14 | + |
| 15 | +from __future__ import annotations |
| 16 | + |
| 17 | +import os |
| 18 | + |
| 19 | +from mcp_server.core.recall_pipeline import mood_congruent_rerank |
| 20 | +from mcp_server.handlers.remember_helpers import ( |
| 21 | + MOOD_EMA_ALPHA, |
| 22 | + update_user_mood_ema, |
| 23 | +) |
| 24 | +from mcp_server.shared.vader import vader_compound |
| 25 | + |
| 26 | + |
| 27 | +class _MoodStore: |
| 28 | + def __init__(self) -> None: |
| 29 | + self.valence: float | None = None |
| 30 | + |
| 31 | + def get_user_mood(self) -> float | None: |
| 32 | + return self.valence |
| 33 | + |
| 34 | + def set_user_mood(self, valence: float, arousal: float = 0.0) -> None: |
| 35 | + self.valence = max(-1.0, min(1.0, float(valence))) |
| 36 | + |
| 37 | + |
| 38 | +def _candidates() -> list[dict]: |
| 39 | + """Five candidates in increasing emotional_valence.""" |
| 40 | + return [ |
| 41 | + { |
| 42 | + "memory_id": i, |
| 43 | + "content": f"mem {i}", |
| 44 | + "score": 1.0 / (i + 1), |
| 45 | + "heat": 0.5, |
| 46 | + "tags": [], |
| 47 | + "domain": "smoke", |
| 48 | + "created_at": "2026-04-30T00:00:00Z", |
| 49 | + "emotional_valence": v, |
| 50 | + } |
| 51 | + for i, v in enumerate([-0.9, -0.5, 0.0, +0.5, +0.9]) |
| 52 | + ] |
| 53 | + |
| 54 | + |
| 55 | +def main() -> None: |
| 56 | + print(f"MOOD_EMA_ALPHA = {MOOD_EMA_ALPHA}") |
| 57 | + store = _MoodStore() |
| 58 | + |
| 59 | + pos_msg = "Wonderful breakthrough! I am thrilled and delighted with this success." |
| 60 | + neg_msg = "This is terrible. I hate this awful broken garbage. Frustrated and angry." |
| 61 | + |
| 62 | + print(f"\n[1] vader_compound(positive) = {vader_compound(pos_msg):+.4f}") |
| 63 | + print(f" vader_compound(negative) = {vader_compound(neg_msg):+.4f}") |
| 64 | + |
| 65 | + print("\n[2] EMA progression — positive then negative messages:") |
| 66 | + print(f" initial mood: {store.valence}") |
| 67 | + new = update_user_mood_ema(pos_msg, source="user", store=store) |
| 68 | + print(f" after positive (user): {new:+.4f}") |
| 69 | + new = update_user_mood_ema(pos_msg, source="user", store=store) |
| 70 | + print(f" after positive (user): {new:+.4f}") |
| 71 | + new = update_user_mood_ema(neg_msg, source="user", store=store) |
| 72 | + print(f" after negative (user): {new:+.4f}") |
| 73 | + |
| 74 | + print("\n[3] Source gating — non-user sources do NOT update mood:") |
| 75 | + pre = store.valence |
| 76 | + for src in ("tool", "consolidation", "import", "session"): |
| 77 | + result = update_user_mood_ema(pos_msg, source=src, store=store) |
| 78 | + print(f" source={src:14s} → returned {result}, mood still {store.valence:+.4f}") |
| 79 | + assert store.valence == pre, "non-user source must not mutate mood" |
| 80 | + |
| 81 | + print("\n[4] Drive mood strongly positive for rerank delta test:") |
| 82 | + for _ in range(5): |
| 83 | + update_user_mood_ema(pos_msg, source="user", store=store) |
| 84 | + print(f" mood after 5 positive iters: {store.valence:+.4f}") |
| 85 | + |
| 86 | + cands = _candidates() |
| 87 | + baseline = mood_congruent_rerank(cands, user_mood=None) |
| 88 | + active = mood_congruent_rerank(cands, user_mood=store.valence) |
| 89 | + base_scores = {c["memory_id"]: c["score"] for c in baseline} |
| 90 | + active_scores = {c["memory_id"]: c["score"] for c in active} |
| 91 | + |
| 92 | + print("\n[5] mood_congruent_rerank deltas (positive mood):") |
| 93 | + print(f" {'id':>3} {'valence':>8} {'baseline':>10} {'active':>10} {'Δ':>10}") |
| 94 | + for cid in sorted(base_scores): |
| 95 | + v = next(c["emotional_valence"] for c in cands if c["memory_id"] == cid) |
| 96 | + bs, as_ = base_scores[cid], active_scores[cid] |
| 97 | + print(f" {cid:>3} {v:+8.2f} {bs:>10.6f} {as_:>10.6f} {as_-bs:>+10.6f}") |
| 98 | + |
| 99 | + delta_congruent = active_scores[4] - base_scores[4] # high +valence |
| 100 | + delta_incongruent = active_scores[0] - base_scores[0] # high -valence |
| 101 | + print( |
| 102 | + f"\n Δ congruent (id=4, valence +0.9): {delta_congruent:+.6f}" |
| 103 | + f"\n Δ incongruent(id=0, valence -0.9): {delta_incongruent:+.6f}" |
| 104 | + ) |
| 105 | + assert delta_congruent > delta_incongruent, "mood-congruent must gain more" |
| 106 | + assert base_scores != active_scores, "rerank must produce a non-identity delta" |
| 107 | + print(" ✓ mood-congruent candidate gains MORE than incongruent") |
| 108 | + print(" ✓ rerank produces score deltas (NOT identity)") |
| 109 | + |
| 110 | + print("\n[6] Ablation symmetry — CORTEX_ABLATE_MOOD_CONGRUENT_RERANK=1:") |
| 111 | + abl_store = _MoodStore() |
| 112 | + os.environ["CORTEX_ABLATE_MOOD_CONGRUENT_RERANK"] = "1" |
| 113 | + try: |
| 114 | + result = update_user_mood_ema(pos_msg, source="user", store=abl_store) |
| 115 | + assert result is None, "EMA write must be skipped when ablated" |
| 116 | + assert abl_store.valence is None, "store must be untouched" |
| 117 | + print(f" EMA hook returned {result}, store.valence = {abl_store.valence}") |
| 118 | + # And rerank itself short-circuits |
| 119 | + active_abl = mood_congruent_rerank(cands, user_mood=+0.7) |
| 120 | + assert active_abl == cands, "rerank must be identity when ablated" |
| 121 | + print(" ✓ rerank stage returns identity when ablated") |
| 122 | + finally: |
| 123 | + os.environ.pop("CORTEX_ABLATE_MOOD_CONGRUENT_RERANK", None) |
| 124 | + |
| 125 | + print("\n✓ smoke OK — VADER → user_mood EMA hook is wired and signal-fed.") |
| 126 | + |
| 127 | + |
| 128 | +if __name__ == "__main__": |
| 129 | + main() |
0 commit comments