enhanced-mcp-memory 1.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- database.py +751 -0
- enhanced_mcp_memory-1.2.1.dist-info/METADATA +274 -0
- enhanced_mcp_memory-1.2.1.dist-info/RECORD +9 -0
- enhanced_mcp_memory-1.2.1.dist-info/WHEEL +5 -0
- enhanced_mcp_memory-1.2.1.dist-info/entry_points.txt +2 -0
- enhanced_mcp_memory-1.2.1.dist-info/licenses/LICENSE +21 -0
- enhanced_mcp_memory-1.2.1.dist-info/top_level.txt +3 -0
- mcp_server_enhanced.py +345 -0
- memory_manager.py +718 -0
database.py
ADDED
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SQLite3 Database Manager for MCP Memory and Task Management
|
|
3
|
+
Handles memories, knowledge graphs, tasks, and project data
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sqlite3
|
|
7
|
+
from sqlite3 import Cursor
|
|
8
|
+
import json
|
|
9
|
+
import uuid
|
|
10
|
+
import time
|
|
11
|
+
from datetime import datetime, timedelta
|
|
12
|
+
from typing import List, Dict, Optional, Any, Union
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
import logging
|
|
15
|
+
from functools import wraps
|
|
16
|
+
|
|
17
|
+
def retry_on_failure(max_retries=3, delay=1.0):
|
|
18
|
+
"""Decorator to retry database operations on failure"""
|
|
19
|
+
def decorator(func):
|
|
20
|
+
@wraps(func)
|
|
21
|
+
def wrapper(*args, **kwargs):
|
|
22
|
+
for attempt in range(max_retries):
|
|
23
|
+
try:
|
|
24
|
+
return func(*args, **kwargs)
|
|
25
|
+
except Exception as e:
|
|
26
|
+
if attempt == max_retries - 1:
|
|
27
|
+
raise
|
|
28
|
+
logging.warning(f"Attempt {attempt + 1} failed: {e}. Retrying in {delay}s...")
|
|
29
|
+
time.sleep(delay * (2 ** attempt)) # Exponential backoff
|
|
30
|
+
return None
|
|
31
|
+
return wrapper
|
|
32
|
+
return decorator
|
|
33
|
+
|
|
34
|
+
class DatabaseManager:
|
|
35
|
+
def __init__(self, db_path: str = "mcp_memory.db"):
|
|
36
|
+
"""Initialize database connection and create tables if they don't exist"""
|
|
37
|
+
self.db_path = db_path
|
|
38
|
+
self.connection: Optional[sqlite3.Connection] = None
|
|
39
|
+
self.setup_database()
|
|
40
|
+
|
|
41
|
+
def _check_connection(self) -> bool:
|
|
42
|
+
"""Check if database connection is valid"""
|
|
43
|
+
if not self.connection:
|
|
44
|
+
logging.error("Database connection not established")
|
|
45
|
+
return False
|
|
46
|
+
return True
|
|
47
|
+
|
|
48
|
+
def setup_database(self) -> None:
|
|
49
|
+
"""Create database connection and initialize tables"""
|
|
50
|
+
try:
|
|
51
|
+
self.connection = sqlite3.connect(self.db_path, check_same_thread=False)
|
|
52
|
+
self.connection.row_factory = sqlite3.Row # Enable column access by name
|
|
53
|
+
self.create_tables()
|
|
54
|
+
logging.info(f"Database initialized at {self.db_path}")
|
|
55
|
+
except Exception as e:
|
|
56
|
+
logging.error(f"Failed to initialize database: {e}")
|
|
57
|
+
raise
|
|
58
|
+
|
|
59
|
+
def create_tables(self) -> None:
|
|
60
|
+
"""Create all necessary tables for the MCP memory system"""
|
|
61
|
+
if not self._check_connection() or not self.connection:
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
cursor: Cursor = self.connection.cursor()
|
|
65
|
+
|
|
66
|
+
# Check if we need to migrate the schema
|
|
67
|
+
self._migrate_schema(cursor)
|
|
68
|
+
|
|
69
|
+
# Projects table - tracks different coding projects
|
|
70
|
+
cursor.execute("""
|
|
71
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
72
|
+
id TEXT PRIMARY KEY,
|
|
73
|
+
name TEXT NOT NULL,
|
|
74
|
+
path TEXT,
|
|
75
|
+
description TEXT,
|
|
76
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
77
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
78
|
+
metadata TEXT -- JSON metadata
|
|
79
|
+
)
|
|
80
|
+
""")
|
|
81
|
+
|
|
82
|
+
# Memories table - stores contextual information and learning
|
|
83
|
+
cursor.execute("""
|
|
84
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
85
|
+
id TEXT PRIMARY KEY,
|
|
86
|
+
project_id TEXT,
|
|
87
|
+
type TEXT NOT NULL, -- 'code', 'conversation', 'decision', 'pattern', 'error'
|
|
88
|
+
title TEXT NOT NULL,
|
|
89
|
+
content TEXT NOT NULL,
|
|
90
|
+
embedding_vector TEXT, -- JSON array for semantic search
|
|
91
|
+
content_hash TEXT, -- Hash for duplicate detection
|
|
92
|
+
file_path TEXT, -- Associated file path
|
|
93
|
+
importance_score REAL DEFAULT 0.5,
|
|
94
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
95
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
96
|
+
accessed_count INTEGER DEFAULT 0,
|
|
97
|
+
last_accessed TIMESTAMP,
|
|
98
|
+
tags TEXT, -- JSON array of tags
|
|
99
|
+
metadata TEXT, -- JSON metadata
|
|
100
|
+
FOREIGN KEY (project_id) REFERENCES projects(id)
|
|
101
|
+
)
|
|
102
|
+
""")
|
|
103
|
+
|
|
104
|
+
# Tasks table - tracks development tasks and TODOs
|
|
105
|
+
cursor.execute("""
|
|
106
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
107
|
+
id TEXT PRIMARY KEY,
|
|
108
|
+
project_id TEXT,
|
|
109
|
+
title TEXT NOT NULL,
|
|
110
|
+
description TEXT,
|
|
111
|
+
status TEXT DEFAULT 'pending', -- 'pending', 'in_progress', 'completed', 'cancelled'
|
|
112
|
+
priority TEXT DEFAULT 'medium', -- 'low', 'medium', 'high', 'critical'
|
|
113
|
+
category TEXT, -- 'bug', 'feature', 'refactor', 'docs', 'test'
|
|
114
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
115
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
116
|
+
due_date TIMESTAMP,
|
|
117
|
+
estimated_hours REAL,
|
|
118
|
+
actual_hours REAL,
|
|
119
|
+
tags TEXT, -- JSON array
|
|
120
|
+
metadata TEXT, -- JSON metadata
|
|
121
|
+
FOREIGN KEY (project_id) REFERENCES projects(id)
|
|
122
|
+
)
|
|
123
|
+
""")
|
|
124
|
+
|
|
125
|
+
# Knowledge Graph - relationships between memories, tasks, and concepts
|
|
126
|
+
cursor.execute("""
|
|
127
|
+
CREATE TABLE IF NOT EXISTS knowledge_relationships (
|
|
128
|
+
id TEXT PRIMARY KEY,
|
|
129
|
+
from_type TEXT NOT NULL, -- 'memory', 'task', 'project', 'concept'
|
|
130
|
+
from_id TEXT NOT NULL,
|
|
131
|
+
to_type TEXT NOT NULL,
|
|
132
|
+
to_id TEXT NOT NULL,
|
|
133
|
+
relationship_type TEXT NOT NULL, -- 'depends_on', 'relates_to', 'conflicts_with', 'implements', 'references'
|
|
134
|
+
strength REAL DEFAULT 1.0, -- relationship strength (0.0 to 1.0)
|
|
135
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
136
|
+
metadata TEXT -- JSON metadata
|
|
137
|
+
)
|
|
138
|
+
""")
|
|
139
|
+
|
|
140
|
+
# Sessions table - tracks AI interaction sessions
|
|
141
|
+
cursor.execute("""
|
|
142
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
143
|
+
id TEXT PRIMARY KEY,
|
|
144
|
+
project_id TEXT,
|
|
145
|
+
started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
146
|
+
ended_at TIMESTAMP,
|
|
147
|
+
interaction_count INTEGER DEFAULT 0,
|
|
148
|
+
context_summary TEXT,
|
|
149
|
+
FOREIGN KEY (project_id) REFERENCES projects(id)
|
|
150
|
+
)
|
|
151
|
+
""")
|
|
152
|
+
|
|
153
|
+
# Context layers - different context layers for memory retrieval
|
|
154
|
+
cursor.execute("""
|
|
155
|
+
CREATE TABLE IF NOT EXISTS context_layers (
|
|
156
|
+
id TEXT PRIMARY KEY,
|
|
157
|
+
name TEXT NOT NULL,
|
|
158
|
+
description TEXT,
|
|
159
|
+
query_pattern TEXT, -- Pattern for automatic activation
|
|
160
|
+
priority INTEGER DEFAULT 1,
|
|
161
|
+
is_active BOOLEAN DEFAULT 1,
|
|
162
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
163
|
+
)
|
|
164
|
+
""")
|
|
165
|
+
|
|
166
|
+
# Create comprehensive indexes for better performance
|
|
167
|
+
# Memory indexes
|
|
168
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_memories_project_id ON memories(project_id)")
|
|
169
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type)")
|
|
170
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_memories_importance ON memories(importance_score DESC)")
|
|
171
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_memories_created_at ON memories(created_at DESC)")
|
|
172
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_memories_last_accessed ON memories(last_accessed DESC)")
|
|
173
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_memories_content_hash ON memories(content_hash)")
|
|
174
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_memories_file_path ON memories(file_path)")
|
|
175
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_memories_accessed_count ON memories(accessed_count DESC)")
|
|
176
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_memories_composite ON memories(project_id, type, importance_score DESC)")
|
|
177
|
+
|
|
178
|
+
# Task indexes
|
|
179
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_tasks_project_id ON tasks(project_id)")
|
|
180
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status)")
|
|
181
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_tasks_priority ON tasks(priority)")
|
|
182
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_tasks_created_at ON tasks(created_at DESC)")
|
|
183
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_tasks_category ON tasks(category)")
|
|
184
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_tasks_composite ON tasks(project_id, status, priority)")
|
|
185
|
+
|
|
186
|
+
# Knowledge relationship indexes
|
|
187
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_relationships_from ON knowledge_relationships(from_type, from_id)")
|
|
188
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_relationships_to ON knowledge_relationships(to_type, to_id)")
|
|
189
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_relationships_type ON knowledge_relationships(relationship_type)")
|
|
190
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_relationships_strength ON knowledge_relationships(strength DESC)")
|
|
191
|
+
|
|
192
|
+
# Session indexes
|
|
193
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_sessions_project_id ON sessions(project_id)")
|
|
194
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_sessions_started_at ON sessions(started_at DESC)")
|
|
195
|
+
|
|
196
|
+
# Context layer indexes
|
|
197
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_context_layers_active ON context_layers(is_active)")
|
|
198
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_context_layers_priority ON context_layers(priority DESC)")
|
|
199
|
+
|
|
200
|
+
# Notifications table - stores system notifications
|
|
201
|
+
cursor.execute("""
|
|
202
|
+
CREATE TABLE IF NOT EXISTS notifications (
|
|
203
|
+
id TEXT PRIMARY KEY,
|
|
204
|
+
project_id TEXT,
|
|
205
|
+
type TEXT NOT NULL, -- 'system', 'task', 'memory', 'alert'
|
|
206
|
+
title TEXT NOT NULL,
|
|
207
|
+
message TEXT NOT NULL,
|
|
208
|
+
is_read BOOLEAN DEFAULT 0,
|
|
209
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
210
|
+
metadata TEXT, -- JSON metadata
|
|
211
|
+
FOREIGN KEY (project_id) REFERENCES projects(id)
|
|
212
|
+
)
|
|
213
|
+
""")
|
|
214
|
+
|
|
215
|
+
# Create indexes for notifications
|
|
216
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_notifications_project ON notifications(project_id)")
|
|
217
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_notifications_read ON notifications(is_read)")
|
|
218
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_notifications_created ON notifications(created_at)")
|
|
219
|
+
|
|
220
|
+
self.connection.commit()
|
|
221
|
+
logging.info("Database tables created successfully")
|
|
222
|
+
|
|
223
|
+
def _migrate_schema(self, cursor: Cursor) -> None:
|
|
224
|
+
"""Migrate database schema to add new columns if they don't exist"""
|
|
225
|
+
try:
|
|
226
|
+
# Check if content_hash column exists in memories table
|
|
227
|
+
cursor.execute("PRAGMA table_info(memories)")
|
|
228
|
+
columns = [row[1] for row in cursor.fetchall()]
|
|
229
|
+
|
|
230
|
+
if 'content_hash' not in columns:
|
|
231
|
+
cursor.execute("ALTER TABLE memories ADD COLUMN content_hash TEXT")
|
|
232
|
+
logging.info("Added content_hash column to memories table")
|
|
233
|
+
|
|
234
|
+
if 'file_path' not in columns:
|
|
235
|
+
cursor.execute("ALTER TABLE memories ADD COLUMN file_path TEXT")
|
|
236
|
+
logging.info("Added file_path column to memories table")
|
|
237
|
+
|
|
238
|
+
if 'embedding_vector' not in columns:
|
|
239
|
+
cursor.execute("ALTER TABLE memories ADD COLUMN embedding_vector TEXT")
|
|
240
|
+
logging.info("Added embedding_vector column to memories table")
|
|
241
|
+
|
|
242
|
+
# Update accessed_count to INTEGER if it's not already
|
|
243
|
+
if 'accessed_count' in columns:
|
|
244
|
+
# Check current type and update if needed
|
|
245
|
+
cursor.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name='memories'")
|
|
246
|
+
table_sql = cursor.fetchone()[0]
|
|
247
|
+
if 'accessed_count REAL' in table_sql or 'accessed_count TEXT' in table_sql:
|
|
248
|
+
# Create a temporary table with correct schema
|
|
249
|
+
cursor.execute("""CREATE TABLE memories_temp AS
|
|
250
|
+
SELECT id, project_id, type, title, content,
|
|
251
|
+
CAST(accessed_count AS INTEGER) as accessed_count,
|
|
252
|
+
importance_score, created_at, updated_at,
|
|
253
|
+
last_accessed, tags, metadata
|
|
254
|
+
FROM memories""")
|
|
255
|
+
cursor.execute("DROP TABLE memories")
|
|
256
|
+
cursor.execute("ALTER TABLE memories_temp RENAME TO memories")
|
|
257
|
+
logging.info("Updated accessed_count column type to INTEGER")
|
|
258
|
+
|
|
259
|
+
except Exception as e:
|
|
260
|
+
logging.warning(f"Schema migration warning: {e}")
|
|
261
|
+
# Continue anyway - the table creation will handle missing tables
|
|
262
|
+
|
|
263
|
+
def get_or_create_project(self, name: str, path: Optional[str] = None,
|
|
264
|
+
description: Optional[str] = None) -> str:
|
|
265
|
+
"""Get existing project or create new one"""
|
|
266
|
+
if not self._check_connection() or not self.connection:
|
|
267
|
+
return ""
|
|
268
|
+
|
|
269
|
+
cursor: Cursor = self.connection.cursor()
|
|
270
|
+
|
|
271
|
+
# Try to find existing project by name or path
|
|
272
|
+
if path:
|
|
273
|
+
cursor.execute("SELECT id FROM projects WHERE path = ? OR name = ?", (path, name))
|
|
274
|
+
else:
|
|
275
|
+
cursor.execute("SELECT id FROM projects WHERE name = ?", (name,))
|
|
276
|
+
|
|
277
|
+
result = cursor.fetchone()
|
|
278
|
+
if result:
|
|
279
|
+
return result['id']
|
|
280
|
+
|
|
281
|
+
# Create new project
|
|
282
|
+
project_id = str(uuid.uuid4())
|
|
283
|
+
cursor.execute("""
|
|
284
|
+
INSERT INTO projects (id, name, path, description)
|
|
285
|
+
VALUES (?, ?, ?, ?)
|
|
286
|
+
""", (project_id, name, path, description))
|
|
287
|
+
self.connection.commit()
|
|
288
|
+
return project_id
|
|
289
|
+
|
|
290
|
+
@retry_on_failure()
|
|
291
|
+
def add_memory(self, project_id: str, memory_type: str, title: str, content: str,
|
|
292
|
+
tags: Optional[List[str]] = None, importance_score: float = 0.5,
|
|
293
|
+
metadata: Optional[Dict[str, Any]] = None) -> str:
|
|
294
|
+
"""Add a new memory to the database"""
|
|
295
|
+
if not self._check_connection() or not self.connection:
|
|
296
|
+
return ""
|
|
297
|
+
|
|
298
|
+
cursor: Cursor = self.connection.cursor()
|
|
299
|
+
memory_id = str(uuid.uuid4())
|
|
300
|
+
|
|
301
|
+
cursor.execute("""
|
|
302
|
+
INSERT INTO memories (id, project_id, type, title, content, importance_score, tags, metadata)
|
|
303
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
304
|
+
""", (
|
|
305
|
+
memory_id, project_id, memory_type, title, content,
|
|
306
|
+
importance_score, json.dumps(tags or []), json.dumps(metadata or {})
|
|
307
|
+
))
|
|
308
|
+
self.connection.commit()
|
|
309
|
+
return memory_id
|
|
310
|
+
|
|
311
|
+
@retry_on_failure()
|
|
312
|
+
def get_memories(self, project_id: Optional[str] = None, memory_type: Optional[str] = None,
|
|
313
|
+
limit: int = 50, sort_by: str = "created_at") -> List[Dict[str, Any]]:
|
|
314
|
+
"""Retrieve memories with optional filters"""
|
|
315
|
+
if not self._check_connection() or not self.connection:
|
|
316
|
+
return []
|
|
317
|
+
|
|
318
|
+
cursor: Cursor = self.connection.cursor()
|
|
319
|
+
|
|
320
|
+
query = "SELECT * FROM memories WHERE 1=1"
|
|
321
|
+
params = []
|
|
322
|
+
|
|
323
|
+
if project_id:
|
|
324
|
+
query += " AND project_id = ?"
|
|
325
|
+
params.append(project_id)
|
|
326
|
+
|
|
327
|
+
if memory_type:
|
|
328
|
+
query += " AND type = ?"
|
|
329
|
+
params.append(memory_type)
|
|
330
|
+
|
|
331
|
+
query += f" ORDER BY {sort_by} DESC LIMIT ?"
|
|
332
|
+
params.append(limit)
|
|
333
|
+
|
|
334
|
+
cursor.execute(query, params)
|
|
335
|
+
return [dict(row) for row in cursor.fetchall()]
|
|
336
|
+
|
|
337
|
+
@retry_on_failure()
|
|
338
|
+
def add_task(self, project_id: str, title: str, description: Optional[str] = None,
|
|
339
|
+
priority: str = "medium", category: Optional[str] = None,
|
|
340
|
+
tags: Optional[List[str]] = None, metadata: Optional[Dict[str, Any]] = None) -> str:
|
|
341
|
+
"""Add a new task"""
|
|
342
|
+
if not self._check_connection() or not self.connection:
|
|
343
|
+
return ""
|
|
344
|
+
|
|
345
|
+
cursor: Cursor = self.connection.cursor()
|
|
346
|
+
task_id = str(uuid.uuid4())
|
|
347
|
+
|
|
348
|
+
cursor.execute("""
|
|
349
|
+
INSERT INTO tasks (id, project_id, title, description, priority, category, tags, metadata)
|
|
350
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
351
|
+
""", (
|
|
352
|
+
task_id, project_id, title, description, priority, category,
|
|
353
|
+
json.dumps(tags or []), json.dumps(metadata or {})
|
|
354
|
+
))
|
|
355
|
+
self.connection.commit()
|
|
356
|
+
return task_id
|
|
357
|
+
|
|
358
|
+
@retry_on_failure()
|
|
359
|
+
def get_tasks(self, project_id: Optional[str] = None, status: Optional[str] = None,
|
|
360
|
+
limit: int = 50) -> List[Dict[str, Any]]:
|
|
361
|
+
"""Retrieve tasks with optional filters"""
|
|
362
|
+
if not self._check_connection() or not self.connection:
|
|
363
|
+
return []
|
|
364
|
+
|
|
365
|
+
cursor: Cursor = self.connection.cursor()
|
|
366
|
+
|
|
367
|
+
query = "SELECT * FROM tasks WHERE 1=1"
|
|
368
|
+
params = []
|
|
369
|
+
|
|
370
|
+
if project_id:
|
|
371
|
+
query += " AND project_id = ?"
|
|
372
|
+
params.append(project_id)
|
|
373
|
+
|
|
374
|
+
if status:
|
|
375
|
+
query += " AND status = ?"
|
|
376
|
+
params.append(status)
|
|
377
|
+
|
|
378
|
+
query += " ORDER BY priority DESC, created_at DESC LIMIT ?"
|
|
379
|
+
params.append(limit)
|
|
380
|
+
|
|
381
|
+
cursor.execute(query, params)
|
|
382
|
+
return [dict(row) for row in cursor.fetchall()]
|
|
383
|
+
|
|
384
|
+
def update_task_status(self, task_id: str, status: str) -> bool:
|
|
385
|
+
"""Update task status"""
|
|
386
|
+
if not self._check_connection() or not self.connection:
|
|
387
|
+
return False
|
|
388
|
+
|
|
389
|
+
cursor: Cursor = self.connection.cursor()
|
|
390
|
+
cursor.execute("""
|
|
391
|
+
UPDATE tasks SET status = ?, updated_at = CURRENT_TIMESTAMP
|
|
392
|
+
WHERE id = ?
|
|
393
|
+
""", (status, task_id))
|
|
394
|
+
self.connection.commit()
|
|
395
|
+
return cursor.rowcount > 0
|
|
396
|
+
|
|
397
|
+
def add_relationship(self, from_type: str, from_id: str, to_type: str,
|
|
398
|
+
to_id: str, relationship_type: str, strength: float = 1.0) -> str:
|
|
399
|
+
"""Add a relationship in the knowledge graph"""
|
|
400
|
+
if not self._check_connection() or not self.connection:
|
|
401
|
+
return ""
|
|
402
|
+
|
|
403
|
+
cursor: Cursor = self.connection.cursor()
|
|
404
|
+
rel_id = str(uuid.uuid4())
|
|
405
|
+
|
|
406
|
+
cursor.execute("""
|
|
407
|
+
INSERT INTO knowledge_relationships
|
|
408
|
+
(id, from_type, from_id, to_type, to_id, relationship_type, strength)
|
|
409
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
410
|
+
""", (rel_id, from_type, from_id, to_type, to_id, relationship_type, strength))
|
|
411
|
+
self.connection.commit()
|
|
412
|
+
return rel_id
|
|
413
|
+
|
|
414
|
+
def get_related_items(self, item_type: str, item_id: str) -> List[Dict[str, Any]]:
|
|
415
|
+
"""Get all items related to a specific item"""
|
|
416
|
+
if not self._check_connection() or not self.connection:
|
|
417
|
+
return []
|
|
418
|
+
|
|
419
|
+
cursor: Cursor = self.connection.cursor()
|
|
420
|
+
cursor.execute("""
|
|
421
|
+
SELECT * FROM knowledge_relationships
|
|
422
|
+
WHERE (from_type = ? AND from_id = ?) OR (to_type = ? AND to_id = ?)
|
|
423
|
+
ORDER BY strength DESC
|
|
424
|
+
""", (item_type, item_id, item_type, item_id))
|
|
425
|
+
return [dict(row) for row in cursor.fetchall()]
|
|
426
|
+
|
|
427
|
+
def search_memories(self, query: str, project_id: Optional[str] = None, limit: int = 10) -> List[Dict[str, Any]]:
|
|
428
|
+
"""Simple text search in memories"""
|
|
429
|
+
if not self._check_connection() or not self.connection:
|
|
430
|
+
return []
|
|
431
|
+
|
|
432
|
+
cursor: Cursor = self.connection.cursor()
|
|
433
|
+
|
|
434
|
+
search_query = """
|
|
435
|
+
SELECT * FROM memories
|
|
436
|
+
WHERE (title LIKE ? OR content LIKE ?)
|
|
437
|
+
"""
|
|
438
|
+
params = [f"%{query}%", f"%{query}%"]
|
|
439
|
+
|
|
440
|
+
if project_id:
|
|
441
|
+
search_query += " AND project_id = ?"
|
|
442
|
+
params.append(project_id)
|
|
443
|
+
|
|
444
|
+
search_query += " ORDER BY importance_score DESC, created_at DESC LIMIT ?"
|
|
445
|
+
params.append(limit)
|
|
446
|
+
|
|
447
|
+
cursor.execute(search_query, params)
|
|
448
|
+
return [dict(row) for row in cursor.fetchall()]
|
|
449
|
+
|
|
450
|
+
def update_memory_access(self, memory_id: str) -> None:
|
|
451
|
+
"""Update memory access count and timestamp (handles string count)"""
|
|
452
|
+
if not self._check_connection() or not self.connection:
|
|
453
|
+
return
|
|
454
|
+
|
|
455
|
+
try:
|
|
456
|
+
cursor: Cursor = self.connection.cursor()
|
|
457
|
+
if not cursor:
|
|
458
|
+
return
|
|
459
|
+
|
|
460
|
+
# First get current count as string
|
|
461
|
+
cursor.execute("SELECT accessed_count FROM memories WHERE id = ?", (memory_id,))
|
|
462
|
+
result = cursor.fetchone()
|
|
463
|
+
if not result:
|
|
464
|
+
return
|
|
465
|
+
|
|
466
|
+
current_count = int(result['accessed_count']) if result['accessed_count'] else 0
|
|
467
|
+
new_count = str(current_count + 1)
|
|
468
|
+
|
|
469
|
+
# Update with new string count
|
|
470
|
+
cursor.execute("""
|
|
471
|
+
UPDATE memories
|
|
472
|
+
SET accessed_count = ?, last_accessed = CURRENT_TIMESTAMP
|
|
473
|
+
WHERE id = ?
|
|
474
|
+
""", (new_count, memory_id))
|
|
475
|
+
self.connection.commit()
|
|
476
|
+
except Exception as e:
|
|
477
|
+
logging.error(f"Error updating memory access: {e}")
|
|
478
|
+
|
|
479
|
+
def get_project_summary(self, project_id: str) -> Dict[str, Union[Dict[str, str], str]]:
|
|
480
|
+
"""Get a summary of project statistics with all string values"""
|
|
481
|
+
if not self._check_connection() or not self.connection:
|
|
482
|
+
return {
|
|
483
|
+
"project": {},
|
|
484
|
+
"memory_counts": {},
|
|
485
|
+
"task_counts": {},
|
|
486
|
+
"total_memories": "0",
|
|
487
|
+
"total_tasks": "0"
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
cursor: Cursor = self.connection.cursor()
|
|
491
|
+
if not cursor:
|
|
492
|
+
return {
|
|
493
|
+
"project": {},
|
|
494
|
+
"memory_counts": {},
|
|
495
|
+
"task_counts": {},
|
|
496
|
+
"total_memories": "0",
|
|
497
|
+
"total_tasks": "0"
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
# Get project info - convert all values to strings
|
|
501
|
+
cursor.execute("SELECT * FROM projects WHERE id = ?", (project_id,))
|
|
502
|
+
project = {str(k): str(v) if v is not None else ""
|
|
503
|
+
for k, v in dict(cursor.fetchone() or {}).items()}
|
|
504
|
+
|
|
505
|
+
# Get memory counts - cast counts to TEXT in SQL
|
|
506
|
+
cursor.execute("""
|
|
507
|
+
SELECT type, CAST(COUNT(*) AS TEXT) as count FROM memories
|
|
508
|
+
WHERE project_id = ? GROUP BY type
|
|
509
|
+
""", (project_id,))
|
|
510
|
+
memory_counts = {str(row['type']): str(row['count']) for row in cursor.fetchall()}
|
|
511
|
+
|
|
512
|
+
# Get task counts - cast counts to TEXT in SQL
|
|
513
|
+
cursor.execute("""
|
|
514
|
+
SELECT status, CAST(COUNT(*) AS TEXT) as count FROM tasks
|
|
515
|
+
WHERE project_id = ? GROUP BY status
|
|
516
|
+
""", (project_id,))
|
|
517
|
+
task_counts = {str(row['status']): str(row['count']) for row in cursor.fetchall()}
|
|
518
|
+
|
|
519
|
+
# Calculate totals using string values
|
|
520
|
+
total_memories = str(sum(int(count) for count in memory_counts.values()))
|
|
521
|
+
total_tasks = str(sum(int(count) for count in task_counts.values()))
|
|
522
|
+
|
|
523
|
+
return {
|
|
524
|
+
"project": project,
|
|
525
|
+
"memory_counts": memory_counts,
|
|
526
|
+
"task_counts": task_counts,
|
|
527
|
+
"total_memories": total_memories,
|
|
528
|
+
"total_tasks": total_tasks
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
def close(self) -> None:
|
|
532
|
+
"""Close database connection"""
|
|
533
|
+
if self.connection:
|
|
534
|
+
self.connection.close()
|
|
535
|
+
self.connection = None
|
|
536
|
+
|
|
537
|
+
# ==================== NOTIFICATION METHODS ====================
|
|
538
|
+
|
|
539
|
+
def add_notification(self, project_id: str, notification_type: str,
|
|
540
|
+
title: str, message: str, metadata: Optional[Dict[str, Any]] = None) -> str:
|
|
541
|
+
"""Add a new notification"""
|
|
542
|
+
if not self._check_connection() or not self.connection:
|
|
543
|
+
return ""
|
|
544
|
+
|
|
545
|
+
cursor: Cursor = self.connection.cursor()
|
|
546
|
+
notification_id = str(uuid.uuid4())
|
|
547
|
+
|
|
548
|
+
cursor.execute("""
|
|
549
|
+
INSERT INTO notifications (id, project_id, type, title, message, metadata)
|
|
550
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
551
|
+
""", (
|
|
552
|
+
notification_id, project_id, notification_type, title, message,
|
|
553
|
+
json.dumps(metadata or {})
|
|
554
|
+
))
|
|
555
|
+
self.connection.commit()
|
|
556
|
+
return notification_id
|
|
557
|
+
|
|
558
|
+
def get_notifications(self, project_id: Optional[str] = None, is_read: Optional[bool] = None,
|
|
559
|
+
limit: int = 50) -> List[Dict[str, Any]]:
|
|
560
|
+
"""Retrieve notifications with optional filters"""
|
|
561
|
+
if not self._check_connection() or not self.connection:
|
|
562
|
+
return []
|
|
563
|
+
|
|
564
|
+
cursor: Cursor = self.connection.cursor()
|
|
565
|
+
|
|
566
|
+
query = "SELECT * FROM notifications WHERE 1=1"
|
|
567
|
+
params = []
|
|
568
|
+
|
|
569
|
+
if project_id:
|
|
570
|
+
query += " AND project_id = ?"
|
|
571
|
+
params.append(project_id)
|
|
572
|
+
|
|
573
|
+
if is_read is not None:
|
|
574
|
+
query += " AND is_read = ?"
|
|
575
|
+
params.append(int(is_read))
|
|
576
|
+
|
|
577
|
+
query += " ORDER BY created_at DESC LIMIT ?"
|
|
578
|
+
params.append(limit)
|
|
579
|
+
|
|
580
|
+
cursor.execute(query, params)
|
|
581
|
+
return [dict(row) for row in cursor.fetchall()]
|
|
582
|
+
|
|
583
|
+
def mark_notification_read(self, notification_id: str) -> bool:
|
|
584
|
+
"""Mark a notification as read"""
|
|
585
|
+
if not self._check_connection() or not self.connection:
|
|
586
|
+
return False
|
|
587
|
+
|
|
588
|
+
cursor: Cursor = self.connection.cursor()
|
|
589
|
+
cursor.execute("""
|
|
590
|
+
UPDATE notifications SET is_read = 1 WHERE id = ?
|
|
591
|
+
""", (notification_id,))
|
|
592
|
+
self.connection.commit()
|
|
593
|
+
return cursor.rowcount > 0
|
|
594
|
+
|
|
595
|
+
def clear_notifications(self, project_id: Optional[str] = None) -> int:
|
|
596
|
+
"""Clear notifications (optionally for a specific project)"""
|
|
597
|
+
if not self._check_connection() or not self.connection:
|
|
598
|
+
return 0
|
|
599
|
+
|
|
600
|
+
cursor: Cursor = self.connection.cursor()
|
|
601
|
+
if project_id:
|
|
602
|
+
cursor.execute("DELETE FROM notifications WHERE project_id = ?", (project_id,))
|
|
603
|
+
else:
|
|
604
|
+
cursor.execute("DELETE FROM notifications")
|
|
605
|
+
self.connection.commit()
|
|
606
|
+
return cursor.rowcount
|
|
607
|
+
|
|
608
|
+
# ==================== CLEANUP AND OPTIMIZATION METHODS ====================
|
|
609
|
+
|
|
610
|
+
def cleanup_old_data(self, days_old: int = 30) -> Dict[str, int]:
|
|
611
|
+
"""Clean up old memories, logs, and completed tasks"""
|
|
612
|
+
if not self._check_connection() or not self.connection:
|
|
613
|
+
return {"memories_deleted": 0, "tasks_deleted": 0, "notifications_deleted": 0}
|
|
614
|
+
|
|
615
|
+
try:
|
|
616
|
+
cutoff_date = datetime.now() - timedelta(days=days_old)
|
|
617
|
+
cursor: Cursor = self.connection.cursor()
|
|
618
|
+
|
|
619
|
+
# Clean old memories with low importance
|
|
620
|
+
cursor.execute("""
|
|
621
|
+
DELETE FROM memories
|
|
622
|
+
WHERE created_at < ? AND importance_score < 0.3
|
|
623
|
+
""", (cutoff_date,))
|
|
624
|
+
memories_deleted = cursor.rowcount
|
|
625
|
+
|
|
626
|
+
# Clean completed tasks older than cutoff
|
|
627
|
+
cursor.execute("""
|
|
628
|
+
DELETE FROM tasks
|
|
629
|
+
WHERE status = 'completed' AND updated_at < ?
|
|
630
|
+
""", (cutoff_date,))
|
|
631
|
+
tasks_deleted = cursor.rowcount
|
|
632
|
+
|
|
633
|
+
# Clean old read notifications
|
|
634
|
+
cursor.execute("""
|
|
635
|
+
DELETE FROM notifications
|
|
636
|
+
WHERE is_read = 1 AND created_at < ?
|
|
637
|
+
""", (cutoff_date,))
|
|
638
|
+
notifications_deleted = cursor.rowcount
|
|
639
|
+
|
|
640
|
+
self.connection.commit()
|
|
641
|
+
|
|
642
|
+
return {
|
|
643
|
+
"memories_deleted": memories_deleted,
|
|
644
|
+
"tasks_deleted": tasks_deleted,
|
|
645
|
+
"notifications_deleted": notifications_deleted
|
|
646
|
+
}
|
|
647
|
+
except Exception as e:
|
|
648
|
+
logging.error(f"Error during cleanup: {e}")
|
|
649
|
+
return {"memories_deleted": 0, "tasks_deleted": 0, "notifications_deleted": 0}
|
|
650
|
+
|
|
651
|
+
def optimize_memories(self) -> Dict[str, int]:
|
|
652
|
+
"""Analyze and optimize memory storage by removing duplicates"""
|
|
653
|
+
if not self._check_connection() or not self.connection:
|
|
654
|
+
return {"duplicates_merged": 0, "orphaned_relationships": 0}
|
|
655
|
+
|
|
656
|
+
try:
|
|
657
|
+
cursor: Cursor = self.connection.cursor()
|
|
658
|
+
|
|
659
|
+
# Find duplicate memories (same content)
|
|
660
|
+
cursor.execute("""
|
|
661
|
+
SELECT content, COUNT(*) as count, GROUP_CONCAT(id) as ids
|
|
662
|
+
FROM memories
|
|
663
|
+
GROUP BY content
|
|
664
|
+
HAVING count > 1
|
|
665
|
+
""")
|
|
666
|
+
|
|
667
|
+
duplicates = cursor.fetchall()
|
|
668
|
+
merged_count = 0
|
|
669
|
+
|
|
670
|
+
for dup in duplicates:
|
|
671
|
+
ids = dup['ids'].split(',')
|
|
672
|
+
if len(ids) > 1:
|
|
673
|
+
# Keep the most recent, delete others
|
|
674
|
+
ids_to_delete = ids[1:] # Keep first (most recent due to ORDER BY)
|
|
675
|
+
placeholders = ','.join(['?'] * len(ids_to_delete))
|
|
676
|
+
cursor.execute(f"""
|
|
677
|
+
DELETE FROM memories
|
|
678
|
+
WHERE id IN ({placeholders})
|
|
679
|
+
""", ids_to_delete)
|
|
680
|
+
merged_count += cursor.rowcount
|
|
681
|
+
|
|
682
|
+
# Clean up orphaned relationships
|
|
683
|
+
cursor.execute("""
|
|
684
|
+
DELETE FROM knowledge_relationships
|
|
685
|
+
WHERE (from_type = 'memory' AND from_id NOT IN (SELECT id FROM memories))
|
|
686
|
+
OR (to_type = 'memory' AND to_id NOT IN (SELECT id FROM memories))
|
|
687
|
+
OR (from_type = 'task' AND from_id NOT IN (SELECT id FROM tasks))
|
|
688
|
+
OR (to_type = 'task' AND to_id NOT IN (SELECT id FROM tasks))
|
|
689
|
+
""")
|
|
690
|
+
orphaned_relationships = cursor.rowcount
|
|
691
|
+
|
|
692
|
+
self.connection.commit()
|
|
693
|
+
|
|
694
|
+
return {
|
|
695
|
+
"duplicates_merged": merged_count,
|
|
696
|
+
"orphaned_relationships": orphaned_relationships
|
|
697
|
+
}
|
|
698
|
+
except Exception as e:
|
|
699
|
+
logging.error(f"Error during optimization: {e}")
|
|
700
|
+
return {"duplicates_merged": 0, "orphaned_relationships": 0}
|
|
701
|
+
|
|
702
|
+
def get_database_stats(self) -> Dict[str, Any]:
|
|
703
|
+
"""Get comprehensive database statistics"""
|
|
704
|
+
if not self._check_connection() or not self.connection:
|
|
705
|
+
return {}
|
|
706
|
+
|
|
707
|
+
try:
|
|
708
|
+
cursor: Cursor = self.connection.cursor()
|
|
709
|
+
stats = {}
|
|
710
|
+
|
|
711
|
+
# Table counts
|
|
712
|
+
tables = ['projects', 'memories', 'tasks', 'knowledge_relationships', 'sessions', 'notifications']
|
|
713
|
+
for table in tables:
|
|
714
|
+
cursor.execute(f"SELECT COUNT(*) as count FROM {table}")
|
|
715
|
+
stats[f"{table}_count"] = cursor.fetchone()['count']
|
|
716
|
+
|
|
717
|
+
# Database size
|
|
718
|
+
cursor.execute("SELECT page_count * page_size as size FROM pragma_page_count(), pragma_page_size()")
|
|
719
|
+
stats['database_size_bytes'] = cursor.fetchone()['size']
|
|
720
|
+
|
|
721
|
+
# Memory statistics
|
|
722
|
+
cursor.execute("""
|
|
723
|
+
SELECT
|
|
724
|
+
AVG(importance_score) as avg_importance,
|
|
725
|
+
MAX(accessed_count) as max_accessed,
|
|
726
|
+
COUNT(DISTINCT type) as memory_types
|
|
727
|
+
FROM memories
|
|
728
|
+
""")
|
|
729
|
+
memory_stats = cursor.fetchone()
|
|
730
|
+
if memory_stats:
|
|
731
|
+
stats.update({
|
|
732
|
+
'avg_memory_importance': memory_stats['avg_importance'] or 0,
|
|
733
|
+
'max_memory_accessed': memory_stats['max_accessed'] or 0,
|
|
734
|
+
'memory_types_count': memory_stats['memory_types'] or 0
|
|
735
|
+
})
|
|
736
|
+
|
|
737
|
+
# Task statistics
|
|
738
|
+
cursor.execute("""
|
|
739
|
+
SELECT
|
|
740
|
+
status,
|
|
741
|
+
COUNT(*) as count
|
|
742
|
+
FROM tasks
|
|
743
|
+
GROUP BY status
|
|
744
|
+
""")
|
|
745
|
+
task_stats = {row['status']: row['count'] for row in cursor.fetchall()}
|
|
746
|
+
stats['task_status_breakdown'] = task_stats
|
|
747
|
+
|
|
748
|
+
return stats
|
|
749
|
+
except Exception as e:
|
|
750
|
+
logging.error(f"Error getting database stats: {e}")
|
|
751
|
+
return {}
|