From ebfa50bf0344683ca7862486bdd79ddeb7a6a3d0 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Mon, 15 Jun 2026 22:54:46 +0200 Subject: [PATCH] Refactor chat memory context loading and implement previous session history retrieval --- backend/chat_memory.py | 154 ++++++++++++++++++++++++++++++++++------- 1 file changed, 130 insertions(+), 24 deletions(-) diff --git a/backend/chat_memory.py b/backend/chat_memory.py index 34d7431..0fcfda0 100644 --- a/backend/chat_memory.py +++ b/backend/chat_memory.py @@ -556,34 +556,16 @@ def _dedupe_hits(candidates: Sequence[Dict[str, Any]], top_k: int, *, minimum_sc return hits -def build_chat_memory_context( - db: Session, - prompt: str, +def _context_payload_from_hits( + hits: Sequence[Dict[str, Any]], *, - exclude_session_id: Optional[str] = None, - top_k: int = DEFAULT_MEMORY_TOP_K, - context_character_budget: int = DEFAULT_MEMORY_CONTEXT_CHARS, + context_character_budget: int, + intro: str, ) -> Dict[str, Any]: - tokens = _query_tokens(prompt) - if not tokens: - return {"context_block": "", "sources": [], "hits": []} - - candidate_limit = max(80, top_k * 24) - candidates = _load_fts_candidates(db, tokens, exclude_session_id=exclude_session_id, limit=candidate_limit) - if len(candidates) < top_k: - candidates.extend(_load_table_candidates(db, tokens, exclude_session_id=exclude_session_id, limit=max(300, candidate_limit))) - if len(candidates) < top_k: - candidates.extend(_load_live_candidates(db, tokens, exclude_session_id=exclude_session_id, limit=1500)) - - hits = _dedupe_hits(candidates, max(1, top_k)) if not hits: return {"context_block": "", "sources": [], "hits": []} - header = ( - "\n" - "Relevant excerpts from previous chats. Use these only when they help answer the current request; " - "they may be outdated and should not override explicit current context or fresh retrieved evidence." - ) + header = f"\n{intro}" blocks = [header] sources: List[Dict[str, Any]] = [] remaining = max(600, int(context_character_budget)) - len(header) - len("") @@ -617,4 +599,128 @@ def build_chat_memory_context( if remaining <= 0: break blocks.append("") - return {"context_block": "\n\n".join(blocks), "sources": sources, "hits": hits[:len(sources)]} + return {"context_block": "\n\n".join(blocks), "sources": sources, "hits": list(hits[:len(sources)])} + + +def _load_previous_session_hits( + db: Session, + *, + exclude_session_id: Optional[str], + limit: int, +) -> List[Dict[str, Any]]: + current_created_at = None + if exclude_session_id: + try: + current_created_at = db.execute( + text("SELECT created_at FROM chat_sessions WHERE session_id = :session_id"), + {"session_id": exclude_session_id}, + ).scalar() + except Exception: + current_created_at = None + + try: + previous_session_id = db.execute(text( + """ + SELECT t.session_id + FROM chat_memory_turns t + JOIN chat_sessions s ON s.session_id = t.session_id + WHERE (:exclude_session_id IS NULL OR t.session_id != :exclude_session_id) + AND (:current_created_at IS NULL OR s.created_at < :current_created_at) + GROUP BY t.session_id, s.created_at + ORDER BY s.created_at DESC + LIMIT 1 + """ + ), { + "exclude_session_id": exclude_session_id, + "current_created_at": current_created_at, + }).scalar() + except Exception: + previous_session_id = None + if not previous_session_id: + return [] + + try: + rows = db.execute(text( + """ + SELECT + turn_key, session_id, session_name, user_message_id, assistant_message_id, + user_message_row_id, assistant_message_row_id, user_content, assistant_content, + attachment_summary, search_text, created_at + FROM chat_memory_turns + WHERE session_id = :session_id + ORDER BY assistant_message_row_id ASC + LIMIT :limit + """ + ), { + "session_id": previous_session_id, + "limit": max(1, limit), + }).mappings().all() + except Exception: + return [] + hits: List[Dict[str, Any]] = [] + for row in rows: + item = _row_mapping(row) + item["_score"] = 1.0 + hits.append(item) + return hits + + +def build_chat_memory_context( + db: Session, + prompt: str, + *, + exclude_session_id: Optional[str] = None, + top_k: int = DEFAULT_MEMORY_TOP_K, + context_character_budget: int = DEFAULT_MEMORY_CONTEXT_CHARS, +) -> Dict[str, Any]: + if _LAST_CONVERSATION_RE.search(str(prompt or "")): + hits = _load_previous_session_hits(db, exclude_session_id=exclude_session_id, limit=max(1, top_k)) + return _context_payload_from_hits( + hits, + context_character_budget=context_character_budget, + intro=( + "Most recent previous chat. Use this to answer questions about the last or previous conversation; " + "do not treat it as fresh external evidence." + ), + ) + + core_tokens = _query_tokens(prompt) + if not core_tokens: + return {"context_block": "", "sources": [], "hits": []} + tokens = _expanded_query_tokens(core_tokens) + minimum_score = _minimum_memory_score(len(core_tokens)) + + candidate_limit = max(80, top_k * 24) + candidates = _load_fts_candidates( + db, + tokens, + exclude_session_id=exclude_session_id, + limit=candidate_limit, + query_token_count=len(core_tokens), + ) + if len(candidates) < top_k: + candidates.extend(_load_table_candidates( + db, + tokens, + exclude_session_id=exclude_session_id, + limit=max(300, candidate_limit), + query_token_count=len(core_tokens), + )) + if len(candidates) < top_k: + candidates.extend(_load_live_candidates( + db, + tokens, + exclude_session_id=exclude_session_id, + limit=1500, + query_token_count=len(core_tokens), + )) + + hits = _dedupe_hits(candidates, max(1, top_k), minimum_score=minimum_score) + return _context_payload_from_hits( + hits, + context_character_budget=context_character_budget, + intro=( + "Relevant excerpts from previous chats. Use these only when they help answer the current request; " + "they may be outdated and should not override explicit current context or fresh retrieved evidence." + ), + )