Extract durable facts
Not every sentence is worth remembering. "What's the weather?" is transient; "I'm a backend engineer who can't use cloud vendors" is durable. We ask the local model to do the triage and return structured triples. Forcing JSON output (format="json") keeps it parseable.
Add to memory.py:
import json
CHAT_MODEL = "llama3.2"
EXTRACT_PROMPT = """Extract only DURABLE facts about the user from the message.
Durable = stable identity, location, role, preferences, constraints, projects.
Ignore transient chit-chat, questions, and one-off requests.
Return JSON: {{"facts": [{{"predicate": "...", "value": "..."}}]}}
Use short, reusable predicate names (name, city, role, preference, constraint, project).
Message: {msg}"""
def extract_facts(message: str) -> list[dict]:
r = ollama.chat(
model=CHAT_MODEL,
messages=[{"role": "user", "content": EXTRACT_PROMPT.format(msg=message)}],
format="json",
)
try:
return json.loads(r["message"]["content"]).get("facts", [])
except (json.JSONDecodeError, KeyError):
return []Upsert with invalidation
This is the heart of the build. Before inserting a new value for a predicate, we close the current one — stamp its valid_to. The old fact stays in the table (history), but it's no longer valid_to IS NULL, so recall from Step 3 will never surface it again.
from datetime import datetime, timezone
def now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
def write_fact(con, predicate: str, value: str,
session: int, subject: str = "user") -> None:
ts = now_iso()
# 1. retire the current value for this (subject, predicate), if any
con.execute(
"UPDATE facts SET valid_to = ?"
" WHERE subject = ? AND predicate = ? AND valid_to IS NULL",
(ts, subject, predicate),
)
# 2. insert the new current value, with its embedding
emb = to_blob(embed(f"{subject} {predicate} {value}"))
con.execute(
"INSERT INTO facts(subject,predicate,value,embedding,valid_from,valid_to,session)"
" VALUES(?,?,?,?,?,NULL,?)",
(subject, predicate, value, emb, ts, session),
)
con.commit()Note we don't guard against writing an identical value — in practice you'd skip the write if value matches the current one, to avoid churning identical rows. Left out here for clarity; it's a two-line check.
Watch the move happen
if __name__ == "__main__":
con = connect()
write_fact(con, "city", "Toronto", session=1)
write_fact(con, "city", "Berlin", session=8) # the move
print("current:", recall(con, "where do I live?", k=1))
print("history:")
for row in con.execute(
"SELECT value, valid_to FROM facts WHERE predicate='city' ORDER BY id"
):
print(" ", row)$ python memory.py
current: [('user', 'city', 'Berlin')]
history:
('Toronto', '2026-...T...') # closed
('Berlin', None) # currentToronto isn't gone — it's closed. You could answer "where did she used to live?" from history if you wanted. But for "where do I live?", recall only sees Berlin. That's the session-15 update test, passing at the data layer.
Reference: Ollama structured outputs (JSON mode) · ollama-python · Agent Long-Term Memory — §03 Writing it down