agent-memory-engine 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. agent_memory/__init__.py +33 -0
  2. agent_memory/cli.py +142 -0
  3. agent_memory/client.py +355 -0
  4. agent_memory/config.py +28 -0
  5. agent_memory/controller/__init__.py +15 -0
  6. agent_memory/controller/conflict.py +95 -0
  7. agent_memory/controller/consolidation.py +136 -0
  8. agent_memory/controller/forgetting.py +29 -0
  9. agent_memory/controller/router.py +62 -0
  10. agent_memory/controller/trust.py +31 -0
  11. agent_memory/embedding/__init__.py +5 -0
  12. agent_memory/embedding/base.py +11 -0
  13. agent_memory/embedding/local_provider.py +38 -0
  14. agent_memory/embedding/openai_provider.py +11 -0
  15. agent_memory/extraction/__init__.py +5 -0
  16. agent_memory/extraction/entity_extractor.py +13 -0
  17. agent_memory/extraction/pipeline.py +123 -0
  18. agent_memory/extraction/prompts.py +40 -0
  19. agent_memory/governance/__init__.py +6 -0
  20. agent_memory/governance/audit.py +14 -0
  21. agent_memory/governance/export.py +72 -0
  22. agent_memory/governance/health.py +40 -0
  23. agent_memory/interfaces/__init__.py +14 -0
  24. agent_memory/interfaces/mcp_server.py +128 -0
  25. agent_memory/interfaces/rest_api.py +71 -0
  26. agent_memory/llm/__init__.py +5 -0
  27. agent_memory/llm/base.py +23 -0
  28. agent_memory/llm/ollama_client.py +64 -0
  29. agent_memory/llm/openai_client.py +94 -0
  30. agent_memory/models.py +149 -0
  31. agent_memory/storage/__init__.py +4 -0
  32. agent_memory/storage/base.py +59 -0
  33. agent_memory/storage/schema.sql +125 -0
  34. agent_memory/storage/sqlite_backend.py +762 -0
  35. agent_memory_engine-0.1.0.dist-info/METADATA +228 -0
  36. agent_memory_engine-0.1.0.dist-info/RECORD +39 -0
  37. agent_memory_engine-0.1.0.dist-info/WHEEL +4 -0
  38. agent_memory_engine-0.1.0.dist-info/entry_points.txt +2 -0
  39. agent_memory_engine-0.1.0.dist-info/licenses/LICENSE +22 -0
@@ -0,0 +1,762 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone
4
+ import importlib
5
+ import json
6
+ from math import sqrt
7
+ from pathlib import Path
8
+ import re
9
+ import sqlite3
10
+ from typing import Any
11
+
12
+ from agent_memory.models import MemoryItem, MemoryLayer, MemoryType, RelationEdge, RelationType
13
+
14
+
15
+ def _utcnow_iso() -> str:
16
+ return datetime.now(timezone.utc).isoformat()
17
+
18
+
19
+ def _serialize_datetime(value: datetime | None) -> str | None:
20
+ return value.isoformat() if value else None
21
+
22
+
23
+ def _deserialize_datetime(value: str | None) -> datetime | None:
24
+ return datetime.fromisoformat(value) if value else None
25
+
26
+
27
+ def _cosine_similarity(left: list[float], right: list[float]) -> float:
28
+ if not left or not right:
29
+ return 0.0
30
+ size = min(len(left), len(right))
31
+ left_trimmed = left[:size]
32
+ right_trimmed = right[:size]
33
+ numerator = sum(a * b for a, b in zip(left_trimmed, right_trimmed, strict=False))
34
+ left_norm = sqrt(sum(a * a for a in left_trimmed))
35
+ right_norm = sqrt(sum(b * b for b in right_trimmed))
36
+ if left_norm == 0 or right_norm == 0:
37
+ return 0.0
38
+ return numerator / (left_norm * right_norm)
39
+
40
+
41
+ def _normalize_embedding(values: list[float]) -> list[float]:
42
+ return [float(value) for value in values]
43
+
44
+
45
+ def _build_fts_query(query: str) -> str:
46
+ terms = re.findall(r"[\w\u4e00-\u9fff-]+", query.lower())
47
+ if not terms:
48
+ return ""
49
+ return " OR ".join(f'"{term}"' for term in terms)
50
+
51
+
52
+ class SQLiteBackend:
53
+ def __init__(self, database_path: str = ":memory:", prefer_sqlite_vec: bool = True) -> None:
54
+ self.database_path = database_path
55
+ self.prefer_sqlite_vec = prefer_sqlite_vec
56
+ self.connection = sqlite3.connect(database_path, check_same_thread=False)
57
+ self.connection.row_factory = sqlite3.Row
58
+ self.connection.execute("PRAGMA foreign_keys = ON")
59
+ if database_path != ":memory:":
60
+ self.connection.execute("PRAGMA journal_mode = WAL")
61
+ self._sqlite_vec = None
62
+ self._sqlite_vec_enabled = False
63
+ self._bootstrap()
64
+ if self.prefer_sqlite_vec:
65
+ self._try_enable_sqlite_vec()
66
+
67
+ def _bootstrap(self) -> None:
68
+ schema_path = Path(__file__).with_name("schema.sql")
69
+ self.connection.executescript(schema_path.read_text())
70
+ self.connection.commit()
71
+
72
+ def close(self) -> None:
73
+ self.connection.close()
74
+
75
+ def add_memory(self, item: MemoryItem) -> MemoryItem:
76
+ payload = self._memory_to_row(item)
77
+ cursor = self.connection.execute(
78
+ """
79
+ INSERT INTO memories (
80
+ id, content, memory_type, created_at, last_accessed, access_count,
81
+ valid_from, valid_until, trust_score, importance, layer, decay_rate,
82
+ source_id, causal_parent_id, supersedes_id, entity_refs_json, tags_json, deleted_at
83
+ ) VALUES (
84
+ :id, :content, :memory_type, :created_at, :last_accessed, :access_count,
85
+ :valid_from, :valid_until, :trust_score, :importance, :layer, :decay_rate,
86
+ :source_id, :causal_parent_id, :supersedes_id, :entity_refs_json, :tags_json, :deleted_at
87
+ )
88
+ """,
89
+ payload,
90
+ )
91
+ memory_rowid = int(cursor.lastrowid)
92
+ self.connection.execute(
93
+ "INSERT INTO memory_vectors (memory_id, memory_rowid, embedding_json) VALUES (?, ?, ?)",
94
+ (item.id, memory_rowid, json.dumps(_normalize_embedding(item.embedding))),
95
+ )
96
+ self.connection.executemany(
97
+ "INSERT OR IGNORE INTO entity_index (entity, memory_id) VALUES (?, ?)",
98
+ [(entity.lower(), item.id) for entity in item.entity_refs],
99
+ )
100
+ self._upsert_vec_index_row(item=item, memory_rowid=memory_rowid)
101
+ self._append_evolution(item.id, "created", {"source_id": item.source_id})
102
+ self._append_audit("system", "create", "memory", item.id, {"source_id": item.source_id})
103
+ self.connection.commit()
104
+ return item
105
+
106
+ def get_memory(self, memory_id: str) -> MemoryItem | None:
107
+ row = self.connection.execute(
108
+ """
109
+ SELECT m.*, v.embedding_json
110
+ FROM memories m
111
+ LEFT JOIN memory_vectors v ON v.memory_id = m.id
112
+ WHERE m.id = ?
113
+ """,
114
+ (memory_id,),
115
+ ).fetchone()
116
+ if row is None:
117
+ return None
118
+ return self._row_to_memory(row)
119
+
120
+ def update_memory(self, item: MemoryItem) -> MemoryItem:
121
+ payload = self._memory_to_row(item)
122
+ self.connection.execute(
123
+ """
124
+ UPDATE memories SET
125
+ content = :content,
126
+ memory_type = :memory_type,
127
+ created_at = :created_at,
128
+ last_accessed = :last_accessed,
129
+ access_count = :access_count,
130
+ valid_from = :valid_from,
131
+ valid_until = :valid_until,
132
+ trust_score = :trust_score,
133
+ importance = :importance,
134
+ layer = :layer,
135
+ decay_rate = :decay_rate,
136
+ source_id = :source_id,
137
+ causal_parent_id = :causal_parent_id,
138
+ supersedes_id = :supersedes_id,
139
+ entity_refs_json = :entity_refs_json,
140
+ tags_json = :tags_json,
141
+ deleted_at = :deleted_at
142
+ WHERE id = :id
143
+ """,
144
+ payload,
145
+ )
146
+ self.connection.execute(
147
+ "UPDATE memory_vectors SET embedding_json = ? WHERE memory_id = ?",
148
+ (json.dumps(_normalize_embedding(item.embedding)), item.id),
149
+ )
150
+ self.connection.execute("DELETE FROM entity_index WHERE memory_id = ?", (item.id,))
151
+ self.connection.executemany(
152
+ "INSERT OR IGNORE INTO entity_index (entity, memory_id) VALUES (?, ?)",
153
+ [(entity.lower(), item.id) for entity in item.entity_refs],
154
+ )
155
+ row = self.connection.execute(
156
+ "SELECT memory_rowid FROM memory_vectors WHERE memory_id = ?",
157
+ (item.id,),
158
+ ).fetchone()
159
+ if row is not None:
160
+ self._delete_vec_index_row(int(row["memory_rowid"]))
161
+ self._upsert_vec_index_row(item=item, memory_rowid=int(row["memory_rowid"]))
162
+ self._append_evolution(item.id, "updated", {"source_id": item.source_id})
163
+ self._append_audit("system", "update", "memory", item.id, {"source_id": item.source_id})
164
+ self.connection.commit()
165
+ return item
166
+
167
+ def soft_delete_memory(self, memory_id: str) -> bool:
168
+ deleted_at = _utcnow_iso()
169
+ cursor = self.connection.execute(
170
+ "UPDATE memories SET deleted_at = ? WHERE id = ? AND deleted_at IS NULL",
171
+ (deleted_at, memory_id),
172
+ )
173
+ if cursor.rowcount == 0:
174
+ return False
175
+ row = self.connection.execute(
176
+ "SELECT memory_rowid FROM memory_vectors WHERE memory_id = ?",
177
+ (memory_id,),
178
+ ).fetchone()
179
+ if row is not None:
180
+ self._delete_vec_index_row(int(row["memory_rowid"]))
181
+ self._append_evolution(memory_id, "deleted", {"deleted_at": deleted_at})
182
+ self._append_audit("system", "delete", "memory", memory_id, {"deleted_at": deleted_at})
183
+ self.connection.commit()
184
+ return True
185
+
186
+ def touch_memory(self, memory_id: str) -> None:
187
+ accessed_at = _utcnow_iso()
188
+ self.connection.execute(
189
+ """
190
+ UPDATE memories
191
+ SET access_count = access_count + 1,
192
+ last_accessed = ?
193
+ WHERE id = ? AND deleted_at IS NULL
194
+ """,
195
+ (accessed_at, memory_id),
196
+ )
197
+ self.connection.commit()
198
+
199
+ def search_full_text(self, query: str, limit: int = 10, memory_type: str | None = None) -> list[tuple[MemoryItem, float]]:
200
+ fts_query = _build_fts_query(query)
201
+ if not fts_query:
202
+ return []
203
+ params: list[object] = [fts_query]
204
+ memory_type_clause = ""
205
+ if memory_type:
206
+ memory_type_clause = "AND m.memory_type = ?"
207
+ params.append(memory_type)
208
+ params.append(limit)
209
+ rows = self.connection.execute(
210
+ f"""
211
+ SELECT m.*, v.embedding_json, bm25(memories_fts) AS rank_score
212
+ FROM memories_fts
213
+ JOIN memories m ON m.rowid = memories_fts.rowid
214
+ LEFT JOIN memory_vectors v ON v.memory_id = m.id
215
+ WHERE memories_fts MATCH ?
216
+ AND m.deleted_at IS NULL
217
+ {memory_type_clause}
218
+ ORDER BY rank_score
219
+ LIMIT ?
220
+ """,
221
+ params,
222
+ ).fetchall()
223
+ return [(self._row_to_memory(row), 1.0 / (1.0 + abs(row["rank_score"]))) for row in rows]
224
+
225
+ def search_by_entities(self, entities: list[str], limit: int = 10, memory_type: str | None = None) -> list[tuple[MemoryItem, float]]:
226
+ if not entities:
227
+ return []
228
+ placeholders = ", ".join("?" for _ in entities)
229
+ params: list[object] = [entity.lower() for entity in entities]
230
+ memory_type_clause = ""
231
+ if memory_type:
232
+ memory_type_clause = "AND m.memory_type = ?"
233
+ params.append(memory_type)
234
+ params.append(limit)
235
+ rows = self.connection.execute(
236
+ f"""
237
+ SELECT m.*, v.embedding_json, COUNT(*) AS entity_hits
238
+ FROM entity_index e
239
+ JOIN memories m ON m.id = e.memory_id
240
+ LEFT JOIN memory_vectors v ON v.memory_id = m.id
241
+ WHERE e.entity IN ({placeholders})
242
+ AND m.deleted_at IS NULL
243
+ {memory_type_clause}
244
+ GROUP BY m.id
245
+ ORDER BY entity_hits DESC, m.created_at DESC
246
+ LIMIT ?
247
+ """,
248
+ params,
249
+ ).fetchall()
250
+ return [(self._row_to_memory(row), float(row["entity_hits"])) for row in rows]
251
+
252
+ def search_by_vector(
253
+ self,
254
+ embedding: list[float],
255
+ limit: int = 10,
256
+ memory_type: str | None = None,
257
+ ) -> list[tuple[MemoryItem, float]]:
258
+ if self._sqlite_vec_enabled:
259
+ results = self._search_by_vector_sqlite_vec(embedding=embedding, limit=limit, memory_type=memory_type)
260
+ if results:
261
+ return results
262
+ return self._search_by_vector_fallback(embedding=embedding, limit=limit, memory_type=memory_type)
263
+
264
+ def _search_by_vector_fallback(
265
+ self,
266
+ embedding: list[float],
267
+ limit: int = 10,
268
+ memory_type: str | None = None,
269
+ ) -> list[tuple[MemoryItem, float]]:
270
+ params: list[object] = []
271
+ memory_type_clause = ""
272
+ if memory_type:
273
+ memory_type_clause = "AND m.memory_type = ?"
274
+ params.append(memory_type)
275
+ rows = self.connection.execute(
276
+ f"""
277
+ SELECT m.*, v.embedding_json
278
+ FROM memories m
279
+ JOIN memory_vectors v ON v.memory_id = m.id
280
+ WHERE m.deleted_at IS NULL
281
+ {memory_type_clause}
282
+ """,
283
+ params,
284
+ ).fetchall()
285
+ scored = []
286
+ for row in rows:
287
+ stored_embedding = json.loads(row["embedding_json"])
288
+ score = _cosine_similarity(embedding, stored_embedding)
289
+ scored.append((self._row_to_memory(row), score))
290
+ scored.sort(key=lambda item: item[1], reverse=True)
291
+ return scored[:limit]
292
+
293
+ def _search_by_vector_sqlite_vec(
294
+ self,
295
+ embedding: list[float],
296
+ limit: int = 10,
297
+ memory_type: str | None = None,
298
+ ) -> list[tuple[MemoryItem, float]]:
299
+ serialized = self._serialize_embedding(embedding)
300
+ if serialized is None:
301
+ return []
302
+ params: list[Any] = [serialized, limit]
303
+ memory_type_clause = ""
304
+ if memory_type:
305
+ memory_type_clause = "AND v.memory_type = ?"
306
+ params.append(memory_type)
307
+ rows = self.connection.execute(
308
+ f"""
309
+ SELECT
310
+ m.*,
311
+ mv.embedding_json,
312
+ v.distance AS vec_distance
313
+ FROM memory_vec_index v
314
+ JOIN memories m ON m.rowid = v.memory_rowid
315
+ JOIN memory_vectors mv ON mv.memory_id = m.id
316
+ WHERE v.embedding MATCH ?
317
+ AND k = ?
318
+ {memory_type_clause}
319
+ AND m.deleted_at IS NULL
320
+ ORDER BY v.distance ASC
321
+ """,
322
+ params,
323
+ ).fetchall()
324
+ return [(self._row_to_memory(row), 1.0 / (1.0 + float(row["vec_distance"]))) for row in rows]
325
+
326
+ def trace_ancestors(self, memory_id: str, max_depth: int = 10) -> list[MemoryItem]:
327
+ rows = self.connection.execute(
328
+ """
329
+ WITH RECURSIVE ancestors(id, depth) AS (
330
+ SELECT causal_parent_id, 1
331
+ FROM memories
332
+ WHERE id = ? AND causal_parent_id IS NOT NULL
333
+ UNION ALL
334
+ SELECT m.causal_parent_id, a.depth + 1
335
+ FROM ancestors a
336
+ JOIN memories m ON m.id = a.id
337
+ WHERE a.depth < ? AND m.causal_parent_id IS NOT NULL
338
+ )
339
+ SELECT m.*, v.embedding_json, a.depth
340
+ FROM ancestors a
341
+ JOIN memories m ON m.id = a.id
342
+ LEFT JOIN memory_vectors v ON v.memory_id = m.id
343
+ WHERE m.deleted_at IS NULL
344
+ ORDER BY a.depth ASC
345
+ """,
346
+ (memory_id, max_depth),
347
+ ).fetchall()
348
+ return [self._row_to_memory(row) for row in rows]
349
+
350
+ def list_memories(self, include_deleted: bool = False) -> list[MemoryItem]:
351
+ deleted_clause = "" if include_deleted else "WHERE m.deleted_at IS NULL"
352
+ rows = self.connection.execute(
353
+ f"""
354
+ SELECT m.*, v.embedding_json
355
+ FROM memories m
356
+ LEFT JOIN memory_vectors v ON v.memory_id = m.id
357
+ {deleted_clause}
358
+ ORDER BY m.created_at DESC
359
+ """
360
+ ).fetchall()
361
+ return [self._row_to_memory(row) for row in rows]
362
+
363
+ def add_relation(self, edge: RelationEdge) -> bool:
364
+ cursor = self.connection.execute(
365
+ """
366
+ INSERT OR IGNORE INTO relations (source_id, target_id, relation_type, created_at)
367
+ VALUES (?, ?, ?, ?)
368
+ """,
369
+ (
370
+ edge.source_id,
371
+ edge.target_id,
372
+ edge.relation_type.value,
373
+ _serialize_datetime(edge.created_at),
374
+ ),
375
+ )
376
+ self.connection.commit()
377
+ return cursor.rowcount > 0
378
+
379
+ def list_relations(self, memory_id: str | None = None) -> list[RelationEdge]:
380
+ if memory_id is None:
381
+ rows = self.connection.execute(
382
+ "SELECT source_id, target_id, relation_type, created_at FROM relations ORDER BY created_at DESC"
383
+ ).fetchall()
384
+ else:
385
+ rows = self.connection.execute(
386
+ """
387
+ SELECT source_id, target_id, relation_type, created_at
388
+ FROM relations
389
+ WHERE source_id = ? OR target_id = ?
390
+ ORDER BY created_at DESC
391
+ """,
392
+ (memory_id, memory_id),
393
+ ).fetchall()
394
+ return [
395
+ RelationEdge(
396
+ source_id=row["source_id"],
397
+ target_id=row["target_id"],
398
+ relation_type=RelationType(row["relation_type"]),
399
+ created_at=_deserialize_datetime(row["created_at"]) or datetime.now(timezone.utc),
400
+ )
401
+ for row in rows
402
+ ]
403
+
404
+ def trace_descendants(self, memory_id: str, max_depth: int = 10) -> list[MemoryItem]:
405
+ rows = self.connection.execute(
406
+ """
407
+ WITH RECURSIVE descendants(id, depth) AS (
408
+ SELECT id, 1
409
+ FROM memories
410
+ WHERE causal_parent_id = ?
411
+ UNION ALL
412
+ SELECT m.id, d.depth + 1
413
+ FROM descendants d
414
+ JOIN memories m ON m.causal_parent_id = d.id
415
+ WHERE d.depth < ?
416
+ )
417
+ SELECT m.*, v.embedding_json, d.depth
418
+ FROM descendants d
419
+ JOIN memories m ON m.id = d.id
420
+ LEFT JOIN memory_vectors v ON v.memory_id = m.id
421
+ WHERE m.deleted_at IS NULL
422
+ ORDER BY d.depth ASC, m.created_at ASC
423
+ """,
424
+ (memory_id, max_depth),
425
+ ).fetchall()
426
+ return [self._row_to_memory(row) for row in rows]
427
+
428
+ def relation_exists_between(
429
+ self,
430
+ left_id: str,
431
+ right_id: str,
432
+ relation_types: list[str] | None = None,
433
+ ) -> bool:
434
+ if relation_types:
435
+ placeholders = ", ".join("?" for _ in relation_types)
436
+ row = self.connection.execute(
437
+ f"""
438
+ SELECT 1
439
+ FROM relations
440
+ WHERE ((source_id = ? AND target_id = ?) OR (source_id = ? AND target_id = ?))
441
+ AND relation_type IN ({placeholders})
442
+ LIMIT 1
443
+ """,
444
+ (left_id, right_id, right_id, left_id, *relation_types),
445
+ ).fetchone()
446
+ else:
447
+ row = self.connection.execute(
448
+ """
449
+ SELECT 1
450
+ FROM relations
451
+ WHERE ((source_id = ? AND target_id = ?) OR (source_id = ? AND target_id = ?))
452
+ LIMIT 1
453
+ """,
454
+ (left_id, right_id, right_id, left_id),
455
+ ).fetchone()
456
+ return row is not None
457
+
458
+ def get_evolution_events(self, memory_id: str | None = None, limit: int = 100) -> list[dict[str, object]]:
459
+ if memory_id is None:
460
+ rows = self.connection.execute(
461
+ """
462
+ SELECT memory_id, event_type, payload_json, created_at
463
+ FROM evolution_log
464
+ ORDER BY created_at DESC
465
+ LIMIT ?
466
+ """,
467
+ (limit,),
468
+ ).fetchall()
469
+ else:
470
+ rows = self.connection.execute(
471
+ """
472
+ SELECT memory_id, event_type, payload_json, created_at
473
+ FROM evolution_log
474
+ WHERE memory_id = ?
475
+ ORDER BY created_at DESC
476
+ LIMIT ?
477
+ """,
478
+ (memory_id, limit),
479
+ ).fetchall()
480
+ return [
481
+ {
482
+ "memory_id": row["memory_id"],
483
+ "event_type": row["event_type"],
484
+ "payload": json.loads(row["payload_json"]),
485
+ "created_at": row["created_at"],
486
+ }
487
+ for row in rows
488
+ ]
489
+
490
+ def get_audit_events(self, limit: int = 100) -> list[dict[str, object]]:
491
+ rows = self.connection.execute(
492
+ """
493
+ SELECT actor, operation, target_type, target_id, payload_json, created_at
494
+ FROM audit_log
495
+ ORDER BY created_at DESC
496
+ LIMIT ?
497
+ """,
498
+ (limit,),
499
+ ).fetchall()
500
+ return [
501
+ {
502
+ "actor": row["actor"],
503
+ "operation": row["operation"],
504
+ "target_type": row["target_type"],
505
+ "target_id": row["target_id"],
506
+ "payload": json.loads(row["payload_json"]),
507
+ "created_at": row["created_at"],
508
+ }
509
+ for row in rows
510
+ ]
511
+
512
+ def health_snapshot(self) -> dict[str, float | int]:
513
+ row = self.connection.execute(
514
+ """
515
+ SELECT
516
+ COUNT(*) AS total_memories,
517
+ SUM(CASE WHEN deleted_at IS NULL THEN 1 ELSE 0 END) AS active_memories,
518
+ AVG(CASE WHEN deleted_at IS NULL THEN trust_score END) AS average_trust_score,
519
+ SUM(
520
+ CASE
521
+ WHEN deleted_at IS NULL
522
+ AND julianday('now') - julianday(last_accessed) > 30
523
+ THEN 1 ELSE 0
524
+ END
525
+ ) AS stale_memories
526
+ FROM memories
527
+ """
528
+ ).fetchone()
529
+ orphan_row = self.connection.execute(
530
+ """
531
+ SELECT COUNT(*) AS orphan_memories
532
+ FROM memories m
533
+ WHERE m.deleted_at IS NULL
534
+ AND NOT EXISTS (
535
+ SELECT 1 FROM relations r
536
+ WHERE r.source_id = m.id OR r.target_id = m.id
537
+ )
538
+ AND m.access_count <= 1
539
+ """
540
+ ).fetchone()
541
+ conflict_row = self.connection.execute(
542
+ """
543
+ SELECT COUNT(*) AS unresolved_conflicts
544
+ FROM relations
545
+ WHERE relation_type = 'contradicts'
546
+ """
547
+ ).fetchone()
548
+ audit_row = self.connection.execute("SELECT COUNT(*) AS audit_events FROM audit_log").fetchone()
549
+ total = int(row["active_memories"] or 0)
550
+ stale = int(row["stale_memories"] or 0)
551
+ orphan = int(orphan_row["orphan_memories"] or 0)
552
+ return {
553
+ "total_memories": total,
554
+ "stale_ratio": (stale / total) if total else 0.0,
555
+ "orphan_ratio": (orphan / total) if total else 0.0,
556
+ "unresolved_conflicts": int(conflict_row["unresolved_conflicts"] or 0),
557
+ "average_trust_score": float(row["average_trust_score"] or 0.0),
558
+ "audit_events": int(audit_row["audit_events"] or 0),
559
+ }
560
+
561
+ def _append_evolution(self, memory_id: str, event_type: str, payload: dict[str, object]) -> None:
562
+ self.connection.execute(
563
+ "INSERT INTO evolution_log (memory_id, event_type, payload_json, created_at) VALUES (?, ?, ?, ?)",
564
+ (memory_id, event_type, json.dumps(payload), _utcnow_iso()),
565
+ )
566
+
567
+ def _append_audit(
568
+ self,
569
+ actor: str,
570
+ operation: str,
571
+ target_type: str,
572
+ target_id: str,
573
+ payload: dict[str, object],
574
+ ) -> None:
575
+ self.connection.execute(
576
+ """
577
+ INSERT INTO audit_log (actor, operation, target_type, target_id, payload_json, created_at)
578
+ VALUES (?, ?, ?, ?, ?, ?)
579
+ """,
580
+ (actor, operation, target_type, target_id, json.dumps(payload), _utcnow_iso()),
581
+ )
582
+
583
+ def _memory_to_row(self, item: MemoryItem) -> dict[str, object]:
584
+ return {
585
+ "id": item.id,
586
+ "content": item.content,
587
+ "memory_type": item.memory_type.value,
588
+ "created_at": _serialize_datetime(item.created_at),
589
+ "last_accessed": _serialize_datetime(item.last_accessed),
590
+ "access_count": item.access_count,
591
+ "valid_from": _serialize_datetime(item.valid_from),
592
+ "valid_until": _serialize_datetime(item.valid_until),
593
+ "trust_score": item.trust_score,
594
+ "importance": item.importance,
595
+ "layer": item.layer.value,
596
+ "decay_rate": item.decay_rate,
597
+ "source_id": item.source_id,
598
+ "causal_parent_id": item.causal_parent_id,
599
+ "supersedes_id": item.supersedes_id,
600
+ "entity_refs_json": json.dumps(item.entity_refs),
601
+ "tags_json": json.dumps(item.tags),
602
+ "deleted_at": _serialize_datetime(item.deleted_at),
603
+ }
604
+
605
+ def _row_to_memory(self, row: sqlite3.Row) -> MemoryItem:
606
+ embedding_json = row["embedding_json"] if "embedding_json" in row.keys() else "[]"
607
+ return MemoryItem(
608
+ id=row["id"],
609
+ content=row["content"],
610
+ memory_type=MemoryType(row["memory_type"]),
611
+ embedding=json.loads(embedding_json or "[]"),
612
+ created_at=_deserialize_datetime(row["created_at"]) or datetime.now(timezone.utc),
613
+ last_accessed=_deserialize_datetime(row["last_accessed"]) or datetime.now(timezone.utc),
614
+ access_count=row["access_count"],
615
+ valid_from=_deserialize_datetime(row["valid_from"]),
616
+ valid_until=_deserialize_datetime(row["valid_until"]),
617
+ trust_score=row["trust_score"],
618
+ importance=row["importance"],
619
+ layer=MemoryLayer(row["layer"]),
620
+ decay_rate=row["decay_rate"],
621
+ source_id=row["source_id"],
622
+ causal_parent_id=row["causal_parent_id"],
623
+ supersedes_id=row["supersedes_id"],
624
+ entity_refs=json.loads(row["entity_refs_json"]),
625
+ tags=json.loads(row["tags_json"]),
626
+ deleted_at=_deserialize_datetime(row["deleted_at"]),
627
+ )
628
+
629
+ def _try_enable_sqlite_vec(self) -> None:
630
+ try:
631
+ sqlite_vec = importlib.import_module("sqlite_vec")
632
+ except ImportError:
633
+ return
634
+ load = getattr(sqlite_vec, "load", None)
635
+ if load is None:
636
+ return
637
+ try:
638
+ load(self.connection)
639
+ except Exception:
640
+ return
641
+ self._sqlite_vec = sqlite_vec
642
+ self._sqlite_vec_enabled = True
643
+ dimension = self._get_backend_meta("sqlite_vec_dimension")
644
+ if dimension:
645
+ self._ensure_vec_index_table(int(dimension))
646
+ self._rebuild_vec_index_if_needed()
647
+
648
+ def _serialize_embedding(self, embedding: list[float]) -> bytes | None:
649
+ if not self._sqlite_vec_enabled or self._sqlite_vec is None:
650
+ return None
651
+ serializer = getattr(self._sqlite_vec, "serialize_float32", None)
652
+ if serializer is None:
653
+ return None
654
+ return serializer(_normalize_embedding(embedding))
655
+
656
+ def _ensure_vec_index_table(self, dimension: int) -> None:
657
+ current_dimension = self._get_backend_meta("sqlite_vec_dimension")
658
+ if current_dimension and int(current_dimension) != dimension:
659
+ raise ValueError(
660
+ f"sqlite-vec index dimension mismatch: existing={current_dimension}, requested={dimension}"
661
+ )
662
+ self.connection.execute(
663
+ f"""
664
+ CREATE VIRTUAL TABLE IF NOT EXISTS memory_vec_index
665
+ USING vec0(
666
+ memory_rowid INTEGER PRIMARY KEY,
667
+ embedding FLOAT[{dimension}],
668
+ memory_type TEXT,
669
+ layer TEXT,
670
+ source_id TEXT,
671
+ trust_score FLOAT,
672
+ created_at TEXT,
673
+ last_accessed TEXT
674
+ )
675
+ """
676
+ )
677
+ self._set_backend_meta("sqlite_vec_dimension", str(dimension))
678
+
679
+ def _upsert_vec_index_row(self, item: MemoryItem, memory_rowid: int) -> None:
680
+ if not self._sqlite_vec_enabled:
681
+ return
682
+ serialized = self._serialize_embedding(item.embedding)
683
+ if serialized is None:
684
+ return
685
+ self._ensure_vec_index_table(len(item.embedding))
686
+ update_cursor = self.connection.execute(
687
+ """
688
+ UPDATE memory_vec_index
689
+ SET embedding = ?, memory_type = ?, layer = ?, source_id = ?, trust_score = ?, created_at = ?, last_accessed = ?
690
+ WHERE memory_rowid = ?
691
+ """,
692
+ (
693
+ serialized,
694
+ item.memory_type.value,
695
+ item.layer.value,
696
+ item.source_id,
697
+ item.trust_score,
698
+ _serialize_datetime(item.created_at),
699
+ _serialize_datetime(item.last_accessed),
700
+ memory_rowid,
701
+ ),
702
+ )
703
+ if update_cursor.rowcount > 0:
704
+ return
705
+ self.connection.execute(
706
+ """
707
+ INSERT INTO memory_vec_index (
708
+ memory_rowid, embedding, memory_type, layer, source_id, trust_score, created_at, last_accessed
709
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
710
+ """,
711
+ (
712
+ memory_rowid,
713
+ serialized,
714
+ item.memory_type.value,
715
+ item.layer.value,
716
+ item.source_id,
717
+ item.trust_score,
718
+ _serialize_datetime(item.created_at),
719
+ _serialize_datetime(item.last_accessed),
720
+ ),
721
+ )
722
+
723
+ def _delete_vec_index_row(self, memory_rowid: int) -> None:
724
+ if not self._sqlite_vec_enabled:
725
+ return
726
+ self.connection.execute("DELETE FROM memory_vec_index WHERE memory_rowid = ?", (memory_rowid,))
727
+
728
+ def _rebuild_vec_index_if_needed(self) -> None:
729
+ if not self._sqlite_vec_enabled:
730
+ return
731
+ count_row = self.connection.execute("SELECT COUNT(*) AS count FROM memory_vec_index").fetchone()
732
+ existing_count = int(count_row["count"] or 0)
733
+ memory_rows = self.connection.execute(
734
+ """
735
+ SELECT
736
+ m.rowid AS memory_rowid,
737
+ m.*,
738
+ mv.embedding_json
739
+ FROM memories m
740
+ JOIN memory_vectors mv ON mv.memory_id = m.id
741
+ WHERE m.deleted_at IS NULL
742
+ """
743
+ ).fetchall()
744
+ if existing_count >= len(memory_rows):
745
+ return
746
+ self.connection.execute("DELETE FROM memory_vec_index")
747
+ for row in memory_rows:
748
+ item = self._row_to_memory(row)
749
+ self._upsert_vec_index_row(item=item, memory_rowid=int(row["memory_rowid"]))
750
+ self.connection.commit()
751
+
752
+ def _get_backend_meta(self, key: str) -> str | None:
753
+ row = self.connection.execute("SELECT value FROM backend_meta WHERE key = ?", (key,)).fetchone()
754
+ if row is None:
755
+ return None
756
+ return str(row["value"])
757
+
758
+ def _set_backend_meta(self, key: str, value: str) -> None:
759
+ self.connection.execute(
760
+ "INSERT OR REPLACE INTO backend_meta (key, value) VALUES (?, ?)",
761
+ (key, value),
762
+ )