feat: tiered, compacting memory (phase 1.5)
Older sessions fade to a general idea; details stay retrievable.
- memory: summaries table (one compacted gist per session, embedded), plus
store_summary/get_summary/recall_summaries and unsummarized_count (tracks
exchanges newer than the current summary)
- lyra/summary.py: summarize_session compacts a session's raw turns into a
third-person gist (default SUMMARY_BACKEND=local, so compaction is free);
maybe_summarize re-summarizes once SUMMARIZE_AFTER new turns accumulate
- chat.build_messages now layers context in tiers: persona -> gists of other
sessions -> a few sharp raw cross-session details -> current session raw
turns -> new message; respond() compacts the session after each turn
- web: POST /sessions/{id}/summarize to compact on demand
- summarization activity surfaces in the live log
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
"""Session summarization: compact a session's raw exchanges into a stored gist.
|
||||
|
||||
This is the compaction half of the tiered memory. Raw exchanges stay for detail
|
||||
recall; the summary is what surfaces when an *older* session is recalled later —
|
||||
"a month ago is a general idea," per the design.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from lyra import config, llm, logbus, memory
|
||||
from lyra.llm import Backend
|
||||
|
||||
# Re-summarize a session once it has accumulated this many new raw exchanges
|
||||
# beyond what its current summary covers.
|
||||
SUMMARIZE_AFTER = 20
|
||||
|
||||
_PROMPT = """You are compacting a conversation into a long-term memory record \
|
||||
(not replying to anyone). Write a concise gist of the session below: what was \
|
||||
discussed, key decisions or outcomes, concrete specifics worth keeping (names, \
|
||||
places, numbers, hands), and the user's apparent mood/state. Third person, \
|
||||
referring to the user as "Brian". 4-8 sentences. No preamble."""
|
||||
|
||||
|
||||
def _transcript(exchanges: list[memory.Exchange]) -> str:
|
||||
return "\n".join(f"{ex.role}: {ex.content}" for ex in exchanges)
|
||||
|
||||
|
||||
def summarize_session(session_id: str, backend: Backend | None = None) -> str | None:
|
||||
"""(Re)generate and store the gist for a session. Returns the summary text.
|
||||
|
||||
Returns None if the session has no exchanges. The summarizer defaults to the
|
||||
local backend so routine compaction stays free.
|
||||
"""
|
||||
exchanges = memory.history(session_id)
|
||||
if not exchanges:
|
||||
return None
|
||||
|
||||
backend = backend or config.load().summary_backend
|
||||
messages = [
|
||||
{"role": "system", "content": _PROMPT},
|
||||
{"role": "user", "content": _transcript(exchanges)},
|
||||
]
|
||||
gist = llm.complete(messages, backend=backend)
|
||||
|
||||
last_id = exchanges[-1].id
|
||||
memory.store_summary(session_id, gist, last_id)
|
||||
logbus.log(
|
||||
"info", "summarized session", session=session_id,
|
||||
exchanges=len(exchanges), backend=backend,
|
||||
)
|
||||
return gist
|
||||
|
||||
|
||||
def maybe_summarize(session_id: str, backend: Backend | None = None) -> None:
|
||||
"""Summarize the session if enough new turns have accumulated since last time."""
|
||||
if memory.unsummarized_count(session_id) >= SUMMARIZE_AFTER:
|
||||
summarize_session(session_id, backend=backend)
|
||||
Reference in New Issue
Block a user