ScottzillaSystems commited on
Commit
ab886a2
·
verified ·
1 Parent(s): ec44b60

Add self-contained conversation memory system

Browse files
Files changed (1) hide show
  1. memory_system.py +140 -0
memory_system.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Conversation Memory System for ScottzillaSystems
3
+ Self-contained — no external APIs needed
4
+ Uses SQLite + sentence-transformers for local embeddings
5
+ Replaces the fraudulent MemPalace project with a real working system.
6
+ """
7
+
8
+ import os
9
+ import json
10
+ import sqlite3
11
+ import hashlib
12
+ from datetime import datetime
13
+ from typing import List, Dict, Optional
14
+
15
+ try:
16
+ from sentence_transformers import SentenceTransformer
17
+ EMBEDDING_MODEL = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
18
+ USE_VECTOR_SEARCH = True
19
+ except Exception:
20
+ EMBEDDING_MODEL = None
21
+ USE_VECTOR_SEARCH = False
22
+ print("[Memory] sentence-transformers not available, using text search fallback")
23
+
24
+
25
+ class ConversationMemory:
26
+ def __init__(self, db_path: str = "./memory_db/conversations.db", user_id: str = "scottzilla"):
27
+ self.db_path = db_path
28
+ self.user_id = user_id
29
+ 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 BLOB
45
+ )
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 None
54
+ 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 None
60
+
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 results
84
+
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()