160 lines
5.4 KiB
Python
160 lines
5.4 KiB
Python
|
|
"""
|
||
|
|
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 perceive→think→act 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()
|