ScottzillaSystems/conversation-memory
1
1"""2Conversation Memory System for ScottzillaSystems3Self-contained โ no external APIs needed4Uses SQLite + sentence-transformers for local embeddings5Replaces the fraudulent MemPalace project with a real working system.6"""7 8import os9import json10import sqlite311import hashlib12from datetime import datetime13from typing import List, Dict, Optional14 15try:16 from sentence_transformers import SentenceTransformer17 EMBEDDING_MODEL = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')18 USE_VECTOR_SEARCH = True19except Exception:20 EMBEDDING_MODEL = None21 USE_VECTOR_SEARCH = False22 print("[Memory] sentence-transformers not available, using text search fallback")23 24 25class ConversationMemory:26 def __init__(self, db_path: str = "./memory_db/conversations.db", user_id: str = "scottzilla"):27 self.db_path = db_path28 self.user_id = user_id29 os.makedirs(os.path.dirname(db_path), exist_ok=True)30 self.conn = sqlite3.connect(db_path)31 self._init_db()32 33 def _init_db(self):34 self.conn.execute("""35 CREATE TABLE IF NOT EXISTS memories (36 id INTEGER PRIMARY KEY AUTOINCREMENT,37 user_id TEXT NOT NULL,38 thread_id TEXT NOT NULL,39 timestamp TEXT NOT NULL,40 role TEXT NOT NULL,41 content TEXT NOT NULL,42 content_hash TEXT NOT NULL,43 metadata TEXT,44 embedding BLOB45 )46 """)47 self.conn.execute("CREATE INDEX IF NOT EXISTS idx_user_thread ON memories(user_id, thread_id, timestamp)")48 self.conn.execute("CREATE INDEX IF NOT EXISTS idx_content ON memories(content)")49 self.conn.commit()50 51 def _get_embedding(self, text: str) -> Optional[bytes]:52 if not USE_VECTOR_SEARCH or EMBEDDING_MODEL is None:53 return None54 try:55 embedding = EMBEDDING_MODEL.encode(text, convert_to_numpy=True)56 return embedding.tobytes()57 except Exception as e:58 print(f"[Memory] Embedding error: {e}")59 return None60 61 def save_message(self, role: str, content: str, thread_id: str, metadata: Optional[Dict] = None) -> Dict:62 timestamp = datetime.utcnow().isoformat()63 content_hash = hashlib.sha256(content.encode()).hexdigest()[:16]64 embedding = self._get_embedding(content)65 meta_json = json.dumps(metadata or {})66 cursor = self.conn.execute(67 "INSERT INTO memories (user_id, thread_id, timestamp, role, content, content_hash, metadata, embedding) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",68 (self.user_id, thread_id, timestamp, role, content, content_hash, meta_json, embedding)69 )70 self.conn.commit()71 return {"id": cursor.lastrowid, "thread_id": thread_id, "timestamp": timestamp, "role": role, "content": content, "metadata": metadata or {}}72 73 def save_conversation(self, messages: List[Dict], thread_id: str, title: Optional[str] = None) -> List[Dict]:74 results = []75 for msg in messages:76 result = self.save_message(77 role=msg.get("role", "unknown"),78 content=msg.get("content", ""),79 thread_id=thread_id,80 metadata={"title": title, **msg.get("metadata", {})}81 )82 results.append(result)83 return results84 85 def get_thread(self, thread_id: str, limit: int = 1000) -> List[Dict]:86 cursor = self.conn.execute(87 "SELECT id, timestamp, role, content, metadata FROM memories WHERE user_id = ? AND thread_id = ? ORDER BY timestamp ASC LIMIT ?",88 (self.user_id, thread_id, limit)89 )90 rows = cursor.fetchall()91 return [{"id": row[0], "timestamp": row[1], "role": row[2], "content": row[3], "metadata": json.loads(row[4])} for row in rows]92 93 def search(self, query: str, thread_id: Optional[str] = None, limit: int = 20) -> List[Dict]:94 if thread_id:95 cursor = self.conn.execute(96 "SELECT id, timestamp, role, content, metadata FROM memories WHERE user_id = ? AND thread_id = ? AND content LIKE ? ORDER BY timestamp DESC LIMIT ?",97 (self.user_id, thread_id, f"%{query}%", limit)98 )99 rows = cursor.fetchall()100 return [{"id": r[0], "timestamp": r[1], "role": r[2], "content": r[3], "metadata": json.loads(r[4])} for r in rows]101 else:102 cursor = self.conn.execute(103 "SELECT id, timestamp, role, content, metadata, thread_id FROM memories WHERE user_id = ? AND content LIKE ? ORDER BY timestamp DESC LIMIT ?",104 (self.user_id, f"%{query}%", limit)105 )106 rows = cursor.fetchall()107 return [{"id": r[0], "timestamp": r[1], "role": r[2], "content": r[3], "metadata": json.loads(r[4]), "thread_id": r[5]} for r in rows]108 109 def get_all_threads(self) -> List[Dict]:110 cursor = self.conn.execute(111 "SELECT thread_id, COUNT(*) as msg_count, MIN(timestamp) as started, MAX(timestamp) as last_msg FROM memories WHERE user_id = ? GROUP BY thread_id ORDER BY last_msg DESC",112 (self.user_id,)113 )114 rows = cursor.fetchall()115 return [{"thread_id": row[0], "message_count": row[1], "started": row[2], "last_message": row[3]} for row in rows]116 117 def export_to_json(self, filepath: str, thread_id: Optional[str] = None):118 if thread_id:119 memories = self.get_thread(thread_id)120 else:121 cursor = self.conn.execute("SELECT id, timestamp, role, content, metadata, thread_id FROM memories WHERE user_id = ? ORDER BY timestamp", (self.user_id,))122 memories = [{"id": r[0], "timestamp": r[1], "role": r[2], "content": r[3], "metadata": json.loads(r[4]), "thread_id": r[5]} for r in cursor.fetchall()]123 with open(filepath, 'w') as f:124 json.dump(memories, f, indent=2)125 print(f"[Memory] Exported {len(memories)} memories to {filepath}")126 127 def export_to_markdown(self, filepath: str, thread_id: str):128 memories = self.get_thread(thread_id)129 with open(filepath, 'w') as f:130 f.write(f"# Conversation: {thread_id}\n\n*Exported: {datetime.utcnow().isoformat()}*\n\n---\n\n")131 for mem in memories:132 role = mem.get("role", "unknown")133 timestamp = mem.get("timestamp", "unknown")134 content = mem.get("content", "")135 emoji = "๐ค" if role == "user" else "๐ค" if role == "assistant" else "๐"136 f.write(f"### {emoji} {role.title()} *({timestamp})*\n\n{content}\n\n---\n\n")137 print(f"[Memory] Exported conversation to {filepath}")138 139 def close(self):140 self.conn.close()141 