memory-chatbot / store.py
BonusLockSMith's picture
Upload folder using huggingface_hub
93dfcf3 verified
Raw History Blame Contribute Delete
3.58 kB
#!/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"