feat(memory): table memory_entries typee + FTS5 (decision/constraint/best-practice/common-error/do-not-use)

This commit is contained in:
2026-07-30 07:42:29 +00:00
parent 8f2fb01acf
commit 0645a8542c
+139
View File
@@ -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()