CRag / rag_system /memory.py
quantumbit's picture
Upload folder using huggingface_hub
4796bbf verified
Raw
History Blame Contribute Delete
2.63 kB
# memory.py
"""
Statelesss-server-friendly conversation memory.
History is passed from the client on each request (no server-side session state)
Compression kicks in when history exceeds the token budget
"""
import logging
import tiktoken
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
from .config import get_settings
from .prompt import STANDALONE_QUESTION_PROMPT
logger = logging.getLogger(__name__)
settings = get_settings()
_enc = tiktoken.encoding_for_model("gpt-4o")
def count_tokens(text: str) -> int:
return len(_enc.encode(text))
def build_lc_messages(
history: list[dict],
system_prompt: str,
) -> list:
"""Convert raw history dicts to Langchain message objects"""
messages = [SystemMessage(content=system_prompt)]
for turn in history:
if turn["role"] == "user":
messages.append(HumanMessage(content=turn["content"]))
else:
messages.append(AIMessage(content=turn["content"]))
return messages
def trim_history_to_budget(
history: list[dict],
max_tokens: int = None,
) -> list[dict]:
"""
Sliding window: keep the MOST recent turns that fit the token budget.
Always keeps at minimum the last 2 turns (1 exchange)
"""
budget = max_tokens or settings.context_window_tokens // 3 # 1/3 of budget for history
trimmed: list[dict] = []
total = 0
for turn in reversed(history[-settings.max_history_turns * 2:]): # multiplied by 2 to take both user and AIResponse
tokens = count_tokens(turn["content"])
if total + tokens > budget and len(trimmed) >= 2: # checks if token exceeds budget limit or trimmed is more than 2, to avoid only user or ai going with sys prompt
break
trimmed.insert(0,turn)
total += tokens
return trimmed
async def resolve_standalone_question(
question: str,
history: list[dict],
llm: ChatOpenAI
) -> str:
"""
If conversation history exists, use LLM to rewrite the followup question
as a self-contained query (critical for multi-turn retrieval accuracy)
"""
if not history:
return question
history_str = "\n".join(
f"{t['role'].capitalize()}: {t['content']}" for t in history[-6:]
)
prompt = STANDALONE_QUESTION_PROMPT.format(
history=history_str,
question=question
)
response = await llm.ainvoke([HumanMessage(content=prompt)])
standalone = response.content.strip()
logger.debug(f"Standalone question: '{standalone}")
return standalone
print("[memory] Module ready")