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>
This commit is contained in:
jon
2026-05-18 13:39:48 -05:00
commit b0aa0438af
1264 changed files with 19255 additions and 0 deletions
+94
View File
@@ -0,0 +1,94 @@
"""Memory extraction — asks Mistral to evaluate a conversation exchange and
decide if it contains a new permanent personal fact worth saving."""
from __future__ import annotations
import json
import logging
import re
from typing import Optional
_log = logging.getLogger(__name__)
_PROMPT = """\
You are a memory curator for a personal AI assistant named Nexus.
A conversation just occurred. Decide if the USER revealed a NEW, PERMANENT \
personal fact that should be saved to long-term memory.
SAVE ONLY facts that are stable and biographical:
- Identity: full name, age, location, nationality
- Possessions: vehicle, home, devices
- Relationships: family members, partner, close friends
- Career: job title, employer, field, skills
- Long-running projects or goals (not today's to-dos)
- Durable preferences or habits explicitly stated
DO NOT SAVE — these are ephemeral and would clutter memory:
- Anything time-bound: "today I have...", "I'm working on X today", "I have a full day of work"
- Mood or energy: "I'm tired", "feeling good", "having a rough day"
- Greetings or small talk: "good morning", "how are you"
- Questions the user asked the assistant
- Near-duplicates of anything in the existing memory list below
Existing memory (do not re-save these or close variants):
{existing_texts}
---
USER: {user_message}
ASSISTANT: {assistant_response}
---
Respond with JSON only — no prose, no markdown fences:
{{"save": true, "section": "<section name, or one from the list above>", "text": "<concise fact about Jon, third person>"}}
OR
{{"save": false}}"""
async def extract_memory(
user_message: str,
assistant_response: str,
existing_sections: list[str],
existing_texts: list[str],
ollama_manager,
) -> Optional[dict]:
"""Ask Mistral to extract a saveable memory fact from a conversation exchange.
Returns {"section": ..., "text": ...} or None.
"""
if existing_texts:
texts_block = "\n".join(f"- {t}" for t in existing_texts[:40])
else:
texts_block = "(none yet)"
prompt = _PROMPT.format(
existing_texts=texts_block,
user_message=user_message[:800],
assistant_response=assistant_response[:800],
)
try:
response = await ollama_manager.chat(
messages=[{"role": "user", "content": prompt}],
model="mistral:latest",
stream=False,
temperature=0.0,
)
if not response:
return None
text = response.strip()
# Strip markdown code fences if the model added them
if "```" in text:
m = re.search(r"```(?:json)?\s*(.*?)\s*```", text, re.DOTALL)
if m:
text = m.group(1).strip()
data = json.loads(text)
if data.get("save") and data.get("section") and data.get("text"):
return {
"section": str(data["section"]).strip(),
"text": str(data["text"]).strip(),
}
except Exception as e:
_log.debug("memory extraction failed: %s", e)
return None
+125
View File
@@ -0,0 +1,125 @@
"""Dedicated memory curator service — run alongside Synapse on port 8001.
Endpoints:
GET / health check
GET /memories list all memory items (optional ?section= filter)
POST /memories direct write — no LLM, saves immediately
PATCH /memories/{id} update a memory item
DELETE /memories/{id} delete a memory item
POST /memories/extract LLM-curated: evaluate a conversation exchange and
optionally save a new permanent fact
"""
from __future__ import annotations
import asyncio
import uuid
from typing import Any, Dict, List, Optional
from fastapi import FastAPI, HTTPException, Body
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from .store import store, MemoryItem
from .extractor import extract_memory
from ..ollama_manager import get_ollama_manager
app = FastAPI(title="Nexus Memory Service", version="1.0")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=False,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/")
async def health():
return {"status": "ok", "count": len(store.all())}
@app.get("/memories")
async def list_memories(section: Optional[str] = None):
items = store.all()
if section:
items = [i for i in items if i.section.lower() == section.lower()]
return {"items": [{"id": i.id, "section": i.section, "text": i.text, "tags": i.tags} for i in items]}
@app.post("/memories")
async def add_memory(payload: Dict[str, Any] = Body(...)):
text = (payload.get("text") or "").strip()
if not text:
raise HTTPException(status_code=400, detail="Missing 'text'")
item = MemoryItem(
id=str(uuid.uuid4()),
section=(payload.get("section") or "General").strip(),
text=text,
tags=payload.get("tags", []),
)
store.add(item)
return {"id": item.id, "section": item.section, "text": item.text, "tags": item.tags}
@app.patch("/memories/{item_id}")
async def update_memory(item_id: str, payload: Dict[str, Any] = Body(...)):
existing = store.get(item_id)
if not existing:
raise HTTPException(status_code=404, detail="Not found")
updated = MemoryItem(
id=item_id,
section=(payload.get("section") or existing.section).strip(),
text=(payload.get("text") or existing.text).strip(),
tags=payload.get("tags", existing.tags),
)
store.update(updated)
return {"id": updated.id, "section": updated.section, "text": updated.text, "tags": updated.tags}
@app.delete("/memories/{item_id}")
async def delete_memory(item_id: str):
if not store.get(item_id):
raise HTTPException(status_code=404, detail="Not found")
store.delete(item_id)
return {"status": "deleted"}
class ExtractRequest(BaseModel):
user_message: str
assistant_response: str
@app.post("/memories/extract")
async def extract_and_save(req: ExtractRequest):
"""LLM-curated extraction — asks Mistral to evaluate the exchange against all
existing memory items and save only new, permanent personal facts."""
existing = store.all()
existing_sections = list({i.section for i in existing})
existing_texts = [i.text for i in existing]
try:
result = await asyncio.wait_for(
extract_memory(
req.user_message,
req.assistant_response,
existing_sections,
existing_texts,
get_ollama_manager(),
),
timeout=20.0,
)
if result:
item = MemoryItem(
id=str(uuid.uuid4()),
section=result["section"],
text=result["text"],
)
store.add(item)
return {"saved": True, "id": item.id, "section": item.section, "text": item.text}
except asyncio.TimeoutError:
pass
except Exception:
pass
return {"saved": False}
+380
View File
@@ -0,0 +1,380 @@
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)