"""Conversation memory: rolling summary plus a recent-message window. Older turns get compressed into a summary stored on the session, so long conversations keep context without unbounded prompt growth. """ import logging from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.config import get_settings from app.llm.registry import get_provider from app.models import ChatSession, Message logger = logging.getLogger("synapse.agent.memory") async def build_history( db: AsyncSession, session: ChatSession ) -> list[dict[str, str]]: """Return chat history for the prompt: summary context + recent window.""" settings = get_settings() result = await db.execute( select(Message) .where(Message.session_id == session.id) .order_by(Message.id.desc()) .limit(settings.history_window_messages) ) recent = list(reversed(result.scalars().all())) history: list[dict[str, str]] = [] for message in recent: history.append({"role": message.role, "content": message.content}) return history async def maybe_summarize(db: AsyncSession, session_id: str) -> None: """Compress older messages into the session summary when the thread grows. Runs after a reply has been sent, so latency is never user-visible. """ settings = get_settings() provider = get_provider() session = await db.get(ChatSession, session_id) if session is None: return result = await db.execute( select(Message).where(Message.session_id == session.id).order_by(Message.id) ) messages = result.scalars().all() unsummarized = [m for m in messages if m.id > session.summarized_until] if len(unsummarized) < settings.summarize_after_messages: return keep = settings.history_window_messages // 2 to_compress = unsummarized[:-keep] if keep else unsummarized if not to_compress: return transcript = "\n".join( f"{m.role.upper()}: {m.content[:800]}" for m in to_compress ) prompt = ( "Update the running summary of this conversation. Keep key facts, " "decisions, names, numbers and open questions. Be concise (under 250 " "words) and neutral.\n\n" f"Existing summary:\n{session.summary or '(none)'}\n\n" f"New messages:\n{transcript}" ) try: summary = await provider.complete( [{"role": "user", "content": prompt}], model=settings.utility_model, temperature=0.2, ) except Exception as exc: logger.warning("summarization failed: %s", exc) return session.summary = summary.strip()[:4000] session.summarized_until = to_compress[-1].id await db.commit() logger.info("summarized session %s up to message %s", session.id, session.summarized_until)