d7c258eba0
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>
57 lines
2.2 KiB
Python
57 lines
2.2 KiB
Python
"""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)
|