synapse / backend /app /agent /memory.py
adwitiyashukla's picture
pushed Synapse, an agentic AI assistant with hybrid RAG
b2c1c67
Raw
History Blame Contribute Delete
2.86 kB
"""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)