Files
the-nexus/nexus/experience_store.py

160 lines
5.4 KiB
Python
Raw Normal View History

"""
Nexus Experience Store Embodied Memory
SQLite-backed store for lived experiences only. The model remembers
what it perceived, what it thought, and what it did nothing else.
Each row is one cycle of the perceivethinkact loop.
"""
import sqlite3
import json
import time
from pathlib import Path
from typing import Optional
DEFAULT_DB = Path.home() / ".nexus" / "experience.db"
MAX_CONTEXT_EXPERIENCES = 20 # Recent experiences fed to the model
class ExperienceStore:
def __init__(self, db_path: Optional[Path] = None):
self.db_path = db_path or DEFAULT_DB
self.db_path.parent.mkdir(parents=True, exist_ok=True)
self.conn = sqlite3.connect(str(self.db_path))
self.conn.execute("PRAGMA journal_mode=WAL")
self.conn.execute("PRAGMA synchronous=NORMAL")
self._init_tables()
def _init_tables(self):
self.conn.executescript("""
CREATE TABLE IF NOT EXISTS experiences (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp REAL NOT NULL,
perception TEXT NOT NULL,
thought TEXT,
action TEXT,
action_result TEXT,
cycle_ms INTEGER DEFAULT 0,
session_id TEXT
);
CREATE TABLE IF NOT EXISTS summaries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp REAL NOT NULL,
summary TEXT NOT NULL,
exp_start INTEGER NOT NULL,
exp_end INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_exp_ts
ON experiences(timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_exp_session
ON experiences(session_id);
""")
self.conn.commit()
def record(
self,
perception: str,
thought: Optional[str] = None,
action: Optional[str] = None,
action_result: Optional[str] = None,
cycle_ms: int = 0,
session_id: Optional[str] = None,
) -> int:
"""Record one perceive→think→act cycle."""
cur = self.conn.execute(
"""INSERT INTO experiences
(timestamp, perception, thought, action, action_result,
cycle_ms, session_id)
VALUES (?, ?, ?, ?, ?, ?, ?)""",
(time.time(), perception, thought, action,
action_result, cycle_ms, session_id),
)
self.conn.commit()
return cur.lastrowid
def recent(self, limit: int = MAX_CONTEXT_EXPERIENCES) -> list[dict]:
"""Fetch the most recent experiences for context."""
rows = self.conn.execute(
"""SELECT id, timestamp, perception, thought, action,
action_result, cycle_ms
FROM experiences
ORDER BY timestamp DESC
LIMIT ?""",
(limit,),
).fetchall()
return [
{
"id": r[0],
"timestamp": r[1],
"perception": r[2],
"thought": r[3],
"action": r[4],
"action_result": r[5],
"cycle_ms": r[6],
}
for r in reversed(rows) # Chronological order
]
def format_for_context(self, limit: int = MAX_CONTEXT_EXPERIENCES) -> str:
"""Format recent experiences as natural language for the model."""
experiences = self.recent(limit)
if not experiences:
return "You have no memories yet. This is your first moment."
lines = []
for exp in experiences:
ago = time.time() - exp["timestamp"]
if ago < 60:
when = f"{int(ago)}s ago"
elif ago < 3600:
when = f"{int(ago / 60)}m ago"
else:
when = f"{int(ago / 3600)}h ago"
line = f"[{when}] You perceived: {exp['perception']}"
if exp["thought"]:
line += f"\n You thought: {exp['thought']}"
if exp["action"]:
line += f"\n You did: {exp['action']}"
if exp["action_result"]:
line += f"\n Result: {exp['action_result']}"
lines.append(line)
return "Your recent experiences:\n\n" + "\n\n".join(lines)
def count(self) -> int:
"""Total experiences recorded."""
return self.conn.execute(
"SELECT COUNT(*) FROM experiences"
).fetchone()[0]
def save_summary(self, summary: str, exp_start: int, exp_end: int):
"""Store a compressed summary of a range of experiences.
Used when context window fills distill old memories."""
self.conn.execute(
"""INSERT INTO summaries (timestamp, summary, exp_start, exp_end)
VALUES (?, ?, ?, ?)""",
(time.time(), summary, exp_start, exp_end),
)
self.conn.commit()
def get_summaries(self, limit: int = 5) -> list[dict]:
"""Fetch recent experience summaries."""
rows = self.conn.execute(
"""SELECT id, timestamp, summary, exp_start, exp_end
FROM summaries ORDER BY timestamp DESC LIMIT ?""",
(limit,),
).fetchall()
return [
{"id": r[0], "timestamp": r[1], "summary": r[2],
"exp_start": r[3], "exp_end": r[4]}
for r in reversed(rows)
]
def close(self):
self.conn.close()