File size: 4,183 Bytes
74e8a7b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
"""
Conversation memory helpers (LangChain ConversationBufferWindowMemory with
a manually enforced window size) and post-response memory hygiene.
"""
from langchain_classic.memory import ConversationBufferWindowMemory

from src.utils.config import MEMORY_WINDOW_K, groq_client

# NOTE: matches notebook's current (possibly unintentional) mismatch โ€” the
# memory object itself is built with MEMORY_WINDOW_K (5), but this local
# trim constant is 3, making 3 the effective operative window. See the
# config.py note above โ€” flag for confirmation before treating as final.
_SAVE_TRIM_K = 3


def get_empty_memory() -> ConversationBufferWindowMemory:
    return ConversationBufferWindowMemory(k=MEMORY_WINDOW_K, return_messages=True, memory_key="history")


def save_to_memory(memory: ConversationBufferWindowMemory, user_msg: str, assistant_msg: str) -> None:
    """Save a turn and manually enforce the (tighter) trim window."""
    memory.save_context({"input": user_msg}, {"output": assistant_msg})
    limit = _SAVE_TRIM_K * 2
    if len(memory.chat_memory.messages) > limit:
        memory.chat_memory.messages = memory.chat_memory.messages[-limit:]


def history_as_messages(memory: ConversationBufferWindowMemory) -> list[dict]:
    """Convert stored memory into OpenAI-style chat messages."""
    chat_history = memory.load_memory_variables({})["history"]
    return [
        {"role": ("user" if m.type == "human" else "assistant"), "content": m.content}
        for m in chat_history
    ]


# โ”€โ”€ NEW: memory hygiene โ€” strip personalization before saving โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
_MEMORY_STRIP_SYSTEM = """You are given an academic advisor's answer to a student.
Your task: return ONLY the factual/informational part of the answer โ€” remove
any section that personalizes the answer to the specific student (GPA checks,
"ูŠู†ุงุณุจูƒ"/"ูŠุชูˆุงูู‚ ู…ุน ุงู‡ุชู…ุงู…ุงุชูƒ" framing, eligibility verdicts, program
recommendations, "ุงู„ุงุฎุชูŠุงุฑ ุงู„ุฃู…ุซู„" or similar closing recommendations).

Rules:
- If the answer has NO personalization content at all, return it unchanged.
- Do NOT summarize or paraphrase the factual content โ€” keep it word-for-word.
- Only remove personalization/recommendation content, nothing else.
- Return ONLY the trimmed answer text, no explanation, no markdown fences.
"""


def strip_personalization_tail(response: str) -> str:
    """
    Before saving an assistant turn to memory, remove personalization/
    recommendation content so a fit-related answer doesn't bias the tone
    of unrelated future turns still inside the k-window. The full
    personalized answer is still shown to the student this turn โ€” only
    what gets remembered is trimmed.
    """
    if not response or len(response) < 50:
        return response
    try:
        resp = groq_client.chat.completions.create(
            model="llama-3.1-8b-instant",
            messages=[
                {"role": "system", "content": _MEMORY_STRIP_SYSTEM},
                {"role": "user", "content": response},
            ],
            temperature=0.0,
            max_tokens=len(response) // 2 + 200,
        )
        trimmed = resp.choices[0].message.content.strip()
        if trimmed:
            print(f"[Memory] stripped {len(response) - len(trimmed)} chars of personalization")
            return trimmed
        return response
    except Exception as e:
        print(f"[Memory strip error] {e}")
        return response

def _save_to_memory_background(memory: ConversationBufferWindowMemory, user_msg: str, assistant_msg: str) -> None:
    """
    Runs on a background thread, after the response has already been returned
    to the caller โ€” memory hygiene (personalization stripping + save) doesn't
    need to block the response, since it only affects what future turns see,
    not the current one.
    """
    try:
        cleaned = strip_personalization_tail(assistant_msg)
        save_to_memory(memory, user_msg, cleaned)
    except Exception as e:
        print(f"[Background memory save error] {e}")
        save_to_memory(memory, user_msg, assistant_msg)  # fail safe โ€” save unstripped rather than lose the turn