synapse-memory-engine 1.3.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.
@@ -0,0 +1,564 @@
1
+ import json
2
+ import sqlite3
3
+ import time
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ from synapse_memory.core.encryption import (
7
+ FernetEncryptionProvider,
8
+ KeyManagementProvider,
9
+ )
10
+
11
+
12
+ class _AutoClosingConnection(sqlite3.Connection):
13
+ """SQLite connection whose context manager COMMITS/ROLLS BACK and CLOSES.
14
+
15
+ Stock sqlite3 ``with conn:`` only commits or rolls back but leaves the file
16
+ handle open — on Windows that locks test .db files (PermissionError on
17
+ cleanup) and leaks WAL handles under FastAPI workers. This subclass closes
18
+ the handle on ``__exit__`` unless the instance is flagged
19
+ ``_sqlite_keep_open`` (used by SQLiteMemoryStore for its long-lived
20
+ ``:memory:`` connection).
21
+ """
22
+
23
+ def __exit__(self, exc_type, exc_val, exc_tb): # type: ignore
24
+ # Commit (or roll back) on context exit, then close the handle UNLESS
25
+ # this is the store's cached ':memory:' connection. SQLite ':memory:'
26
+ # databases are per-connection, so that one must stay open for the
27
+ # lifetime of the store (closing it would discard the schema and every
28
+ # migration). File-based connections always close here so Windows test
29
+ # teardown can delete the .db / -wal / -shm files without PermissionError.
30
+ try:
31
+ if exc_type is None:
32
+ try:
33
+ self.commit()
34
+ except Exception:
35
+ pass
36
+ else:
37
+ try:
38
+ self.rollback()
39
+ except Exception:
40
+ pass
41
+ finally:
42
+ # Skip closing the flagged long-lived ':memory:' connection so its
43
+ # schema and migrations survive this `with`; close everything else.
44
+ if not getattr(self, '_sqlite_keep_open', False):
45
+ try:
46
+ self.close()
47
+ except Exception:
48
+ pass
49
+ return False
50
+
51
+
52
+ class SQLiteMemoryStore:
53
+ """
54
+ Sovereign SQLite persistence backend with mandatory authenticated encryption at rest
55
+ and database-level multi-tenant isolation.
56
+ """
57
+
58
+ def __init__(
59
+ self,
60
+ db_path: str = "synapse_memory.db",
61
+ encryption_provider: Optional[Any] = None,
62
+ fail_closed: bool = False
63
+ ):
64
+ self.db_path = db_path
65
+ # Parity with PGVectorMemoryStore: strict callers (tests, provisioning)
66
+ # may demand that schema bootstrap errors raise instead of warn.
67
+ self.fail_closed = fail_closed
68
+ if encryption_provider is not None:
69
+ self.encryption_provider = encryption_provider
70
+ else:
71
+ key = KeyManagementProvider.get_encryption_key()
72
+ self.encryption_provider = FernetEncryptionProvider(key)
73
+ self._conn: Optional[sqlite3.Connection] = None # cached shared connection for ':memory:'
74
+ self._initialize_database()
75
+
76
+ def _get_connection(self) -> sqlite3.Connection:
77
+ # SQLite ':memory:' is per-connection, so the schema created during
78
+ # _initialize_database must live on ONE shared connection. Cache a
79
+ # single connection only for ':memory:' stores; file-based stores use a
80
+ # fresh connection per query so that test teardown can remove the DB
81
+ # file cleanly.
82
+ if self.db_path == ":memory:" and self._conn is not None:
83
+ return self._conn
84
+ conn = sqlite3.connect(
85
+ self.db_path,
86
+ timeout=10.0,
87
+ check_same_thread=False,
88
+ factory=_AutoClosingConnection,
89
+ )
90
+ conn.row_factory = sqlite3.Row
91
+ try:
92
+ conn.execute("PRAGMA journal_mode=WAL;")
93
+ conn.execute("PRAGMA busy_timeout=5000;")
94
+ conn.execute("PRAGMA synchronous=NORMAL;")
95
+ except Exception:
96
+ pass
97
+ if self.db_path == ":memory:":
98
+ self._conn = conn
99
+ setattr(conn, "_sqlite_keep_open", True)
100
+ return conn
101
+
102
+ def close(self) -> None:
103
+ """Release the cached connection and checkpoint WAL sidecars.
104
+
105
+ File-based stores open a short-lived connection to run a WAL
106
+ checkpoint so no ``-wal``/``-shm`` files linger after tests or
107
+ shutdown; ':memory:' stores skip that (there is nothing on disk).
108
+ Either way the cached ':memory:' connection is then torn down.
109
+ """
110
+ try:
111
+ if self.db_path != ":memory:":
112
+ conn = sqlite3.connect(self.db_path, timeout=5.0)
113
+ try:
114
+ conn.execute("PRAGMA wal_checkpoint(TRUNCATE);")
115
+ conn.commit()
116
+ finally:
117
+ conn.close()
118
+ except Exception:
119
+ pass
120
+ finally:
121
+ self._close_conn()
122
+
123
+ def _close_conn(self) -> None:
124
+ """Close the store's cached ':memory:' connection, if one exists."""
125
+ conn = self._conn
126
+ if conn is not None:
127
+ try:
128
+ conn.close()
129
+ except Exception:
130
+ pass
131
+ self._conn = None
132
+
133
+ def _return_connection(self, conn: Any) -> None:
134
+ """Pool-compatible no-op.
135
+
136
+ SQLiteMemoryStore owns a single cached ':memory:' connection instead of
137
+ an external pool, so there is nothing to return. Exposing this method
138
+ lets ``run_migrations(..., owns_connection=False)`` hand the connection
139
+ back without closing it, keeping the ':memory:' schema alive.
140
+ """
141
+ pass
142
+
143
+ def _initialize_database(self) -> None:
144
+ """Creates the memory table with tenant isolation and index constraints on creation."""
145
+ conn = self._get_connection()
146
+ cursor = conn.cursor()
147
+ cursor.execute("""
148
+ CREATE TABLE IF NOT EXISTS memories (
149
+ id TEXT PRIMARY KEY,
150
+ tenant_id TEXT NOT NULL DEFAULT 'default',
151
+ content TEXT NOT NULL,
152
+ category TEXT NOT NULL,
153
+ confidence REAL NOT NULL,
154
+ created_at REAL NOT NULL,
155
+ last_accessed_at REAL NOT NULL,
156
+ access_count INTEGER NOT NULL,
157
+ token_cost INTEGER NOT NULL,
158
+ embedding TEXT,
159
+ feedback_multiplier REAL NOT NULL,
160
+ supersedes VARCHAR(128),
161
+ superseded_by VARCHAR(128)
162
+ )
163
+ """)
164
+
165
+ # Migration check: Ensure tenant_id column exists if the table was
166
+ # created by an older schema version.
167
+ cursor.execute("PRAGMA table_info(memories)")
168
+ columns = [info[1] for info in cursor.fetchall()]
169
+ if "tenant_id" not in columns:
170
+ cursor.execute(
171
+ "ALTER TABLE memories ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'default'"
172
+ )
173
+
174
+ cursor.execute(
175
+ "CREATE INDEX IF NOT EXISTS idx_tenant ON memories(tenant_id)"
176
+ )
177
+ cursor.execute(
178
+ "CREATE INDEX IF NOT EXISTS idx_tenant_category ON memories(tenant_id, category)"
179
+ )
180
+ cursor.execute(
181
+ "CREATE INDEX IF NOT EXISTS idx_category ON memories(category)"
182
+ )
183
+ cursor.execute(
184
+ "CREATE INDEX IF NOT EXISTS idx_created_at ON memories(created_at)"
185
+ )
186
+ cursor.execute(
187
+ "CREATE INDEX IF NOT EXISTS idx_last_accessed ON memories(last_accessed_at)"
188
+ )
189
+ conn.commit()
190
+
191
+ # Apply forward-only schema migrations (adds embedding_dimension /
192
+ # model_version + migration ledger). Idempotent + versioned.
193
+ # ':memory:' is per-connection in SQLite, so the schema created during
194
+ # _initialize_database lives on our cached connection and must survive
195
+ # this run of migrations. Pass owns_connection=False so the cached
196
+ # connection is NOT closed here.
197
+ from synapse_memory.core.migrations import run_migrations
198
+ if self.db_path == ":memory:":
199
+ run_migrations(
200
+ self._get_connection,
201
+ is_postgres=False,
202
+ fail_closed=self.fail_closed,
203
+ owns_connection=False,
204
+ close_on_exit=False,
205
+ )
206
+ else:
207
+ # File-based stores hand run_migrations a callable that returns the
208
+ # schema-init connection and let it own+close it. This also lets
209
+ # test teardown remove the DB file cleanly.
210
+ run_migrations(
211
+ lambda: conn,
212
+ is_postgres=False,
213
+ fail_closed=self.fail_closed,
214
+ )
215
+
216
+ def insert_memory(self, memory: Dict[str, Any]) -> None:
217
+ """Inserts a structured memory node into the SQLite table with tenant isolation."""
218
+ content = self.encryption_provider.encrypt(memory["content"])
219
+ embedding = self.encryption_provider.encrypt(
220
+ json.dumps(memory.get("embedding", []))
221
+ )
222
+ tenant_id = memory.get("tenant_id") or memory.get("tenantId") or "default"
223
+
224
+ with self._get_connection() as conn:
225
+ cursor = conn.cursor()
226
+ cursor.execute("""
227
+ INSERT OR REPLACE INTO memories (
228
+ id, tenant_id, content, category, confidence, created_at,
229
+ last_accessed_at, access_count, token_cost,
230
+ embedding, feedback_multiplier, embedding_dimension, model_version,
231
+ supersedes, superseded_by
232
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
233
+ """, (
234
+ memory["id"],
235
+ tenant_id,
236
+ content,
237
+ memory["category"],
238
+ memory["confidence"],
239
+ memory["created_at"],
240
+ memory.get("last_accessed_at", memory["created_at"]),
241
+ memory.get("access_count", 0),
242
+ memory["token_cost"],
243
+ embedding,
244
+ memory.get("feedback_multiplier", 1.0),
245
+ memory.get("embedding_dimension", len(memory.get("embedding", []))),
246
+ memory.get("model_version", "unknown"),
247
+ memory.get("supersedes"),
248
+ memory.get("superseded_by"),
249
+ ))
250
+ conn.commit()
251
+
252
+ def mark_superseded(self, memory_id: str, superseded_by: str, tenant_id: Optional[str] = None) -> None:
253
+ """Stamp `superseded_by` on an old memory that a dedup MERGE replaced.
254
+
255
+ Scoped to ``tenant_id`` when supplied: unlike PostgreSQL (where RLS
256
+ enforces the boundary), SQLite has no row-level security, so the tenant
257
+ predicate must be part of the statement itself. Without it a supersede
258
+ issued in one tenant could rewrite another tenant's row.
259
+ """
260
+ with self._get_connection() as conn:
261
+ cursor = conn.cursor()
262
+ if tenant_id is not None:
263
+ cursor.execute(
264
+ "UPDATE memories SET superseded_by = ? WHERE id = ? AND tenant_id = ?",
265
+ (superseded_by, memory_id, tenant_id),
266
+ )
267
+ else:
268
+ cursor.execute(
269
+ "UPDATE memories SET superseded_by = ? WHERE id = ?",
270
+ (superseded_by, memory_id),
271
+ )
272
+ conn.commit()
273
+
274
+ def get_all_memories(
275
+ self, tenant_id: Optional[str] = None, include_superseded: bool = False
276
+ ) -> List[Dict[str, Any]]:
277
+ """Retrieves memories with deserialized embeddings, optionally scoped to tenant_id.
278
+
279
+ Superseded rows (dedup MERGE lineage) are hidden by default: they remain
280
+ for audit/lineage only and must never be served to retrieval. Capacity
281
+ pruning passes include_superseded=True so stale rows stay evictable.
282
+ """
283
+ query = "SELECT * FROM memories"
284
+ clauses: List[str] = []
285
+ params: List[Any] = []
286
+ if tenant_id is not None:
287
+ clauses.append("tenant_id = ?")
288
+ params.append(tenant_id)
289
+ if not include_superseded:
290
+ clauses.append("superseded_by IS NULL")
291
+ if clauses:
292
+ query += " WHERE " + " AND ".join(clauses)
293
+ with self._get_connection() as conn:
294
+ cursor = conn.cursor()
295
+ cursor.execute(query, tuple(params))
296
+ rows = cursor.fetchall()
297
+
298
+ memories = []
299
+ for row in rows:
300
+ mem = dict(row)
301
+ mem["content"] = self.encryption_provider.decrypt(mem["content"])
302
+ mem["embedding"] = json.loads(
303
+ self.encryption_provider.decrypt(mem["embedding"])
304
+ )
305
+ memories.append(mem)
306
+ return memories
307
+
308
+ def get_memory_by_id(self, memory_id: str, tenant_id: Optional[str] = None) -> Optional[Dict[str, Any]]:
309
+ """Retrieves a single memory node by its unique primary key ID and optional tenant scope."""
310
+ with self._get_connection() as conn:
311
+ cursor = conn.cursor()
312
+ if tenant_id is not None:
313
+ cursor.execute(
314
+ "SELECT * FROM memories WHERE id = ? AND tenant_id = ?",
315
+ (memory_id, tenant_id)
316
+ )
317
+ else:
318
+ cursor.execute("SELECT * FROM memories WHERE id = ?", (memory_id,))
319
+ row = cursor.fetchone()
320
+ if row:
321
+ mem = dict(row)
322
+ mem["content"] = self.encryption_provider.decrypt(mem["content"])
323
+ mem["embedding"] = json.loads(
324
+ self.encryption_provider.decrypt(mem["embedding"])
325
+ )
326
+ return mem
327
+ return None
328
+
329
+ def update_access_count(
330
+ self, memory_id: str, count_increment: int = 1, tenant_id: Optional[str] = None
331
+ ) -> None:
332
+ """Increases the usage metrics for Ebbinghaus stabilization weight tracking."""
333
+ with self._get_connection() as conn:
334
+ cursor = conn.cursor()
335
+ if tenant_id is not None:
336
+ cursor.execute("""
337
+ UPDATE memories
338
+ SET access_count = access_count + ?, last_accessed_at = ?
339
+ WHERE id = ? AND tenant_id = ?
340
+ """, (count_increment, time.time(), memory_id, tenant_id))
341
+ else:
342
+ cursor.execute("""
343
+ UPDATE memories
344
+ SET access_count = access_count + ?, last_accessed_at = ?
345
+ WHERE id = ?
346
+ """, (count_increment, time.time(), memory_id))
347
+ conn.commit()
348
+
349
+ def update_confidence(
350
+ self, memory_id: str, new_confidence: float, tenant_id: Optional[str] = None
351
+ ) -> None:
352
+ """Saves mutated confidence values resulting from active RLAIF feedback."""
353
+ with self._get_connection() as conn:
354
+ cursor = conn.cursor()
355
+ if tenant_id is not None:
356
+ cursor.execute("""
357
+ UPDATE memories
358
+ SET confidence = ?
359
+ WHERE id = ? AND tenant_id = ?
360
+ """, (new_confidence, memory_id, tenant_id))
361
+ else:
362
+ cursor.execute("""
363
+ UPDATE memories
364
+ SET confidence = ?
365
+ WHERE id = ?
366
+ """, (new_confidence, memory_id))
367
+ conn.commit()
368
+
369
+ def delete_memory(self, memory_id: str, tenant_id: Optional[str] = None) -> None:
370
+ with self._get_connection() as conn:
371
+ cursor = conn.cursor()
372
+ if tenant_id is not None:
373
+ cursor.execute(
374
+ "DELETE FROM memories WHERE id = ? AND tenant_id = ?",
375
+ (memory_id, tenant_id)
376
+ )
377
+ else:
378
+ cursor.execute("DELETE FROM memories WHERE id = ?", (memory_id,))
379
+ conn.commit()
380
+
381
+ def clear_memories(self, tenant_id: Optional[str] = None) -> int:
382
+ """Clears memories for a given tenant or the entire store and returns count of deleted rows."""
383
+ with self._get_connection() as conn:
384
+ cursor = conn.cursor()
385
+ if tenant_id is not None:
386
+ cursor.execute("DELETE FROM memories WHERE tenant_id = ?", (tenant_id,))
387
+ else:
388
+ cursor.execute("DELETE FROM memories")
389
+ deleted_count = cursor.rowcount
390
+ conn.commit()
391
+ return deleted_count
392
+
393
+ def get_memories_by_category(
394
+ self, category: str, tenant_id: Optional[str] = None, include_superseded: bool = False
395
+ ) -> List[Dict[str, Any]]:
396
+ """Retrieves memories filtered by category, utilizing the category index.
397
+
398
+ Superseded rows are hidden by default (see get_all_memories).
399
+ """
400
+ query = "SELECT * FROM memories WHERE category = ?"
401
+ params: List[Any] = [category]
402
+ if tenant_id is not None:
403
+ query += " AND tenant_id = ?"
404
+ params.append(tenant_id)
405
+ if not include_superseded:
406
+ query += " AND superseded_by IS NULL"
407
+ with self._get_connection() as conn:
408
+ cursor = conn.cursor()
409
+ cursor.execute(query, tuple(params))
410
+ rows = cursor.fetchall()
411
+
412
+ memories = []
413
+ for row in rows:
414
+ mem = dict(row)
415
+ mem["content"] = self.encryption_provider.decrypt(mem["content"])
416
+ mem["embedding"] = json.loads(
417
+ self.encryption_provider.decrypt(mem["embedding"])
418
+ )
419
+ memories.append(mem)
420
+ return memories
421
+
422
+ def vector_search(
423
+ self,
424
+ query_vec: List[float],
425
+ top_k: int = 10,
426
+ category: Optional[str] = None,
427
+ tenant_id: Optional[str] = None
428
+ ) -> List[Dict[str, Any]]:
429
+ """
430
+ Dense retrieval over the SQLite corpus using cosine similarity.
431
+
432
+ NOTE: SQLite is a single-node dev/local backend. This is an exact O(N)
433
+ in-memory scan, not an approximate HNSW index. For team/production scale
434
+ use the pgvector backend which pushes `embedding <=> query` down to SQL.
435
+ Results match the PGVectorMemoryStore.vector_search contract:
436
+ each node carries `cosine_similarity` and `relevance_score`.
437
+ """
438
+ memories = (
439
+ self.get_memories_by_category(category, tenant_id=tenant_id)
440
+ if category
441
+ else self.get_all_memories(tenant_id=tenant_id)
442
+ )
443
+
444
+ mag_q = sum(a * a for a in query_vec) ** 0.5
445
+ results: List[Dict[str, Any]] = []
446
+ for mem in memories:
447
+ vec = mem.get("embedding", [])
448
+ if vec and len(vec) == len(query_vec):
449
+ dot = sum(a * b for a, b in zip(query_vec, vec))
450
+ mag_v = sum(b * b for b in vec) ** 0.5
451
+ sim = dot / (mag_q * mag_v) if mag_q * mag_v > 0 else 0.0
452
+ else:
453
+ sim = 0.0
454
+ mem_copy = mem.copy()
455
+ mem_copy["cosine_similarity"] = sim
456
+ mem_copy["relevance_score"] = sim
457
+ results.append(mem_copy)
458
+
459
+ results.sort(key=lambda x: x["cosine_similarity"], reverse=True)
460
+ return results[:top_k]
461
+
462
+ def lexical_search(
463
+ self,
464
+ query: str,
465
+ top_k: int = 100,
466
+ category: Optional[str] = None,
467
+ tenant_id: Optional[str] = None
468
+ ) -> List[Dict[str, Any]]:
469
+ """
470
+ Sparse (lexical) retrieval over the SQLite corpus using token-overlap
471
+ ranking — the exact-scan analogue of PostgreSQL tsvector GIN ranking.
472
+ Matches the PGVectorMemoryStore.lexical_search contract: each node
473
+ carries `lexical_rank` and `relevance_score`.
474
+ """
475
+ memories = (
476
+ self.get_memories_by_category(category, tenant_id=tenant_id)
477
+ if category
478
+ else self.get_all_memories(tenant_id=tenant_id)
479
+ )
480
+
481
+ query_terms = set(query.lower().split())
482
+ if not query_terms:
483
+ return []
484
+
485
+ results: List[Dict[str, Any]] = []
486
+ for mem in memories:
487
+ words = set(str(mem.get("content", "")).lower().split())
488
+ overlap = len(query_terms.intersection(words))
489
+ if overlap > 0:
490
+ mem_copy = mem.copy()
491
+ mem_copy["lexical_rank"] = float(overlap) / float(len(query_terms))
492
+ mem_copy["relevance_score"] = mem_copy["lexical_rank"]
493
+ results.append(mem_copy)
494
+
495
+ results.sort(key=lambda x: x.get("lexical_rank", 0.0), reverse=True)
496
+ return results[:top_k]
497
+
498
+ def count_memories(self, tenant_id: Optional[str] = None) -> int:
499
+ with self._get_connection() as conn:
500
+ cursor = conn.cursor()
501
+ if tenant_id is not None:
502
+ cursor.execute(
503
+ "SELECT count(*) FROM memories WHERE tenant_id = ?", (tenant_id,)
504
+ )
505
+ else:
506
+ cursor.execute("SELECT count(*) FROM memories")
507
+ return int(cursor.fetchone()[0])
508
+
509
+ def count_active_memories(self, tenant_id: Optional[str] = None) -> int:
510
+ """Count of retrieval-visible (non-superseded) memories."""
511
+ query = "SELECT count(*) FROM memories WHERE superseded_by IS NULL"
512
+ params: List[Any] = []
513
+ if tenant_id is not None:
514
+ query += " AND tenant_id = ?"
515
+ params.append(tenant_id)
516
+ with self._get_connection() as conn:
517
+ cursor = conn.cursor()
518
+ cursor.execute(query, tuple(params))
519
+ return int(cursor.fetchone()[0])
520
+
521
+ def query_candidates(
522
+ self,
523
+ category: Optional[str] = None,
524
+ tenant_id: Optional[str] = None,
525
+ min_confidence: float = 0.0,
526
+ limit: int = 100,
527
+ order_by: str = "last_accessed_at DESC"
528
+ ) -> List[Dict[str, Any]]:
529
+ """
530
+ Retrieves a bounded candidate set directly using SQL indexes
531
+ instead of loading the entire corpus into application memory.
532
+ """
533
+ query = "SELECT * FROM memories WHERE confidence >= ? AND superseded_by IS NULL"
534
+ params: List[Any] = [min_confidence]
535
+ if tenant_id is not None:
536
+ query += " AND tenant_id = ?"
537
+ params.append(tenant_id)
538
+ if category:
539
+ query += " AND category = ?"
540
+ params.append(category)
541
+
542
+ safe_orders = {
543
+ "last_accessed_at DESC": "last_accessed_at DESC",
544
+ "created_at DESC": "created_at DESC",
545
+ "access_count DESC": "access_count DESC",
546
+ "confidence DESC": "confidence DESC"
547
+ }
548
+ order_clause = safe_orders.get(order_by, "last_accessed_at DESC")
549
+ query += f" ORDER BY {order_clause} LIMIT ?"
550
+ params.append(limit)
551
+
552
+ with self._get_connection() as conn:
553
+ cursor = conn.cursor()
554
+ cursor.execute(query, tuple(params))
555
+ rows = cursor.fetchall()
556
+ memories = []
557
+ for row in rows:
558
+ mem = dict(row)
559
+ mem["content"] = self.encryption_provider.decrypt(mem["content"])
560
+ mem["embedding"] = json.loads(
561
+ self.encryption_provider.decrypt(mem["embedding"])
562
+ )
563
+ memories.append(mem)
564
+ return memories
@@ -0,0 +1,12 @@
1
+ """
2
+ LangChain Integration Example and backward-compatible adapter.
3
+ Uses the production SynapseLangChainMemory from synapse_memory.integrations.langchain.
4
+ """
5
+
6
+ from synapse_memory.integrations.langchain import (
7
+ SynapseLangChainMemory,
8
+ HAS_LANGCHAIN,
9
+ BaseMemory,
10
+ )
11
+
12
+ __all__ = ["SynapseLangChainMemory", "HAS_LANGCHAIN", "BaseMemory"]
@@ -0,0 +1,12 @@
1
+ """
2
+ LlamaIndex Integration Example and backward-compatible adapter.
3
+ Uses the production SynapseLlamaIndexMemory from synapse_memory.integrations.llamaindex.
4
+ """
5
+
6
+ from synapse_memory.integrations.llamaindex import (
7
+ SynapseLlamaIndexMemory,
8
+ HAS_LLAMAINDEX,
9
+ BaseMemory,
10
+ )
11
+
12
+ __all__ = ["SynapseLlamaIndexMemory", "HAS_LLAMAINDEX", "BaseMemory"]
@@ -0,0 +1,9 @@
1
+ """
2
+ Production framework integrations for Synapse Memory.
3
+ Provides drop-in memory modules for LangChain and LlamaIndex.
4
+ """
5
+
6
+ from synapse_memory.integrations.langchain import SynapseLangChainMemory
7
+ from synapse_memory.integrations.llamaindex import SynapseLlamaIndexMemory
8
+
9
+ __all__ = ["SynapseLangChainMemory", "SynapseLlamaIndexMemory"]