HumboldtJoker commited on
Commit
f2913c2
·
verified ·
1 Parent(s): 00a42d3

Clean up nested duplicate: v2/v2/engine/memory.py

Browse files
Files changed (1) hide show
  1. v2/v2/engine/memory.py +0 -410
v2/v2/engine/memory.py DELETED
@@ -1,410 +0,0 @@
1
- """Rivet Memory — persistent, per-user memory over SQLite.
2
-
3
- Same shape as Ember's semantic memory (flat records + relevance recall)
4
- but scoped for a code assistant and dependency-free: instead of an
5
- embedding model, recall scores stored memories against the query with
6
- TF-IDF-style keyword overlap, weighted by recency and how often a memory
7
- has recurred. Good enough for "what has this user hit before" — not a
8
- vector store.
9
-
10
- Rivet decides what to remember (calls remember_*). Memory decides what
11
- to surface (recall / session_brief ranks and returns it). Memory never
12
- inspects code or talks to the model — it only stores and ranks text
13
- Rivet hands it.
14
-
15
- One SQLite file per deployment. Each user gets their own table
16
- (memories_<user_id>) so one user's history never leaks into another's
17
- recall results.
18
- """
19
-
20
- import argparse
21
- import hashlib
22
- import math
23
- import os
24
- import re
25
- import sqlite3
26
- from collections import Counter
27
- from dataclasses import dataclass
28
- from datetime import datetime, timezone
29
- from pathlib import Path
30
-
31
-
32
- DEFAULT_DB_PATH = Path(
33
- os.environ.get("RIVET_MEMORY_DB")
34
- or (Path(__file__).resolve().parent.parent / "data" / "rivet_memory.db")
35
- )
36
-
37
- KIND_FILE = "file" # a file the user works on
38
- KIND_QA = "qa" # a question asked + the answer given
39
- KIND_PATTERN = "pattern" # a recurring behavior (e.g. keeps proposing destructive migrations)
40
- KIND_GATE = "gate_correction" # a discipline-gate flag that fired on this user's work
41
- KIND_CONTEXT = "context" # single-value facts: current_branch, recent_pr, ...
42
-
43
- _STOPWORDS = {
44
- "the", "a", "an", "is", "are", "was", "were", "to", "of", "in", "on",
45
- "for", "and", "or", "it", "this", "that", "with", "at", "as", "be",
46
- "by", "from", "how", "what", "why", "do", "does", "did", "i", "you",
47
- "we", "my", "me", "can", "will", "should", "please",
48
- }
49
-
50
- # Vocabulary from DESIGN.md's architecture map — matched against file
51
- # paths so file memories carry cross-cutting tags ("auth", "migration")
52
- # even when the path itself doesn't spell them out.
53
- _DOMAIN_TAGS = {
54
- "auth": ("auth", "jwt", "session", "middleware"),
55
- "migration": ("migration", "migrations", "schema"),
56
- "webhook": ("webhook", "webhooks", "stripe"),
57
- "socket": ("socket", "sockets", "realtime", "pubsub"),
58
- "store": ("store", "stores", "zustand"),
59
- "route": ("route", "routes", "api"),
60
- "job": ("job", "jobs", "pg-boss", "queue"),
61
- }
62
-
63
-
64
- def _now() -> str:
65
- return datetime.now(timezone.utc).isoformat()
66
-
67
-
68
- def _sanitize_user_id(user_id: str) -> str:
69
- slug = re.sub(r"[^a-zA-Z0-9_]", "_", user_id.strip())
70
- if not slug:
71
- raise ValueError("user_id must contain at least one alphanumeric character")
72
- return slug
73
-
74
-
75
- def _tokenize(text: str) -> list:
76
- words = re.findall(r"[a-z0-9_./-]+", text.lower())
77
- return [w for w in words if w not in _STOPWORDS and len(w) > 1]
78
-
79
-
80
- def _content_key(text: str) -> str:
81
- return hashlib.sha1(text.strip().lower().encode("utf-8")).hexdigest()[:16]
82
-
83
-
84
- def _path_tags(file_path: str) -> list:
85
- lowered = file_path.lower()
86
- tags = set()
87
- for tag, needles in _DOMAIN_TAGS.items():
88
- if any(n in lowered for n in needles):
89
- tags.add(tag)
90
- return sorted(tags)
91
-
92
-
93
- def _age_days(iso_timestamp: str, now: datetime) -> float:
94
- try:
95
- ts = datetime.fromisoformat(iso_timestamp)
96
- except ValueError:
97
- return 0.0
98
- if ts.tzinfo is None:
99
- ts = ts.replace(tzinfo=timezone.utc)
100
- return max(0.0, (now - ts).total_seconds() / 86400.0)
101
-
102
-
103
- @dataclass
104
- class MemoryEntry:
105
- id: int
106
- kind: str
107
- key: str
108
- content: str
109
- tags: str
110
- weight: float
111
- hit_count: int
112
- created_at: str
113
- updated_at: str
114
- score: float = 0.0
115
-
116
- @property
117
- def tag_list(self) -> list:
118
- return [t for t in self.tags.split(",") if t] if self.tags else []
119
-
120
-
121
- def _to_entry(row: sqlite3.Row, score: float = 0.0) -> MemoryEntry:
122
- return MemoryEntry(
123
- id=row["id"], kind=row["kind"], key=row["key"], content=row["content"],
124
- tags=row["tags"] or "", weight=row["weight"], hit_count=row["hit_count"],
125
- created_at=row["created_at"], updated_at=row["updated_at"], score=score,
126
- )
127
-
128
-
129
- class MemoryStore:
130
- """Per-user persistent memory backed by SQLite. One table per user."""
131
-
132
- def __init__(self, db_path=DEFAULT_DB_PATH):
133
- self.db_path = Path(db_path)
134
- self.db_path.parent.mkdir(parents=True, exist_ok=True)
135
- self._conn = sqlite3.connect(str(self.db_path))
136
- self._conn.row_factory = sqlite3.Row
137
- self._conn.execute("PRAGMA journal_mode=WAL")
138
- self._conn.execute("""
139
- CREATE TABLE IF NOT EXISTS users (
140
- user_id TEXT PRIMARY KEY,
141
- table_name TEXT NOT NULL,
142
- first_seen TEXT NOT NULL,
143
- last_seen TEXT NOT NULL
144
- )
145
- """)
146
- self._conn.commit()
147
-
148
- def close(self):
149
- self._conn.close()
150
-
151
- def __enter__(self):
152
- return self
153
-
154
- def __exit__(self, *exc):
155
- self.close()
156
-
157
- # -- per-user table management ------------------------------------
158
-
159
- def _table_for(self, user_id: str) -> str:
160
- table = f"memories_{_sanitize_user_id(user_id)}"
161
- now = _now()
162
- row = self._conn.execute(
163
- "SELECT table_name FROM users WHERE user_id = ?", (user_id,)
164
- ).fetchone()
165
- if row is None:
166
- self._conn.execute(
167
- "INSERT INTO users (user_id, table_name, first_seen, last_seen) VALUES (?, ?, ?, ?)",
168
- (user_id, table, now, now),
169
- )
170
- self._conn.execute(f"""
171
- CREATE TABLE IF NOT EXISTS "{table}" (
172
- id INTEGER PRIMARY KEY AUTOINCREMENT,
173
- kind TEXT NOT NULL,
174
- key TEXT NOT NULL,
175
- content TEXT NOT NULL,
176
- tags TEXT DEFAULT '',
177
- weight REAL NOT NULL DEFAULT 1.0,
178
- hit_count INTEGER NOT NULL DEFAULT 1,
179
- created_at TEXT NOT NULL,
180
- updated_at TEXT NOT NULL,
181
- UNIQUE(kind, key)
182
- )
183
- """)
184
- self._conn.commit()
185
- else:
186
- self._conn.execute("UPDATE users SET last_seen = ? WHERE user_id = ?", (now, user_id))
187
- self._conn.commit()
188
- table = row["table_name"]
189
- return table
190
-
191
- # -- writing (Rivet decides what to remember) ----------------------
192
-
193
- def remember(self, user_id: str, kind: str, content: str, key: str = None,
194
- tags=None, weight: float = 1.0) -> None:
195
- """Store or reinforce a memory. Same (kind, key) upserts: content is
196
- replaced with the latest version, weight and hit_count accumulate so
197
- recurring evidence outranks one-off mentions."""
198
- table = self._table_for(user_id)
199
- key = key or _content_key(content)
200
- tag_str = ",".join(sorted(set(tags))) if tags else ""
201
- now = _now()
202
- self._conn.execute(f"""
203
- INSERT INTO "{table}" (kind, key, content, tags, weight, hit_count, created_at, updated_at)
204
- VALUES (?, ?, ?, ?, ?, 1, ?, ?)
205
- ON CONFLICT(kind, key) DO UPDATE SET
206
- content = excluded.content,
207
- tags = CASE WHEN excluded.tags != '' THEN excluded.tags ELSE tags END,
208
- weight = weight + excluded.weight,
209
- hit_count = hit_count + 1,
210
- updated_at = excluded.updated_at
211
- """, (kind, key, content, tag_str, weight, now, now))
212
- self._conn.commit()
213
-
214
- def remember_file(self, user_id: str, file_path: str, note: str = "") -> None:
215
- """A file the user touched, optionally with what they were doing there."""
216
- self.remember(user_id, KIND_FILE, note or file_path, key=file_path,
217
- tags=_path_tags(file_path), weight=1.0)
218
-
219
- def remember_qa(self, user_id: str, intent: str, outcome: str, tags=None) -> None:
220
- """A question + answer, compressed to intent/outcome (not full transcript)."""
221
- content = f"Q: {intent.strip()}\nA: {outcome.strip()}"
222
- self.remember(user_id, KIND_QA, content, key=_content_key(intent), tags=tags, weight=1.0)
223
-
224
- def remember_pattern(self, user_id: str, pattern: str, detail: str = "", tags=None) -> None:
225
- """A recurring behavior worth flagging preemptively next time it shows up."""
226
- self.remember(user_id, KIND_PATTERN, detail or pattern, key=pattern,
227
- tags=tags, weight=2.0)
228
-
229
- def remember_gate_correction(self, user_id: str, rule: str, message: str,
230
- severity: str = "WARN") -> None:
231
- """A discipline-gate flag that fired against this user's suggestion."""
232
- self.remember(user_id, KIND_GATE, message, key=f"{severity}:{rule}",
233
- tags=[rule.lower(), severity.lower()], weight=1.5)
234
-
235
- def set_context(self, user_id: str, key: str, value: str) -> None:
236
- """Single-value session facts: current_branch, recent_pr, etc."""
237
- self.remember(user_id, KIND_CONTEXT, str(value), key=key, tags=[key], weight=1.0)
238
-
239
- def get_context(self, user_id: str, key: str, default=None):
240
- table = self._table_for(user_id)
241
- row = self._conn.execute(
242
- f'SELECT content FROM "{table}" WHERE kind = ? AND key = ?', (KIND_CONTEXT, key)
243
- ).fetchone()
244
- return row["content"] if row else default
245
-
246
- # -- reading (memory decides what to surface) -----------------------
247
-
248
- def recall(self, user_id: str, query: str, top_n: int = 8, kinds=None) -> list:
249
- """Rank stored memories against a free-text query.
250
-
251
- Scoring is TF-IDF-style keyword overlap (computed on the fly over
252
- this user's memories — no external embedding model), boosted by
253
- recency and by how often the memory has recurred.
254
- """
255
- table = self._table_for(user_id)
256
- sql = f'SELECT * FROM "{table}"'
257
- params = []
258
- if kinds:
259
- sql += f" WHERE kind IN ({','.join('?' for _ in kinds)})"
260
- params.extend(kinds)
261
- rows = self._conn.execute(sql, params).fetchall()
262
- if not rows:
263
- return []
264
-
265
- query_terms = _tokenize(query)
266
- if not query_terms:
267
- return self._rank_by_importance(rows, top_n)
268
-
269
- doc_freq = Counter()
270
- doc_terms = []
271
- for row in rows:
272
- terms = Counter(_tokenize(f"{row['key']} {row['content']} {row['tags']}"))
273
- doc_terms.append(terms)
274
- doc_freq.update(terms.keys())
275
-
276
- n_docs = len(rows)
277
- now = datetime.now(timezone.utc)
278
- scored = []
279
- for row, terms in zip(rows, doc_terms):
280
- overlap = sum(
281
- terms[qt] * math.log(1 + n_docs / doc_freq[qt])
282
- for qt in query_terms if terms.get(qt)
283
- )
284
- if overlap <= 0:
285
- continue
286
- recency = 1.0 / (1.0 + _age_days(row["updated_at"], now) / 14.0)
287
- importance = math.log(1 + row["hit_count"]) * row["weight"]
288
- score = overlap * (0.6 + 0.4 * recency) * (1.0 + 0.3 * importance)
289
- scored.append(_to_entry(row, score))
290
-
291
- scored.sort(key=lambda e: e.score, reverse=True)
292
- return scored[:top_n]
293
-
294
- def _rank_by_importance(self, rows, top_n: int) -> list:
295
- now = datetime.now(timezone.utc)
296
- scored = []
297
- for row in rows:
298
- recency = 1.0 / (1.0 + _age_days(row["updated_at"], now) / 14.0)
299
- importance = math.log(1 + row["hit_count"]) * row["weight"]
300
- scored.append(_to_entry(row, importance * recency))
301
- scored.sort(key=lambda e: e.score, reverse=True)
302
- return scored[:top_n]
303
-
304
- def top_files(self, user_id: str, n: int = 5) -> list:
305
- table = self._table_for(user_id)
306
- rows = self._conn.execute(
307
- f'SELECT * FROM "{table}" WHERE kind = ? ORDER BY hit_count DESC, updated_at DESC LIMIT ?',
308
- (KIND_FILE, n),
309
- ).fetchall()
310
- return [_to_entry(r) for r in rows]
311
-
312
- def recent_patterns(self, user_id: str, n: int = 5) -> list:
313
- table = self._table_for(user_id)
314
- rows = self._conn.execute(
315
- f'SELECT * FROM "{table}" WHERE kind = ? ORDER BY weight DESC, updated_at DESC LIMIT ?',
316
- (KIND_PATTERN, n),
317
- ).fetchall()
318
- return [_to_entry(r) for r in rows]
319
-
320
- def recent_gate_corrections(self, user_id: str, n: int = 5) -> list:
321
- table = self._table_for(user_id)
322
- rows = self._conn.execute(
323
- f'SELECT * FROM "{table}" WHERE kind = ? ORDER BY updated_at DESC LIMIT ?',
324
- (KIND_GATE, n),
325
- ).fetchall()
326
- return [_to_entry(r) for r in rows]
327
-
328
- def session_brief(self, user_id: str, first_message: str = "", top_n: int = 8) -> dict:
329
- """Everything Rivet should know when a session starts: memories
330
- relevant to the first message, plus standing context that should
331
- always be visible (frequent files, recurring patterns, gate history,
332
- branch/PR state) regardless of what was asked."""
333
- return {
334
- "relevant": self.recall(user_id, first_message, top_n=top_n) if first_message else [],
335
- "top_files": self.top_files(user_id),
336
- "known_patterns": self.recent_patterns(user_id),
337
- "gate_history": self.recent_gate_corrections(user_id),
338
- "current_branch": self.get_context(user_id, "current_branch"),
339
- "recent_pr": self.get_context(user_id, "recent_pr"),
340
- }
341
-
342
- def to_system_block(self, user_id: str, first_message: str = "") -> str:
343
- """Format session_brief as a text block ready to inject into the
344
- model's system context, in the same spirit as RivetContext in
345
- context_loader.py."""
346
- brief = self.session_brief(user_id, first_message)
347
- parts = ["# USER MEMORY\n"]
348
-
349
- if brief["current_branch"] or brief["recent_pr"]:
350
- parts.append("## Current Work")
351
- if brief["current_branch"]:
352
- parts.append(f"- Branch: {brief['current_branch']}")
353
- if brief["recent_pr"]:
354
- parts.append(f"- Recent PR: {brief['recent_pr']}")
355
- parts.append("")
356
-
357
- if brief["top_files"]:
358
- parts.append("## Frequently Touched Files")
359
- parts.extend(f"- {e.key}: {e.content}" for e in brief["top_files"])
360
- parts.append("")
361
-
362
- if brief["known_patterns"]:
363
- parts.append("## Recurring Patterns (flag preemptively)")
364
- parts.extend(f"- {e.key}: {e.content}" for e in brief["known_patterns"])
365
- parts.append("")
366
-
367
- if brief["gate_history"]:
368
- parts.append("## Discipline Gate History")
369
- parts.extend(f"- {e.content}" for e in brief["gate_history"])
370
- parts.append("")
371
-
372
- if brief["relevant"]:
373
- parts.append("## Relevant To This Question")
374
- parts.extend(f"- {e.content}" for e in brief["relevant"])
375
- parts.append("")
376
-
377
- return "\n".join(parts).strip()
378
-
379
-
380
- def _cli():
381
- parser = argparse.ArgumentParser(description="Rivet memory database utility")
382
- parser.add_argument("command", choices=["init", "demo"])
383
- parser.add_argument("--db", default=str(DEFAULT_DB_PATH))
384
- args = parser.parse_args()
385
-
386
- if args.command == "init":
387
- store = MemoryStore(args.db)
388
- print(f"Memory database ready at {store.db_path}")
389
- store.close()
390
- return
391
-
392
- # demo: quick smoke test exercising the write/recall paths
393
- store = MemoryStore(args.db)
394
- user = "demo_user"
395
- store.remember_file(user, "src/routes/auth.ts", "adding refresh-token rotation")
396
- store.remember_qa(user, "why does the webhook route 500 on missing signature",
397
- "STRIPE_SECRET falls back to '' — Audit C2, reject instead of skip")
398
- store.remember_pattern(user, "proposes_destructive_migration",
399
- "suggested DROP COLUMN twice — always steer to additive migrations")
400
- store.remember_gate_correction(user, "webhook_bypass",
401
- "Webhook signature bypass pattern (Audit C2) flagged and blocked",
402
- severity="BLOCK")
403
- store.set_context(user, "current_branch", "feat/refresh-token-rotation")
404
-
405
- print(store.to_system_block(user, first_message="can I touch the webhook signature check?"))
406
- store.close()
407
-
408
-
409
- if __name__ == "__main__":
410
- _cli()