Files
NexusOS/synapse/memory/store.py
T
jon b0aa0438af Initial commit: NexusOS project + desktop theme baseline
Captures the known-good state after theme consolidation and
consistency reconciliation:
- NexusOS GTK/xfwm4/icon theme assets (assets/themes, management/Mint-Y-Nexus)
- Tightened right-click menus, visible separators, no menu icons
- assets/themes/install-theme.sh: idempotent restore of all wiring
  (symlinks, xfconf xsettings+xfwm4, GTK 3/4 settings.ini)
- .gitignore excludes venv/ollama/models/runtime/db

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 13:39:48 -05:00

380 lines
12 KiB
Python

from pathlib import Path
from typing import Any, Dict, List, Optional
from pydantic import BaseModel
import sqlite3
import json
import os
import time
# -----------------------------
# Models
# -----------------------------
class MemoryItem(BaseModel):
id: str
section: str = "General"
text: str
tags: List[str] = []
class MessageItem(BaseModel):
role: str # "user" or "assistant"
content: str
timestamp: float
model: Optional[str] = None
tokens: Optional[int] = None
class ConversationItem(BaseModel):
id: str
messages: List[MessageItem] = []
created_at: float
updated_at: float
@property
def preview(self) -> str:
for msg in self.messages:
if msg.role == "user":
return msg.content[:80]
return "Empty conversation"
@property
def timestamp(self) -> float:
return self.created_at
# -----------------------------
# Persistent Store
# -----------------------------
class PersistentMemoryStore:
def __init__(self, db_path: Path):
self.db_path = db_path
os.makedirs(self.db_path.parent, exist_ok=True)
self._ensure_tables()
self._cache: Dict[str, MemoryItem] = self._load_all_memory()
# -----------------------------
# Internal helpers
# -----------------------------
def _connect(self):
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=WAL;")
return conn
def _ensure_tables(self):
conn = self._connect()
cur = conn.cursor()
cur.execute("""
CREATE TABLE IF NOT EXISTS memory (
id TEXT PRIMARY KEY,
section TEXT NOT NULL DEFAULT 'General',
text TEXT NOT NULL,
tags TEXT
)
""")
# Migrate: add section column if it doesn't exist yet
try:
cur.execute("ALTER TABLE memory ADD COLUMN section TEXT NOT NULL DEFAULT 'General'")
except Exception:
pass
cur.execute("""
CREATE TABLE IF NOT EXISTS conversations (
id TEXT PRIMARY KEY,
created_at REAL NOT NULL,
updated_at REAL NOT NULL
)
""")
cur.execute("""
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
conversation_id TEXT NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL,
timestamp REAL NOT NULL,
model TEXT,
tokens INTEGER,
FOREIGN KEY (conversation_id) REFERENCES conversations(id)
)
""")
for col, typedef in (("model", "TEXT"), ("tokens", "INTEGER")):
try:
cur.execute(f"ALTER TABLE messages ADD COLUMN {col} {typedef}")
except Exception:
pass
cur.execute("""
CREATE INDEX IF NOT EXISTS idx_messages_conversation_id
ON messages (conversation_id)
""")
cur.execute("""
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)
""")
conn.commit()
conn.close()
# -----------------------------
# Loaders
# -----------------------------
def _load_all_memory(self) -> Dict[str, MemoryItem]:
conn = self._connect()
cur = conn.cursor()
cur.execute("SELECT id, section, text, tags FROM memory")
rows = cur.fetchall()
conn.close()
cache = {}
for row in rows:
try:
tags = json.loads(row["tags"]) if row["tags"] else []
except Exception:
tags = []
cache[row["id"]] = MemoryItem(
id=row["id"],
section=row["section"] or "General",
text=row["text"],
tags=tags,
)
return cache
# -----------------------------
# Memory API
# -----------------------------
def add(self, item: MemoryItem):
self._cache[item.id] = item
conn = self._connect()
try:
cur = conn.cursor()
cur.execute(
"INSERT OR REPLACE INTO memory (id, section, text, tags) VALUES (?, ?, ?, ?)",
(item.id, item.section or "General", item.text, json.dumps(item.tags))
)
conn.commit()
finally:
conn.close()
def update(self, item: MemoryItem):
self.add(item)
def delete(self, item_id: str):
self._cache.pop(item_id, None)
conn = self._connect()
try:
cur = conn.cursor()
cur.execute("DELETE FROM memory WHERE id = ?", (item_id,))
conn.commit()
finally:
conn.close()
def get(self, item_id: str) -> Optional[MemoryItem]:
return self._cache.get(item_id)
def all(self) -> List[MemoryItem]:
return list(self._cache.values())
# -----------------------------
# Conversation API
# -----------------------------
def create_conversation(self, conversation_id: str) -> ConversationItem:
now = time.time()
conn = self._connect()
try:
cur = conn.cursor()
cur.execute(
"INSERT OR IGNORE INTO conversations (id, created_at, updated_at) VALUES (?, ?, ?)",
(conversation_id, now, now)
)
conn.commit()
finally:
conn.close()
return ConversationItem(id=conversation_id, created_at=now, updated_at=now)
def add_message(self, conversation_id: str, role: str, content: str, model: str = None, tokens: int = None):
now = time.time()
conn = self._connect()
try:
cur = conn.cursor()
cur.execute(
"INSERT INTO messages (conversation_id, role, content, timestamp, model, tokens) VALUES (?, ?, ?, ?, ?, ?)",
(conversation_id, role, content, now, model, tokens)
)
cur.execute(
"UPDATE conversations SET updated_at = ? WHERE id = ?",
(now, conversation_id)
)
conn.commit()
finally:
conn.close()
def get_conversation(self, conversation_id: str) -> Optional[ConversationItem]:
conn = self._connect()
cur = conn.cursor()
cur.execute("SELECT * FROM conversations WHERE id = ?", (conversation_id,))
row = cur.fetchone()
if not row:
conn.close()
return None
cur.execute(
"SELECT role, content, timestamp, model, tokens FROM messages WHERE conversation_id = ? ORDER BY timestamp ASC",
(conversation_id,)
)
msg_rows = cur.fetchall()
conn.close()
messages = [MessageItem(role=r["role"], content=r["content"], timestamp=r["timestamp"], model=r["model"], tokens=r["tokens"]) for r in msg_rows]
return ConversationItem(
id=row["id"],
messages=messages,
created_at=row["created_at"],
updated_at=row["updated_at"]
)
def all_conversations(self) -> List[ConversationItem]:
conn = self._connect()
cur = conn.cursor()
cur.execute("""
SELECT c.id, c.created_at, c.updated_at,
m.role, m.content, m.timestamp, m.model, m.tokens
FROM conversations c
LEFT JOIN messages m ON m.conversation_id = c.id
ORDER BY c.updated_at DESC, m.timestamp ASC
""")
rows = cur.fetchall()
conn.close()
convs: Dict[str, ConversationItem] = {}
order: list[str] = []
for row in rows:
cid = row["id"]
if cid not in convs:
convs[cid] = ConversationItem(
id=cid,
created_at=row["created_at"],
updated_at=row["updated_at"],
)
order.append(cid)
if row["role"] is not None:
convs[cid].messages.append(
MessageItem(role=row["role"], content=row["content"], timestamp=row["timestamp"], model=row["model"], tokens=row["tokens"])
)
return [convs[cid] for cid in order]
def delete_conversation(self, conversation_id: str):
conn = self._connect()
try:
cur = conn.cursor()
cur.execute("DELETE FROM messages WHERE conversation_id = ?", (conversation_id,))
cur.execute("DELETE FROM conversations WHERE id = ?", (conversation_id,))
conn.commit()
finally:
conn.close()
# -----------------------------
# Search API
# -----------------------------
def search_conversations(self, query: str, limit: int = 3) -> List[dict]:
"""Return up to `limit` conversations that contain the query string,
with full user+assistant exchange pairs around each match."""
if not query or not query.strip():
return []
q = query.strip().lower()
conn = self._connect()
cur = conn.cursor()
cur.execute("""
SELECT DISTINCT c.id, c.created_at, c.updated_at
FROM conversations c
JOIN messages m ON m.conversation_id = c.id
WHERE LOWER(m.content) LIKE ?
ORDER BY c.updated_at DESC
LIMIT ?
""", (f"%{q}%", limit))
rows = cur.fetchall()
results = []
for row in rows:
# Load all messages in order so we can find complete exchange pairs
cur.execute("""
SELECT role, content FROM messages
WHERE conversation_id = ?
ORDER BY timestamp ASC
""", (row["id"],))
all_msgs = [{"role": r["role"], "content": r["content"]} for r in cur.fetchall()]
# For each matching message, collect the full user+assistant pair around it
seen_pairs: set = set()
matches = []
for i, msg in enumerate(all_msgs):
if q not in msg["content"].lower():
continue
if msg["role"] == "user":
start, end = i, i + 1 if i + 1 < len(all_msgs) else i
else:
start, end = (i - 1 if i > 0 else i), i
if (start, end) in seen_pairs:
continue
seen_pairs.add((start, end))
for m in all_msgs[start:end + 1]:
matches.append({"role": m["role"], "content": m["content"][:500]})
if len(seen_pairs) >= 2:
break
if matches:
results.append({
"id": row["id"],
"updated_at": row["updated_at"],
"matches": matches,
})
conn.close()
return results
# -----------------------------
# Settings API
# -----------------------------
_SETTINGS_DEFAULTS: Dict[str, Any] = {
"model": "",
"temperature": 0.7,
"system_prompt": "",
"timeout": 120,
}
def get_settings(self) -> Dict[str, Any]:
conn = self._connect()
cur = conn.cursor()
cur.execute("SELECT key, value FROM settings")
rows = cur.fetchall()
conn.close()
result = dict(self._SETTINGS_DEFAULTS)
for row in rows:
try:
result[row["key"]] = json.loads(row["value"])
except Exception:
result[row["key"]] = row["value"]
return result
def update_settings(self, data: Dict[str, Any]):
conn = self._connect()
try:
cur = conn.cursor()
for key, value in data.items():
if key in self._SETTINGS_DEFAULTS:
cur.execute(
"INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)",
(key, json.dumps(value))
)
conn.commit()
finally:
conn.close()
# -----------------------------
# Store Instance
# -----------------------------
from ..nexus_config import MEMORY_DB
DB_PATH = MEMORY_DB
store = PersistentMemoryStore(DB_PATH)