File size: 3,580 Bytes
93dfcf3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env python
"""Long-term memory for the Memory Chatbot (#16).

The architecture, in four moves:
  1. EXTRACT — after each exchange, an LLM pulls durable facts about the user (name, prefs, projects).
  2. STORE   — facts are written to SQLite, keyed by user, deduped. This is what survives across sessions.
  3. RETRIEVE— on each new turn, the user's stored facts are loaded.
  4. INJECT  — they're placed in the system prompt so the bot "remembers you."

This is the distinction the rung teaches: the short-term conversation buffer lives in the request;
long-term memory lives here, in a store that outlives the session.
"""
import os
import sqlite3
import threading

import llm

DB = os.environ.get("MEMBOT_DB", os.path.join(os.path.dirname(os.path.abspath(__file__)), "memory.db"))
_lock = threading.Lock()

EXTRACT_SYS = (
    "You maintain a chatbot's LONG-TERM memory of a user. From the latest exchange, extract only DURABLE "
    "facts worth remembering across future sessions — the user's name, stable preferences, ongoing projects, "
    "role, constraints, or personal context they'd expect you to recall later. Do NOT store the assistant's "
    "words, one-off questions, greetings, or transient chit-chat. Return a JSON array of short first-person-"
    "about-the-user fact strings (e.g. [\"Name is Alex\", \"Prefers Python\", \"Building a bakery website\"]). "
    "If nothing durable was shared, return []."
)


def _conn():
    c = sqlite3.connect(DB)
    c.execute("""CREATE TABLE IF NOT EXISTS facts (
        id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, fact TEXT NOT NULL,
        created_at TEXT DEFAULT (datetime('now')))""")
    return c


def get_facts(user_id: str) -> list:
    with _lock, _conn() as c:
        rows = c.execute("SELECT id, fact FROM facts WHERE user_id=? ORDER BY id", (user_id,)).fetchall()
    return [{"id": r[0], "fact": r[1]} for r in rows]


def add_fact(user_id: str, fact: str):
    fact = (fact or "").strip()
    if not fact:
        return
    with _lock, _conn() as c:
        exists = c.execute("SELECT 1 FROM facts WHERE user_id=? AND lower(fact)=lower(?)",
                           (user_id, fact)).fetchone()
        if not exists:
            c.execute("INSERT INTO facts (user_id, fact) VALUES (?, ?)", (user_id, fact))


def delete_fact(user_id: str, fact_id: int):
    with _lock, _conn() as c:
        c.execute("DELETE FROM facts WHERE user_id=? AND id=?", (user_id, fact_id))


def clear(user_id: str):
    with _lock, _conn() as c:
        c.execute("DELETE FROM facts WHERE user_id=?", (user_id,))


def remember_from_exchange(user_id: str, user_msg: str, assistant_msg: str) -> list:
    """EXTRACT + STORE. Returns the list of newly-added facts (for UI feedback)."""
    prompt = f"USER SAID: {user_msg}\nASSISTANT REPLIED: {assistant_msg}\n\nExtract durable facts about the user."
    facts = llm.extract_json(EXTRACT_SYS, prompt)
    if not isinstance(facts, list):
        return []
    before = {f["fact"].lower() for f in get_facts(user_id)}
    added = []
    for f in facts:
        if isinstance(f, str) and f.strip() and f.strip().lower() not in before:
            add_fact(user_id, f.strip())
            added.append(f.strip())
    return added


def memory_block(user_id: str) -> str:
    """RETRIEVE + format for INJECTion into the system prompt."""
    facts = get_facts(user_id)
    if not facts:
        return ""
    lines = "\n".join(f"- {f['fact']}" for f in facts)
    return f"\n\nWhat you remember about this user from past sessions:\n{lines}\n"