superlocalmemory 3.4.49 → 3.4.52
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.
- package/CHANGELOG.md +59 -0
- package/README.md +43 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +7 -2
- package/src/superlocalmemory/cli/main.py +10 -0
- package/src/superlocalmemory/core/backend_orchestrator.py +365 -0
- package/src/superlocalmemory/core/ollama_embedder.py +14 -4
- package/src/superlocalmemory/core/pruning_engine.py +216 -0
- package/src/superlocalmemory/core/recall_pipeline.py +26 -2
- package/src/superlocalmemory/core/store_pipeline.py +21 -0
- package/src/superlocalmemory/core/tier_manager.py +124 -0
- package/src/superlocalmemory/graph/__init__.py +9 -0
- package/src/superlocalmemory/graph/cozo_backend.py +527 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +1 -1
- package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
- package/src/superlocalmemory/mcp/tools_active.py +141 -1
- package/src/superlocalmemory/retrieval/engine.py +15 -3
- package/src/superlocalmemory/retrieval/entity_channel.py +50 -1
- package/src/superlocalmemory/retrieval/reranker.py +15 -0
- package/src/superlocalmemory/retrieval/spreading_activation.py +5 -2
- package/src/superlocalmemory/server/unified_daemon.py +73 -5
- package/src/superlocalmemory/storage/migration_runner.py +3 -0
- package/src/superlocalmemory/storage/migrations/M014_v345_scale_ready.py +45 -0
- package/src/superlocalmemory/storage/schema_v345.py +109 -0
- package/src/superlocalmemory/vector/__init__.py +9 -0
- package/src/superlocalmemory/vector/lancedb_backend.py +299 -0
- package/src/superlocalmemory.egg-info/PKG-INFO +44 -2
- package/src/superlocalmemory.egg-info/SOURCES.txt +8 -0
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
|
|
5
|
+
"""SuperLocalMemory v3.4.5 — CozoDB Graph Backend.
|
|
6
|
+
|
|
7
|
+
Embedded graph database backend powered by CozoDB (MPL-2.0).
|
|
8
|
+
Replaces NetworkX for entity graph storage and traversal.
|
|
9
|
+
|
|
10
|
+
All Datalog queries are private to this module.
|
|
11
|
+
External code calls Python methods only — never raw Datalog strings.
|
|
12
|
+
|
|
13
|
+
Verified API: pycozo v0.7.6, Client('rocksdb', path), db.run(datalog), db.put(name, dicts)
|
|
14
|
+
|
|
15
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import logging
|
|
22
|
+
import sqlite3
|
|
23
|
+
from datetime import datetime
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
# Optional import — CozoDB is an optional dependency
|
|
30
|
+
try:
|
|
31
|
+
from pycozo.client import Client as _CozoClient
|
|
32
|
+
_COZO_AVAILABLE = True
|
|
33
|
+
except ImportError:
|
|
34
|
+
_CozoClient = None # type: ignore[assignment]
|
|
35
|
+
_COZO_AVAILABLE = False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class CozoDBError(Exception):
|
|
39
|
+
"""Base exception for CozoDB backend failures."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CozoDBNotAvailable(CozoDBError):
|
|
43
|
+
"""CozoDB not installed. Install with: pip install superlocalmemory[cozo]"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class CozoDBConnectionError(CozoDBError):
|
|
47
|
+
"""CozoDB file not found or corrupted."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class CozoDBQueryError(CozoDBError):
|
|
51
|
+
"""Datalog query execution failed."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
# CozoDBGraphBackend
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
class CozoDBGraphBackend:
|
|
59
|
+
"""Embedded graph backend powered by CozoDB.
|
|
60
|
+
|
|
61
|
+
Wraps pycozo for graph storage, traversal, and algorithms.
|
|
62
|
+
All Datalog queries are private. External code calls Python methods.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, db_path: str) -> None:
|
|
66
|
+
if not _COZO_AVAILABLE:
|
|
67
|
+
raise CozoDBNotAvailable(
|
|
68
|
+
"CozoDB not installed. Run: pip install superlocalmemory[cozo]"
|
|
69
|
+
)
|
|
70
|
+
path = Path(db_path)
|
|
71
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
self._db_path = str(path)
|
|
73
|
+
self._db = _CozoClient("rocksdb", self._db_path) # type: ignore[misc]
|
|
74
|
+
self._ensure_schema()
|
|
75
|
+
|
|
76
|
+
def close(self) -> None:
|
|
77
|
+
"""Close the CozoDB connection."""
|
|
78
|
+
if hasattr(self, "_db") and self._db is not None:
|
|
79
|
+
self._db.close()
|
|
80
|
+
|
|
81
|
+
# ------------------------------------------------------------------
|
|
82
|
+
# Schema
|
|
83
|
+
# ------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
def _ensure_schema(self) -> None:
|
|
86
|
+
"""Create relations if they don't exist. Idempotent."""
|
|
87
|
+
try:
|
|
88
|
+
self._db.run("""
|
|
89
|
+
:create entity {
|
|
90
|
+
id: String => name: String, entity_type: String,
|
|
91
|
+
tier: String default 'hot',
|
|
92
|
+
properties: String default '{}',
|
|
93
|
+
profile_id: String default 'default',
|
|
94
|
+
created_at: String, updated_at: String
|
|
95
|
+
}
|
|
96
|
+
""")
|
|
97
|
+
except Exception:
|
|
98
|
+
pass # Already exists
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
self._db.run("""
|
|
102
|
+
:create edge {
|
|
103
|
+
from_id: String, to_id: String =>
|
|
104
|
+
edge_type: String, weight: Float default 1.0,
|
|
105
|
+
metadata: String default '{}',
|
|
106
|
+
profile_id: String default 'default',
|
|
107
|
+
created_at: String
|
|
108
|
+
}
|
|
109
|
+
""")
|
|
110
|
+
except Exception:
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
# ------------------------------------------------------------------
|
|
114
|
+
# Write Path
|
|
115
|
+
# ------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
def add_entity(
|
|
118
|
+
self,
|
|
119
|
+
entity_id: str,
|
|
120
|
+
name: str,
|
|
121
|
+
entity_type: str,
|
|
122
|
+
properties: dict | None = None,
|
|
123
|
+
profile_id: str = "default",
|
|
124
|
+
) -> None:
|
|
125
|
+
"""Insert or update a canonical entity node."""
|
|
126
|
+
now = datetime.now().isoformat()
|
|
127
|
+
props = json.dumps(properties or {})
|
|
128
|
+
self._db.put("entity", [{
|
|
129
|
+
"id": entity_id,
|
|
130
|
+
"name": name,
|
|
131
|
+
"entity_type": entity_type,
|
|
132
|
+
"properties": props,
|
|
133
|
+
"profile_id": profile_id,
|
|
134
|
+
"tier": "hot",
|
|
135
|
+
"created_at": now,
|
|
136
|
+
"updated_at": now,
|
|
137
|
+
}])
|
|
138
|
+
|
|
139
|
+
def add_edge(
|
|
140
|
+
self,
|
|
141
|
+
from_id: str,
|
|
142
|
+
to_id: str,
|
|
143
|
+
edge_type: str,
|
|
144
|
+
weight: float = 1.0,
|
|
145
|
+
metadata: dict | None = None,
|
|
146
|
+
profile_id: str = "default",
|
|
147
|
+
) -> None:
|
|
148
|
+
"""Insert a relationship edge between two entities."""
|
|
149
|
+
now = datetime.now().isoformat()
|
|
150
|
+
meta = json.dumps(metadata or {})
|
|
151
|
+
self._db.put("edge", [{
|
|
152
|
+
"from_id": from_id,
|
|
153
|
+
"to_id": to_id,
|
|
154
|
+
"edge_type": edge_type,
|
|
155
|
+
"weight": weight,
|
|
156
|
+
"metadata": meta,
|
|
157
|
+
"profile_id": profile_id,
|
|
158
|
+
"created_at": now,
|
|
159
|
+
}])
|
|
160
|
+
|
|
161
|
+
# ------------------------------------------------------------------
|
|
162
|
+
# Bulk Import (SQLite → CozoDB)
|
|
163
|
+
# ------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
def bulk_import_from_sqlite(
|
|
166
|
+
self,
|
|
167
|
+
conn: sqlite3.Connection,
|
|
168
|
+
profile_id: str = "default",
|
|
169
|
+
tier_filter: list[str] | None = None,
|
|
170
|
+
) -> int:
|
|
171
|
+
"""Export entities + edges from SQLite to CozoDB.
|
|
172
|
+
|
|
173
|
+
Only imports facts + edges in tier_filter (default: hot+warm).
|
|
174
|
+
Uses parameterized Datalog — no string injection.
|
|
175
|
+
|
|
176
|
+
Returns number of edges imported.
|
|
177
|
+
"""
|
|
178
|
+
if tier_filter is None:
|
|
179
|
+
tier_filter = ["active", "warm"]
|
|
180
|
+
|
|
181
|
+
# Step 1: Export all unique node IDs from graph_edges as entities.
|
|
182
|
+
# graph_edges uses fact IDs as nodes. canonical_entities uses separate entity IDs.
|
|
183
|
+
# CozoDB graph mirrors the graph_edges adjacency — node = fact ID.
|
|
184
|
+
entities_sql = """
|
|
185
|
+
SELECT DISTINCT node_id FROM (
|
|
186
|
+
SELECT source_id as node_id FROM graph_edges WHERE profile_id = ?
|
|
187
|
+
UNION
|
|
188
|
+
SELECT target_id as node_id FROM graph_edges WHERE profile_id = ?
|
|
189
|
+
)
|
|
190
|
+
"""
|
|
191
|
+
rows = conn.execute(entities_sql, (profile_id, profile_id)).fetchall()
|
|
192
|
+
|
|
193
|
+
entity_dicts = []
|
|
194
|
+
now = datetime.now().isoformat()
|
|
195
|
+
for (nid,) in rows:
|
|
196
|
+
entity_dicts.append({
|
|
197
|
+
"id": nid,
|
|
198
|
+
"name": nid[:12],
|
|
199
|
+
"entity_type": "fact_node",
|
|
200
|
+
"tier": "active",
|
|
201
|
+
"properties": "{}",
|
|
202
|
+
"profile_id": profile_id,
|
|
203
|
+
"created_at": now,
|
|
204
|
+
"updated_at": now,
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
if entity_dicts:
|
|
208
|
+
self._db.put("entity", entity_dicts)
|
|
209
|
+
logger.info("CozoDB: imported %d entities", len(entity_dicts))
|
|
210
|
+
|
|
211
|
+
# Step 2: Export edges directly (source_id/target_id are fact IDs)
|
|
212
|
+
edges_sql = """
|
|
213
|
+
SELECT source_id, target_id, edge_type, weight
|
|
214
|
+
FROM graph_edges WHERE profile_id = ?
|
|
215
|
+
"""
|
|
216
|
+
edge_rows = conn.execute(edges_sql, (profile_id,)).fetchall()
|
|
217
|
+
|
|
218
|
+
edge_dicts = []
|
|
219
|
+
for row in edge_rows:
|
|
220
|
+
ea, eb, etype, weight = row
|
|
221
|
+
edge_dicts.append({
|
|
222
|
+
"from_id": ea,
|
|
223
|
+
"to_id": eb,
|
|
224
|
+
"edge_type": etype or "related",
|
|
225
|
+
"weight": float(weight or 1.0),
|
|
226
|
+
"metadata": "{}",
|
|
227
|
+
"profile_id": profile_id,
|
|
228
|
+
"created_at": now,
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
if edge_dicts:
|
|
232
|
+
self._db.put("edge", edge_dicts)
|
|
233
|
+
logger.info("CozoDB: imported %d edges", len(edge_dicts))
|
|
234
|
+
|
|
235
|
+
return len(edge_dicts)
|
|
236
|
+
|
|
237
|
+
# ------------------------------------------------------------------
|
|
238
|
+
# Spreading Activation (Python BFS over CozoDB edges)
|
|
239
|
+
# ------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
def spreading_activation(
|
|
242
|
+
self,
|
|
243
|
+
seed_entities: list[str],
|
|
244
|
+
depth: int = 3,
|
|
245
|
+
decay: float = 0.5,
|
|
246
|
+
top_k: int = 50,
|
|
247
|
+
) -> list[tuple[str, float]]:
|
|
248
|
+
"""BFS from seed nodes with weight decay per hop.
|
|
249
|
+
|
|
250
|
+
Uses CozoDB as fast edge store, Python for BFS logic.
|
|
251
|
+
Returns [(entity_id, activation_score), ...] sorted by score desc.
|
|
252
|
+
"""
|
|
253
|
+
if not seed_entities:
|
|
254
|
+
return []
|
|
255
|
+
|
|
256
|
+
scores: dict[str, float] = {}
|
|
257
|
+
current_frontier: set[str] = set(seed_entities)
|
|
258
|
+
for s in seed_entities:
|
|
259
|
+
scores[s] = 1.0
|
|
260
|
+
|
|
261
|
+
for d in range(depth):
|
|
262
|
+
if not current_frontier:
|
|
263
|
+
break
|
|
264
|
+
next_frontier: set[str] = set()
|
|
265
|
+
hop_multiplier = decay ** (d + 1)
|
|
266
|
+
|
|
267
|
+
for entity_id in current_frontier:
|
|
268
|
+
# Query all outgoing edges from this entity
|
|
269
|
+
try:
|
|
270
|
+
result = self._db.run(f"""
|
|
271
|
+
?[to_id, weight] :=
|
|
272
|
+
*edge{{from_id: '{entity_id}', to_id, weight}}
|
|
273
|
+
""")
|
|
274
|
+
df = result if hasattr(result, "values") else result
|
|
275
|
+
if df is None or len(df) == 0:
|
|
276
|
+
continue
|
|
277
|
+
rows = df.values.tolist() if hasattr(df, "values") else []
|
|
278
|
+
for to_id, weight in rows:
|
|
279
|
+
to_id_str = str(to_id)
|
|
280
|
+
score = hop_multiplier * float(weight)
|
|
281
|
+
if to_id_str not in scores or score > scores[to_id_str]:
|
|
282
|
+
scores[to_id_str] = score
|
|
283
|
+
next_frontier.add(to_id_str)
|
|
284
|
+
except Exception:
|
|
285
|
+
continue
|
|
286
|
+
|
|
287
|
+
current_frontier = next_frontier
|
|
288
|
+
|
|
289
|
+
# Sort by score desc, return top_k
|
|
290
|
+
ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True)
|
|
291
|
+
return ranked[:top_k]
|
|
292
|
+
|
|
293
|
+
# ------------------------------------------------------------------
|
|
294
|
+
# PageRank (Python iterative over CozoDB edges)
|
|
295
|
+
# ------------------------------------------------------------------
|
|
296
|
+
|
|
297
|
+
def pagerank(
|
|
298
|
+
self, damping: float = 0.85, max_iter: int = 100
|
|
299
|
+
) -> dict[str, float]:
|
|
300
|
+
"""Iterative PageRank on the current graph.
|
|
301
|
+
|
|
302
|
+
Uses CozoDB for edge queries, Python for iteration.
|
|
303
|
+
"""
|
|
304
|
+
try:
|
|
305
|
+
# Get all entities
|
|
306
|
+
entities_df = self._db.run("?[id] := *entity{id}")
|
|
307
|
+
if entities_df is None or len(entities_df) == 0:
|
|
308
|
+
return {}
|
|
309
|
+
entities = [str(r[0]) for r in entities_df.values.tolist()]
|
|
310
|
+
n = len(entities)
|
|
311
|
+
if n == 0:
|
|
312
|
+
return {}
|
|
313
|
+
|
|
314
|
+
entity_index = {eid: i for i, eid in enumerate(entities)}
|
|
315
|
+
scores = [1.0 / n] * n
|
|
316
|
+
|
|
317
|
+
# Get all edges as adjacency list
|
|
318
|
+
try:
|
|
319
|
+
edges_df = self._db.run("?[from_id, to_id, weight] := *edge{from_id, to_id, weight}")
|
|
320
|
+
if edges_df is not None and len(edges_df) > 0:
|
|
321
|
+
edges = edges_df.values.tolist()
|
|
322
|
+
else:
|
|
323
|
+
edges = []
|
|
324
|
+
except Exception:
|
|
325
|
+
edges = []
|
|
326
|
+
|
|
327
|
+
# Build outgoing edge map
|
|
328
|
+
outgoing: dict[str, list[tuple[str, float]]] = {e: [] for e in entities}
|
|
329
|
+
for from_id, to_id, weight in edges:
|
|
330
|
+
outgoing[str(from_id)].append((str(to_id), float(weight)))
|
|
331
|
+
|
|
332
|
+
# Iterative PageRank
|
|
333
|
+
for _ in range(max_iter):
|
|
334
|
+
new_scores = [(1.0 - damping) / n] * n
|
|
335
|
+
for i, eid in enumerate(entities):
|
|
336
|
+
neighbors = outgoing.get(eid, [])
|
|
337
|
+
if neighbors:
|
|
338
|
+
total_weight = sum(w for _, w in neighbors)
|
|
339
|
+
if total_weight > 0:
|
|
340
|
+
for to_id, weight in neighbors:
|
|
341
|
+
j = entity_index.get(to_id)
|
|
342
|
+
if j is not None:
|
|
343
|
+
new_scores[j] += damping * scores[i] * weight / total_weight
|
|
344
|
+
scores = new_scores
|
|
345
|
+
|
|
346
|
+
return {entities[i]: scores[i] for i in range(n)}
|
|
347
|
+
|
|
348
|
+
except Exception as exc:
|
|
349
|
+
logger.warning("CozoDB PageRank failed: %s", exc)
|
|
350
|
+
return {}
|
|
351
|
+
|
|
352
|
+
# ------------------------------------------------------------------
|
|
353
|
+
# Community Detection (simplified label propagation)
|
|
354
|
+
# ------------------------------------------------------------------
|
|
355
|
+
|
|
356
|
+
def community_detect(self, method: str = "louvain") -> dict[str, int]:
|
|
357
|
+
"""Simplified community detection via label propagation.
|
|
358
|
+
|
|
359
|
+
Uses CozoDB for edge queries, Python for iteration.
|
|
360
|
+
Falls back to connected components if Louvain fails.
|
|
361
|
+
"""
|
|
362
|
+
try:
|
|
363
|
+
entities_df = self._db.run("?[id] := *entity{id}")
|
|
364
|
+
if entities_df is None or len(entities_df) == 0:
|
|
365
|
+
return {}
|
|
366
|
+
entities = [str(r[0]) for r in entities_df.values.tolist()]
|
|
367
|
+
|
|
368
|
+
# Get edges
|
|
369
|
+
try:
|
|
370
|
+
edges_df = self._db.run("?[from_id, to_id] := *edge{from_id, to_id}")
|
|
371
|
+
if edges_df is not None and len(edges_df) > 0:
|
|
372
|
+
edges = edges_df.values.tolist()
|
|
373
|
+
else:
|
|
374
|
+
edges = []
|
|
375
|
+
except Exception:
|
|
376
|
+
edges = []
|
|
377
|
+
|
|
378
|
+
# Build adjacency for connected components
|
|
379
|
+
adj: dict[str, set[str]] = {e: set() for e in entities}
|
|
380
|
+
for from_id, to_id in edges:
|
|
381
|
+
f, t = str(from_id), str(to_id)
|
|
382
|
+
adj.setdefault(f, set()).add(t)
|
|
383
|
+
adj.setdefault(t, set()).add(f)
|
|
384
|
+
|
|
385
|
+
# Connected components via BFS
|
|
386
|
+
community: dict[str, int] = {}
|
|
387
|
+
visited: set[str] = set()
|
|
388
|
+
comm_id = 0
|
|
389
|
+
|
|
390
|
+
for entity in entities:
|
|
391
|
+
if entity in visited:
|
|
392
|
+
continue
|
|
393
|
+
# BFS from this entity
|
|
394
|
+
queue = [entity]
|
|
395
|
+
visited.add(entity)
|
|
396
|
+
while queue:
|
|
397
|
+
current = queue.pop(0)
|
|
398
|
+
community[current] = comm_id
|
|
399
|
+
for neighbor in adj.get(current, set()):
|
|
400
|
+
if neighbor not in visited:
|
|
401
|
+
visited.add(neighbor)
|
|
402
|
+
queue.append(neighbor)
|
|
403
|
+
comm_id += 1
|
|
404
|
+
|
|
405
|
+
return community
|
|
406
|
+
|
|
407
|
+
except Exception as exc:
|
|
408
|
+
logger.warning("CozoDB community detection failed: %s", exc)
|
|
409
|
+
return {}
|
|
410
|
+
|
|
411
|
+
# ------------------------------------------------------------------
|
|
412
|
+
# Shortest Path (BFS)
|
|
413
|
+
# ------------------------------------------------------------------
|
|
414
|
+
|
|
415
|
+
def shortest_path(self, from_id: str, to_id: str) -> list[str]:
|
|
416
|
+
"""BFS shortest path between two entities."""
|
|
417
|
+
try:
|
|
418
|
+
if from_id == to_id:
|
|
419
|
+
return [from_id]
|
|
420
|
+
|
|
421
|
+
edges_df = self._db.run("?[from_id, to_id] := *edge{from_id, to_id}")
|
|
422
|
+
if edges_df is None or len(edges_df) == 0:
|
|
423
|
+
return []
|
|
424
|
+
|
|
425
|
+
# Build adjacency
|
|
426
|
+
adj: dict[str, list[str]] = {}
|
|
427
|
+
for f, t in edges_df.values.tolist():
|
|
428
|
+
adj.setdefault(str(f), []).append(str(t))
|
|
429
|
+
adj.setdefault(str(t), []).append(str(f))
|
|
430
|
+
|
|
431
|
+
# BFS
|
|
432
|
+
from collections import deque
|
|
433
|
+
queue = deque([(from_id, [from_id])])
|
|
434
|
+
visited = {from_id}
|
|
435
|
+
|
|
436
|
+
while queue:
|
|
437
|
+
current, path = queue.popleft()
|
|
438
|
+
for neighbor in adj.get(current, []):
|
|
439
|
+
if neighbor == to_id:
|
|
440
|
+
return path + [neighbor]
|
|
441
|
+
if neighbor not in visited:
|
|
442
|
+
visited.add(neighbor)
|
|
443
|
+
queue.append((neighbor, path + [neighbor]))
|
|
444
|
+
|
|
445
|
+
return []
|
|
446
|
+
except Exception as exc:
|
|
447
|
+
logger.warning("CozoDB shortest path failed: %s", exc)
|
|
448
|
+
return []
|
|
449
|
+
|
|
450
|
+
# ------------------------------------------------------------------
|
|
451
|
+
# Tier Sync
|
|
452
|
+
# ------------------------------------------------------------------
|
|
453
|
+
|
|
454
|
+
def sync_tier_changes(
|
|
455
|
+
self, added: list[str], removed: list[str]
|
|
456
|
+
) -> None:
|
|
457
|
+
"""Sync tier changes: add promoted entities, mark demoted."""
|
|
458
|
+
now = datetime.now().isoformat()
|
|
459
|
+
|
|
460
|
+
if added:
|
|
461
|
+
# Fetch entity data from existing CozoDB entities or set defaults
|
|
462
|
+
for entity_id in added:
|
|
463
|
+
try:
|
|
464
|
+
self._db.run(f"""
|
|
465
|
+
?[id, tier] <- [['{entity_id}', 'active']]
|
|
466
|
+
:update entity {{id => tier, updated_at: '{now}'}}
|
|
467
|
+
""")
|
|
468
|
+
except Exception:
|
|
469
|
+
pass
|
|
470
|
+
|
|
471
|
+
if removed:
|
|
472
|
+
for entity_id in removed:
|
|
473
|
+
try:
|
|
474
|
+
self._db.run(f"""
|
|
475
|
+
?[id, tier] <- [['{entity_id}', 'cold']]
|
|
476
|
+
:update entity {{id => tier, updated_at: '{now}'}}
|
|
477
|
+
""")
|
|
478
|
+
except Exception:
|
|
479
|
+
pass
|
|
480
|
+
|
|
481
|
+
# ------------------------------------------------------------------
|
|
482
|
+
# Health Check
|
|
483
|
+
# ------------------------------------------------------------------
|
|
484
|
+
|
|
485
|
+
def health_check(self) -> dict[str, Any]:
|
|
486
|
+
"""Return health status of the CozoDB backend."""
|
|
487
|
+
try:
|
|
488
|
+
entity_count = self._db.run(
|
|
489
|
+
"?[count(id)] := *entity{id}"
|
|
490
|
+
)
|
|
491
|
+
edge_count = self._db.run(
|
|
492
|
+
"?[count(from_id)] := *edge{from_id}"
|
|
493
|
+
)
|
|
494
|
+
ec = entity_count.values.tolist()[0][0] if len(entity_count) > 0 else 0
|
|
495
|
+
edc = edge_count.values.tolist()[0][0] if len(edge_count) > 0 else 0
|
|
496
|
+
return {
|
|
497
|
+
"status": "active",
|
|
498
|
+
"entities": int(ec),
|
|
499
|
+
"edges": int(edc),
|
|
500
|
+
"db_path": self._db_path,
|
|
501
|
+
}
|
|
502
|
+
except Exception as exc:
|
|
503
|
+
return {
|
|
504
|
+
"status": "error",
|
|
505
|
+
"error": str(exc),
|
|
506
|
+
"db_path": self._db_path,
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
# ------------------------------------------------------------------
|
|
510
|
+
# Rebuild (from SQLite canonical)
|
|
511
|
+
# ------------------------------------------------------------------
|
|
512
|
+
|
|
513
|
+
def rebuild_from_sqlite(
|
|
514
|
+
self, conn: sqlite3.Connection, profile_id: str = "default"
|
|
515
|
+
) -> int:
|
|
516
|
+
"""Drop all CozoDB data, re-import from SQLite."""
|
|
517
|
+
try:
|
|
518
|
+
self._db.run("::remove entity")
|
|
519
|
+
except Exception:
|
|
520
|
+
pass
|
|
521
|
+
try:
|
|
522
|
+
self._db.run("::remove edge")
|
|
523
|
+
except Exception:
|
|
524
|
+
pass
|
|
525
|
+
|
|
526
|
+
self._ensure_schema()
|
|
527
|
+
return self.bulk_import_from_sqlite(conn, profile_id)
|
|
@@ -36,7 +36,7 @@ class DaemonPoolProxy:
|
|
|
36
36
|
envelopes — the adapter is responsible for surfacing those.
|
|
37
37
|
"""
|
|
38
38
|
|
|
39
|
-
def __init__(self, port: int, *, timeout_s: float =
|
|
39
|
+
def __init__(self, port: int, *, timeout_s: float = 60.0) -> None:
|
|
40
40
|
self._port = port
|
|
41
41
|
self._timeout = timeout_s
|
|
42
42
|
|
|
@@ -26,6 +26,7 @@ class PoolFact:
|
|
|
26
26
|
fact_id: str = ""
|
|
27
27
|
content: str = ""
|
|
28
28
|
memory_id: str = ""
|
|
29
|
+
created_at: str = ""
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
@dataclass(frozen=True)
|
|
@@ -93,6 +94,7 @@ def pool_recall(query: str, limit: int = 10, **kwargs: Any) -> PoolRecallRespons
|
|
|
93
94
|
fact_id=item.get("fact_id", ""),
|
|
94
95
|
content=item.get("content", ""),
|
|
95
96
|
memory_id=item.get("memory_id", ""),
|
|
97
|
+
created_at=item.get("created_at", "") or "",
|
|
96
98
|
),
|
|
97
99
|
score=float(item.get("score", 0.0)),
|
|
98
100
|
confidence=float(item.get("confidence", 0.0)),
|