145 lines
5.2 KiB
Python
145 lines
5.2 KiB
Python
"""Project memory store: short task summaries for prompt context.
|
||
|
||
Zero third-party dependencies; Python standard library only.
|
||
|
||
Design notes:
|
||
- Summaries are the durable, compacted memory of a project. They are
|
||
deliberately short (goal / conclusion / files changed / open issues)
|
||
so that a prompt can carry recent context without replaying history.
|
||
- ``build_prompt_context`` never returns the whole history: it returns
|
||
at most ``top_k`` recent active summaries, formatted as a prompt prefix.
|
||
- ``deprecate`` is a soft mark (status change, not deletion); ``delete``
|
||
is a physical removal reserved for user-initiated correction/forgetting.
|
||
"""
|
||
|
||
import json
|
||
import sqlite3
|
||
import time
|
||
|
||
|
||
class MemoryStore:
|
||
"""SQLite-backed store for per-project task summaries."""
|
||
|
||
def __init__(self, db_path):
|
||
self.db_path = db_path
|
||
self.conn = sqlite3.connect(db_path)
|
||
self.conn.row_factory = sqlite3.Row
|
||
self.conn.execute(
|
||
"""CREATE TABLE IF NOT EXISTS summaries(
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
project_id TEXT,
|
||
thread_id TEXT,
|
||
goal TEXT,
|
||
conclusion TEXT,
|
||
files_changed TEXT,
|
||
open_issues TEXT,
|
||
status TEXT DEFAULT 'active',
|
||
created_at REAL
|
||
)"""
|
||
)
|
||
self.conn.commit()
|
||
|
||
def add_summary(self, project_id, thread_id, goal, conclusion,
|
||
files_changed=None, open_issues=None):
|
||
"""Insert a summary; returns the new row id."""
|
||
cur = self.conn.execute(
|
||
"""INSERT INTO summaries
|
||
(project_id, thread_id, goal, conclusion,
|
||
files_changed, open_issues, status, created_at)
|
||
VALUES (?, ?, ?, ?, ?, ?, 'active', ?)""",
|
||
(
|
||
project_id,
|
||
thread_id,
|
||
goal,
|
||
conclusion,
|
||
json.dumps(list(files_changed) if files_changed is not None else []),
|
||
json.dumps(list(open_issues) if open_issues is not None else []),
|
||
time.time(),
|
||
),
|
||
)
|
||
self.conn.commit()
|
||
return cur.lastrowid
|
||
|
||
def list_summaries(self, project_id, include_deprecated=False):
|
||
"""List summaries for one project, newest first.
|
||
|
||
By default only ``status='active'`` rows are returned; pass
|
||
``include_deprecated=True`` to also see deprecated ones.
|
||
"""
|
||
if include_deprecated:
|
||
rows = self.conn.execute(
|
||
"""SELECT * FROM summaries WHERE project_id = ?
|
||
ORDER BY created_at DESC, id DESC""",
|
||
(project_id,),
|
||
).fetchall()
|
||
else:
|
||
rows = self.conn.execute(
|
||
"""SELECT * FROM summaries WHERE project_id = ?
|
||
AND status = 'active'
|
||
ORDER BY created_at DESC, id DESC""",
|
||
(project_id,),
|
||
).fetchall()
|
||
return [self._row_to_dict(row) for row in rows]
|
||
|
||
def deprecate(self, summary_id):
|
||
"""Mark a summary deprecated (soft mark, not a physical delete)."""
|
||
self.conn.execute(
|
||
"UPDATE summaries SET status = 'deprecated' WHERE id = ?",
|
||
(summary_id,),
|
||
)
|
||
self.conn.commit()
|
||
|
||
def delete(self, summary_id):
|
||
"""Physically delete a summary.
|
||
|
||
Deletion is a user-initiated action (correction / forgetting):
|
||
it is the only way a summary is removed from storage, as opposed
|
||
to :meth:`deprecate`, which merely hides it from default listings
|
||
and prompt context while keeping the record.
|
||
"""
|
||
self.conn.execute(
|
||
"DELETE FROM summaries WHERE id = ?",
|
||
(summary_id,),
|
||
)
|
||
self.conn.commit()
|
||
|
||
def build_prompt_context(self, project_id, top_k=5):
|
||
"""Build a prompt prefix from the newest ``top_k`` active summaries.
|
||
|
||
Each summary becomes one line::
|
||
|
||
- [thread_id] 目标:...;结论:...;改动文件:...;遗留:...
|
||
|
||
Returns an empty string when the project has no active summaries.
|
||
Never returns the full history: only the newest ``top_k`` rows
|
||
(see :meth:`list_summaries`) are included.
|
||
"""
|
||
summaries = self.list_summaries(project_id)[:top_k]
|
||
if not summaries:
|
||
return ""
|
||
lines = []
|
||
for s in summaries:
|
||
files = "、".join(s["files_changed"]) if s["files_changed"] else "无"
|
||
issues = "、".join(s["open_issues"]) if s["open_issues"] else "无"
|
||
lines.append(
|
||
"- [{thread_id}] 目标:{goal};结论:{conclusion};"
|
||
"改动文件:{files};遗留:{issues}".format(
|
||
thread_id=s["thread_id"],
|
||
goal=s["goal"],
|
||
conclusion=s["conclusion"],
|
||
files=files,
|
||
issues=issues,
|
||
)
|
||
)
|
||
return "\n".join(lines)
|
||
|
||
@staticmethod
|
||
def _row_to_dict(row):
|
||
d = dict(row)
|
||
for key in ("files_changed", "open_issues"):
|
||
try:
|
||
d[key] = json.loads(d[key]) if d[key] else []
|
||
except (TypeError, ValueError):
|
||
d[key] = []
|
||
return d
|