From 0645a8542c02de11ad1bc6dd0dafcebc5282a6b3 Mon Sep 17 00:00:00 2001 From: bolbol Date: Thu, 30 Jul 2026 07:42:29 +0000 Subject: [PATCH] feat(memory): table memory_entries typee + FTS5 (decision/constraint/best-practice/common-error/do-not-use) --- app/models.py | 139 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/app/models.py b/app/models.py index 4416d69..b574f71 100644 --- a/app/models.py +++ b/app/models.py @@ -41,6 +41,43 @@ async def init_db(): description TEXT ) """) + await db.execute(""" + CREATE TABLE IF NOT EXISTS memory_entries ( + id TEXT PRIMARY KEY, + scope TEXT NOT NULL, + project TEXT, + type TEXT NOT NULL, + title TEXT NOT NULL, + body TEXT NOT NULL, + tags TEXT, + status TEXT NOT NULL DEFAULT 'active', + superseded_by TEXT, + created_by TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """) + await db.execute(""" + CREATE VIRTUAL TABLE IF NOT EXISTS memory_entries_fts USING fts5( + title, body, tags, content='memory_entries', content_rowid='rowid' + ) + """) + await db.execute(""" + CREATE TRIGGER IF NOT EXISTS memory_entries_ai AFTER INSERT ON memory_entries BEGIN + INSERT INTO memory_entries_fts(rowid, title, body, tags) VALUES (new.rowid, new.title, new.body, new.tags); + END + """) + await db.execute(""" + CREATE TRIGGER IF NOT EXISTS memory_entries_ad AFTER DELETE ON memory_entries BEGIN + INSERT INTO memory_entries_fts(memory_entries_fts, rowid, title, body, tags) VALUES('delete', old.rowid, old.title, old.body, old.tags); + END + """) + await db.execute(""" + CREATE TRIGGER IF NOT EXISTS memory_entries_au AFTER UPDATE ON memory_entries BEGIN + INSERT INTO memory_entries_fts(memory_entries_fts, rowid, title, body, tags) VALUES('delete', old.rowid, old.title, old.body, old.tags); + INSERT INTO memory_entries_fts(rowid, title, body, tags) VALUES (new.rowid, new.title, new.body, new.tags); + END + """) await db.commit() async def get_rule(scope: str) -> Optional[Dict[str, Any]]: @@ -132,3 +169,105 @@ async def get_audit_logs(limit: int = 50) -> List[Dict[str, Any]]: async with db.execute("SELECT id, agent_name, scope, timestamp, ip_address FROM audit_log ORDER BY timestamp DESC LIMIT ?", (limit,)) as cursor: rows = await cursor.fetchall() return [dict(row) for row in rows] + + +_TYPE_ABBR = { + "decision": "dec", + "constraint": "cst", + "best-practice": "bp", + "common-error": "err", + "do-not-use": "dnu", +} + +async def _next_entry_id(db, scope: str, type_: str) -> str: + abbr = _TYPE_ABBR.get(type_, type_) + prefix = f"{scope}-{abbr}-" + async with db.execute( + "SELECT id FROM memory_entries WHERE scope=? AND type=? ORDER BY created_at DESC, id DESC LIMIT 1", + (scope, type_) + ) as cur: + row = await cur.fetchone() + n = 1 + if row: + try: + n = int(row[0].rsplit("-", 1)[-1]) + 1 + except ValueError: + n = 1 + return f"{prefix}{n:04d}" + +async def add_memory_entry(scope: str, type_: str, title: str, body: str, created_by: str, + project: Optional[str] = None, tags: Optional[List[str]] = None) -> Dict[str, Any]: + tags_json = json.dumps(tags or []) + async with aiosqlite.connect(DB_PATH) as db: + entry_id = await _next_entry_id(db, scope, type_) + await db.execute( + """INSERT INTO memory_entries (id, scope, project, type, title, body, tags, created_by) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", + (entry_id, scope, project, type_, title, body, tags_json, created_by) + ) + await db.commit() + return {"id": entry_id, "scope": scope, "project": project, "type": type_, + "title": title, "body": body, "tags": tags or [], "status": "active", "created_by": created_by} + +async def list_memory_entries(scope: str, project: Optional[str] = None, + type_: Optional[str] = None, status: str = "active") -> List[Dict[str, Any]]: + query = ("SELECT id, scope, project, type, title, body, tags, status, superseded_by, " + "created_by, created_at, updated_at FROM memory_entries WHERE scope=?") + params: List[Any] = [scope] + if project is not None: + query += " AND project=?" + params.append(project) + if type_ is not None: + query += " AND type=?" + params.append(type_) + if status: + query += " AND status=?" + params.append(status) + query += " ORDER BY created_at DESC" + async with aiosqlite.connect(DB_PATH) as db: + db.row_factory = aiosqlite.Row + async with db.execute(query, params) as cursor: + rows = await cursor.fetchall() + result = [] + for row in rows: + d = dict(row) + d["tags"] = json.loads(d["tags"]) if d["tags"] else [] + result.append(d) + return result + +async def search_memory_entries(query_text: str, scope: Optional[str] = None) -> List[Dict[str, Any]]: + sql = ("SELECT me.id, me.scope, me.project, me.type, me.title, me.body, me.tags, " + "me.status, me.created_by, me.created_at " + "FROM memory_entries_fts JOIN memory_entries me ON me.rowid = memory_entries_fts.rowid " + "WHERE memory_entries_fts MATCH ? AND me.status = 'active'") + params: List[Any] = [query_text] + if scope: + sql += " AND me.scope = ?" + params.append(scope) + sql += " ORDER BY rank" + async with aiosqlite.connect(DB_PATH) as db: + db.row_factory = aiosqlite.Row + async with db.execute(sql, params) as cursor: + rows = await cursor.fetchall() + result = [] + for row in rows: + d = dict(row) + d["tags"] = json.loads(d["tags"]) if d["tags"] else [] + result.append(d) + return result + +async def supersede_memory_entry(entry_id: str, superseded_by: str): + async with aiosqlite.connect(DB_PATH) as db: + await db.execute( + "UPDATE memory_entries SET status='superseded', superseded_by=?, updated_at=CURRENT_TIMESTAMP WHERE id=?", + (superseded_by, entry_id) + ) + await db.commit() + +async def archive_memory_entry(entry_id: str): + async with aiosqlite.connect(DB_PATH) as db: + await db.execute( + "UPDATE memory_entries SET status='archived', updated_at=CURRENT_TIMESTAMP WHERE id=?", + (entry_id,) + ) + await db.commit()