lectio / backend /app /core /memory.py
adesh01's picture
feat: Lectio — HF Space deployment
14fda63
Raw
History Blame Contribute Delete
3.44 kB
"""Conversation memory: persist turns, load via window or semantic retrieval.
Strategy selection:
session has ≤ SEMANTIC_SWITCH_AT turns → sliding window (last WINDOW_TURNS)
session exceeds SEMANTIC_SWITCH_AT → semantic: embed query, top-k past turns
Denomination switch mid-session injects a system note so stale framing
assumptions are not carried forward.
"""
from pgvector.psycopg import Vector
from app.config import get_settings
from app.core.db import get_conn
from app.core.embeddings import embed_passages, embed_query
from app.logging_config import get_logger
log = get_logger(__name__)
_s = get_settings()
_INSERT = """
INSERT INTO conversations (session_id, role, content, denomination, embedding)
VALUES (%s, %s, %s, %s, %s)
"""
_COUNT = """
SELECT count(*) FROM conversations WHERE session_id = %s
"""
_WINDOW = """
SELECT role, content, denomination FROM conversations
WHERE session_id = %s
ORDER BY created_at DESC
LIMIT %s
"""
_SEMANTIC = """
SELECT role, content, denomination,
1 - (embedding <=> %s) AS similarity
FROM conversations
WHERE session_id = %s
AND embedding IS NOT NULL
ORDER BY embedding <=> %s
LIMIT %s
"""
_LAST_DENOMINATION = """
SELECT denomination FROM conversations
WHERE session_id = %s
ORDER BY created_at DESC
LIMIT 1
"""
def save_turn(session_id: str, role: str, content: str, denomination: str) -> None:
"""Persist one turn with embedding."""
vec = Vector(embed_passages([content])[0])
with get_conn() as conn:
conn.execute(_INSERT, (session_id, role, content, denomination, vec))
conn.commit()
def load_memory(
session_id: str, query: str, current_denomination: str
) -> tuple[list[dict], str, str]:
"""Return (turns, strategy_used, denomination_guard_note).
turns: list of {role, content} dicts ready to inject into prompt.
strategy_used: 'window' | 'semantic'
denomination_guard_note: non-empty string when denomination changed mid-session.
"""
with get_conn() as conn:
total: int = conn.execute(_COUNT, (session_id,)).fetchone()[0]
# Denomination switch guard
guard_note = ""
if total > 0:
prev_denom = conn.execute(
_LAST_DENOMINATION, (session_id,)
).fetchone()
if prev_denom and prev_denom[0] != current_denomination:
guard_note = (
f"[System: the user has switched from {prev_denom[0]} to "
f"{current_denomination} tradition. Update canon and framing accordingly.]"
)
if total <= _s.memory_semantic_switch_at:
rows = conn.execute(
_WINDOW, (session_id, _s.memory_window_turns)
).fetchall()
turns = [{"role": r[0], "content": r[1]} for r in reversed(rows)]
strategy = "window"
else:
vec = Vector(embed_query(query))
rows = conn.execute(
_SEMANTIC, (vec, session_id, vec, _s.rag_top_k)
).fetchall()
turns = [{"role": r[0], "content": r[1]} for r in rows]
strategy = "semantic"
log.info(
"memory.loaded",
session_id=session_id,
total_turns=total,
loaded=len(turns),
strategy=strategy,
denom_switched=bool(guard_note),
)
return turns, strategy, guard_note