cloud: production control plane (API, auth/RBAC, queue, sandbox, memory/KB, fallback/cost, webhooks)
This commit is contained in:
1 parent
b97c34465f
commit
b74d99e959
17 files changed
+3420
No files matched your search
+144
@@ -0,0 +1,144 @@
|
||||
"""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
|
||||
Reference in new issue
Block a user