bfb81428ab
Complete the consolidation pipeline: summaries -> profile + eras -> narrative.
- memory: eras table (per-month digests) + Era, summaries_by_month, store_era,
list_eras, recall_eras; narrative table + set/get_narrative
- lyra/era.py (lyra-era): groups session gists by the month the session occurred
(real timestamps) and map-reduces each month into a "what was happening" digest
- lyra/narrative.py (lyra-narrative): distills profile + recent eras into the
current arc/trends/callbacks ("remember when…", "you're trending toward…")
- chat.build_messages injects the narrative alongside the profile
Verified on the real corpus: 17 monthly eras (Dec 2024-Jun 2026) + a narrative
that surfaces specific callbacks (the $573 Hollywood session, 4 years sober).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
84 lines
3.0 KiB
Python
84 lines
3.0 KiB
Python
"""Era rollups: per-month "what was happening" digests (consolidation step 3).
|
|
|
|
Groups session gists by the calendar month the session occurred (from real
|
|
exchange timestamps) and map-reduces each month into one digest. These are the
|
|
temporal memory tier — they answer "what was going on last December" and feed
|
|
the narrative engine. Runs on the consolidation backend (MI50 in steady state).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
|
|
from lyra import config, llm, logbus, memory
|
|
from lyra.llm import Backend, Message
|
|
|
|
BATCH_CHARS = 18000
|
|
|
|
_PROMPT = """You are writing a monthly memory digest about Brian from the session \
|
|
summaries below (all from the same month). Capture: what he was focused on (poker \
|
|
and otherwise), notable events/results/decisions, recurring themes, and his mood \
|
|
and arc across the month. Third person, referring to him as "Brian". 5-10 \
|
|
sentences. This is a memory record, not a reply. No preamble."""
|
|
|
|
_MERGE_PROMPT = """Merge these partial monthly digests (same month) into one \
|
|
coherent digest about Brian for that month. Keep it tight, 5-10 sentences, no \
|
|
repetition. Third person."""
|
|
|
|
|
|
def _batch_texts(texts: list[str], budget: int) -> list[str]:
|
|
blocks, buf, size = [], [], 0
|
|
for t in texts:
|
|
if size + len(t) > budget and buf:
|
|
blocks.append("\n\n".join(buf))
|
|
buf, size = [], 0
|
|
buf.append(t)
|
|
size += len(t)
|
|
if buf:
|
|
blocks.append("\n\n".join(buf))
|
|
return blocks
|
|
|
|
|
|
def _call(prompt: str, body: str, backend: Backend) -> str:
|
|
messages: list[Message] = [
|
|
{"role": "system", "content": prompt},
|
|
{"role": "user", "content": body},
|
|
]
|
|
return llm.complete(messages, backend=backend)
|
|
|
|
|
|
def _digest_month(gists: list[str], backend: Backend) -> str:
|
|
"""Map-reduce a month's session gists into one digest."""
|
|
blocks = _batch_texts(gists, BATCH_CHARS)
|
|
partials = [_call(_PROMPT, b, backend) for b in blocks]
|
|
while len(partials) > 1:
|
|
partials = [_call(_MERGE_PROMPT, g, backend) for g in _batch_texts(partials, BATCH_CHARS)]
|
|
return partials[0]
|
|
|
|
|
|
def rebuild_eras(backend: Backend | None = None) -> dict:
|
|
"""(Re)build a digest for every month that has session gists."""
|
|
backend = backend or config.load().summary_backend
|
|
by_month = memory.summaries_by_month()
|
|
months = 0
|
|
for month in sorted(by_month):
|
|
digest = _digest_month(by_month[month], backend)
|
|
memory.store_era(month, digest, len(by_month[month]))
|
|
months += 1
|
|
logbus.log("info", "era built", month=month, sessions=len(by_month[month]))
|
|
report = {"months": months}
|
|
logbus.log("info", "eras complete", **report)
|
|
return report
|
|
|
|
|
|
def main() -> int:
|
|
report = rebuild_eras()
|
|
if not report["months"]:
|
|
print("No summaries yet — run lyra-summarize first.")
|
|
return 1
|
|
for era in memory.list_eras():
|
|
print(f"\n## {era.month} ({era.session_count} sessions)\n{era.content}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|