superlocalmemory 3.2.2 → 3.3.0
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 +43 -1
- package/README.md +106 -71
- package/package.json +1 -2
- package/pyproject.toml +16 -1
- package/src/superlocalmemory/cli/commands.py +309 -0
- package/src/superlocalmemory/cli/main.py +44 -0
- package/src/superlocalmemory/core/config.py +282 -11
- package/src/superlocalmemory/core/consolidation_engine.py +37 -0
- package/src/superlocalmemory/core/engine.py +21 -0
- package/src/superlocalmemory/core/engine_wiring.py +58 -8
- package/src/superlocalmemory/dynamics/activation_guided_quantization.py +374 -0
- package/src/superlocalmemory/dynamics/eap_scheduler.py +276 -0
- package/src/superlocalmemory/dynamics/ebbinghaus_langevin_coupling.py +171 -0
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +804 -0
- package/src/superlocalmemory/hooks/auto_invoker.py +46 -8
- package/src/superlocalmemory/hooks/auto_parameterize.py +147 -0
- package/src/superlocalmemory/infra/heartbeat_monitor.py +140 -0
- package/src/superlocalmemory/infra/pid_manager.py +193 -0
- package/src/superlocalmemory/infra/process_reaper.py +572 -0
- package/src/superlocalmemory/learning/consolidation_quantization_worker.py +115 -0
- package/src/superlocalmemory/learning/forgetting_scheduler.py +263 -0
- package/src/superlocalmemory/learning/quantization_scheduler.py +320 -0
- package/src/superlocalmemory/math/ebbinghaus.py +309 -0
- package/src/superlocalmemory/math/fisher_quantized.py +251 -0
- package/src/superlocalmemory/math/hopfield.py +279 -0
- package/src/superlocalmemory/math/polar_quant.py +379 -0
- package/src/superlocalmemory/math/qjl.py +115 -0
- package/src/superlocalmemory/mcp/server.py +2 -0
- package/src/superlocalmemory/mcp/tools_v3.py +10 -0
- package/src/superlocalmemory/mcp/tools_v33.py +351 -0
- package/src/superlocalmemory/parameterization/__init__.py +47 -0
- package/src/superlocalmemory/parameterization/pattern_extractor.py +534 -0
- package/src/superlocalmemory/parameterization/pii_filter.py +106 -0
- package/src/superlocalmemory/parameterization/prompt_injector.py +216 -0
- package/src/superlocalmemory/parameterization/prompt_lifecycle.py +275 -0
- package/src/superlocalmemory/parameterization/soft_prompt_generator.py +425 -0
- package/src/superlocalmemory/retrieval/engine.py +21 -3
- package/src/superlocalmemory/retrieval/forgetting_filter.py +145 -0
- package/src/superlocalmemory/retrieval/hopfield_channel.py +335 -0
- package/src/superlocalmemory/retrieval/quantization_aware_search.py +133 -0
- package/src/superlocalmemory/retrieval/spreading_activation.py +1 -1
- package/src/superlocalmemory/retrieval/strategy.py +16 -6
- package/src/superlocalmemory/retrieval/vector_store.py +1 -1
- package/src/superlocalmemory/server/routes/agents.py +68 -8
- package/src/superlocalmemory/server/routes/learning.py +18 -1
- package/src/superlocalmemory/server/routes/lifecycle.py +36 -17
- package/src/superlocalmemory/server/routes/v3_api.py +503 -1
- package/src/superlocalmemory/storage/database.py +206 -0
- package/src/superlocalmemory/storage/embedding_migrator.py +178 -0
- package/src/superlocalmemory/storage/migration_v33.py +140 -0
- package/src/superlocalmemory/storage/quantized_store.py +261 -0
- package/src/superlocalmemory/storage/schema_v32.py +137 -0
- package/conftest.py +0 -5
|
@@ -38,24 +38,29 @@ async def lifecycle_status():
|
|
|
38
38
|
conn = sqlite3.connect(str(DB_PATH))
|
|
39
39
|
conn.row_factory = sqlite3.Row
|
|
40
40
|
|
|
41
|
-
#
|
|
41
|
+
# V3.3: Use fact_retention.lifecycle_zone (Ebbinghaus-driven, authoritative)
|
|
42
|
+
# Falls back to atomic_facts.lifecycle for pre-3.3 databases
|
|
42
43
|
states = {}
|
|
43
44
|
try:
|
|
44
45
|
rows = conn.execute(
|
|
45
|
-
"SELECT
|
|
46
|
-
"FROM
|
|
46
|
+
"SELECT lifecycle_zone, COUNT(*) as cnt "
|
|
47
|
+
"FROM fact_retention WHERE profile_id = ? GROUP BY lifecycle_zone",
|
|
47
48
|
(profile,),
|
|
48
49
|
).fetchall()
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
if rows:
|
|
51
|
+
states = {
|
|
52
|
+
row['lifecycle_zone']: row['cnt']
|
|
53
|
+
for row in rows
|
|
54
|
+
}
|
|
53
55
|
except sqlite3.OperationalError:
|
|
54
|
-
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
if not states:
|
|
59
|
+
# Fallback: V3.0-3.2 schema (atomic_facts.lifecycle column)
|
|
55
60
|
try:
|
|
56
61
|
rows = conn.execute(
|
|
57
62
|
"SELECT lifecycle, COUNT(*) as cnt "
|
|
58
|
-
"FROM
|
|
63
|
+
"FROM atomic_facts WHERE profile_id = ? GROUP BY lifecycle",
|
|
59
64
|
(profile,),
|
|
60
65
|
).fetchall()
|
|
61
66
|
states = {
|
|
@@ -63,8 +68,20 @@ async def lifecycle_status():
|
|
|
63
68
|
for row in rows
|
|
64
69
|
}
|
|
65
70
|
except sqlite3.OperationalError:
|
|
66
|
-
#
|
|
67
|
-
|
|
71
|
+
# V2 fallback: memories table
|
|
72
|
+
try:
|
|
73
|
+
rows = conn.execute(
|
|
74
|
+
"SELECT lifecycle, COUNT(*) as cnt "
|
|
75
|
+
"FROM memories WHERE profile = ? GROUP BY lifecycle",
|
|
76
|
+
(profile,),
|
|
77
|
+
).fetchall()
|
|
78
|
+
states = {
|
|
79
|
+
(row['lifecycle'] or 'active'): row['cnt']
|
|
80
|
+
for row in rows
|
|
81
|
+
}
|
|
82
|
+
except sqlite3.OperationalError:
|
|
83
|
+
# No lifecycle column at all — count everything as active
|
|
84
|
+
total = conn.execute(
|
|
68
85
|
"SELECT COUNT(*) FROM atomic_facts WHERE profile_id = ?",
|
|
69
86
|
(profile,),
|
|
70
87
|
).fetchone()[0]
|
|
@@ -72,15 +89,17 @@ async def lifecycle_status():
|
|
|
72
89
|
|
|
73
90
|
total = sum(states.values())
|
|
74
91
|
|
|
75
|
-
# Age distribution per state
|
|
92
|
+
# Age distribution per state (V3.3: join fact_retention with atomic_facts)
|
|
76
93
|
age_stats = {}
|
|
77
|
-
for state in ('active', 'warm', 'cold', '
|
|
94
|
+
for state in ('active', 'warm', 'cold', 'archive', 'forgotten'):
|
|
78
95
|
try:
|
|
79
96
|
row = conn.execute(
|
|
80
|
-
"SELECT AVG(julianday('now') - julianday(created_at)) as avg_age, "
|
|
81
|
-
"MIN(julianday('now') - julianday(created_at)) as min_age, "
|
|
82
|
-
"MAX(julianday('now') - julianday(created_at)) as max_age "
|
|
83
|
-
"FROM
|
|
97
|
+
"SELECT AVG(julianday('now') - julianday(af.created_at)) as avg_age, "
|
|
98
|
+
"MIN(julianday('now') - julianday(af.created_at)) as min_age, "
|
|
99
|
+
"MAX(julianday('now') - julianday(af.created_at)) as max_age "
|
|
100
|
+
"FROM fact_retention fr "
|
|
101
|
+
"JOIN atomic_facts af ON fr.fact_id = af.fact_id "
|
|
102
|
+
"WHERE fr.profile_id = ? AND fr.lifecycle_zone = ?",
|
|
84
103
|
(profile, state),
|
|
85
104
|
).fetchone()
|
|
86
105
|
if row and row['avg_age'] is not None:
|
|
@@ -112,11 +112,22 @@ async def set_mode(request: Request):
|
|
|
112
112
|
new_config.active_profile = old_config.active_profile
|
|
113
113
|
new_config.save()
|
|
114
114
|
|
|
115
|
+
# V3.3: Check if embedding model changed — flag for re-indexing
|
|
116
|
+
needs_reindex = (
|
|
117
|
+
old_config.embedding.provider != new_config.embedding.provider
|
|
118
|
+
or old_config.embedding.model_name != new_config.embedding.model_name
|
|
119
|
+
)
|
|
120
|
+
|
|
115
121
|
# Reset engine to pick up new config
|
|
116
122
|
if hasattr(request.app.state, "engine"):
|
|
117
123
|
request.app.state.engine = None
|
|
118
124
|
|
|
119
|
-
return {
|
|
125
|
+
return {
|
|
126
|
+
"success": True,
|
|
127
|
+
"mode": new_mode,
|
|
128
|
+
"needs_reindex": needs_reindex,
|
|
129
|
+
"message": "Embedding re-indexing will run on next recall." if needs_reindex else "",
|
|
130
|
+
}
|
|
120
131
|
except Exception as e:
|
|
121
132
|
return JSONResponse({"error": str(e)}, status_code=500)
|
|
122
133
|
|
|
@@ -1190,3 +1201,494 @@ async def get_vector_store_status(request: Request, profile: str = ""):
|
|
|
1190
1201
|
return result
|
|
1191
1202
|
except Exception as e:
|
|
1192
1203
|
return JSONResponse({"error": str(e)}, status_code=500)
|
|
1204
|
+
|
|
1205
|
+
|
|
1206
|
+
# ── Phase 10: V3.3 API Endpoints ────────────────────────────
|
|
1207
|
+
# 7 new endpoints for the V3.3 dashboard:
|
|
1208
|
+
# Forgetting (2), Quantization (1), CCQ (1),
|
|
1209
|
+
# Soft Prompts (1), Process Health (1), V3.3 Overview (1)
|
|
1210
|
+
#
|
|
1211
|
+
# Rules enforced:
|
|
1212
|
+
# 01 - Profile scoping on ALL endpoints
|
|
1213
|
+
# 06 - No engine import from routes (direct sqlite3)
|
|
1214
|
+
# 11 - Parameterized SQL everywhere
|
|
1215
|
+
# 19 - Silent errors with JSONResponse
|
|
1216
|
+
# ──────────────────────────────────────────────────────────────
|
|
1217
|
+
|
|
1218
|
+
|
|
1219
|
+
# ── 1a. GET /api/v3/forgetting/stats ────────────────────────
|
|
1220
|
+
|
|
1221
|
+
@router.get("/forgetting/stats")
|
|
1222
|
+
async def forgetting_stats(request: Request, profile: str = ""):
|
|
1223
|
+
"""Get memory retention zone distribution."""
|
|
1224
|
+
try:
|
|
1225
|
+
from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
|
|
1226
|
+
import sqlite3 as _sqlite3
|
|
1227
|
+
pid = profile or get_active_profile()
|
|
1228
|
+
|
|
1229
|
+
zones = {"active": 0, "warm": 0, "cold": 0, "archive": 0, "forgotten": 0}
|
|
1230
|
+
total = 0
|
|
1231
|
+
|
|
1232
|
+
if not DB_PATH.exists():
|
|
1233
|
+
return {"total": total, "zones": zones}
|
|
1234
|
+
|
|
1235
|
+
conn = _sqlite3.connect(str(DB_PATH))
|
|
1236
|
+
conn.row_factory = _sqlite3.Row
|
|
1237
|
+
|
|
1238
|
+
try:
|
|
1239
|
+
rows = conn.execute(
|
|
1240
|
+
"SELECT lifecycle_zone, COUNT(*) AS cnt "
|
|
1241
|
+
"FROM fact_retention WHERE profile_id = ? "
|
|
1242
|
+
"GROUP BY lifecycle_zone",
|
|
1243
|
+
(pid,),
|
|
1244
|
+
).fetchall()
|
|
1245
|
+
for row in rows:
|
|
1246
|
+
zone = dict(row)["lifecycle_zone"]
|
|
1247
|
+
cnt = dict(row)["cnt"]
|
|
1248
|
+
if zone in zones:
|
|
1249
|
+
zones[zone] = cnt
|
|
1250
|
+
total += cnt
|
|
1251
|
+
except Exception:
|
|
1252
|
+
# Table may not exist in older DBs -- graceful fallback
|
|
1253
|
+
pass
|
|
1254
|
+
|
|
1255
|
+
conn.close()
|
|
1256
|
+
return {"total": total, "zones": zones}
|
|
1257
|
+
except Exception as e:
|
|
1258
|
+
return JSONResponse({"error": str(e)}, status_code=500)
|
|
1259
|
+
|
|
1260
|
+
|
|
1261
|
+
# ── 1b. POST /api/v3/forgetting/run ─────────────────────────
|
|
1262
|
+
|
|
1263
|
+
@router.post("/forgetting/run")
|
|
1264
|
+
async def run_forgetting(request: Request):
|
|
1265
|
+
"""Trigger a forgetting decay cycle.
|
|
1266
|
+
|
|
1267
|
+
Body: {"profile": ""} (optional profile override).
|
|
1268
|
+
"""
|
|
1269
|
+
try:
|
|
1270
|
+
body = await request.json()
|
|
1271
|
+
profile = body.get("profile", "")
|
|
1272
|
+
|
|
1273
|
+
from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
|
|
1274
|
+
import sqlite3 as _sqlite3
|
|
1275
|
+
pid = profile or get_active_profile()
|
|
1276
|
+
|
|
1277
|
+
if not DB_PATH.exists():
|
|
1278
|
+
return {"success": False, "error": "Database not found"}
|
|
1279
|
+
|
|
1280
|
+
conn = _sqlite3.connect(str(DB_PATH))
|
|
1281
|
+
conn.row_factory = _sqlite3.Row
|
|
1282
|
+
|
|
1283
|
+
updated = 0
|
|
1284
|
+
try:
|
|
1285
|
+
# Apply Ebbinghaus decay: reduce retention for facts not accessed recently
|
|
1286
|
+
# Formula: retention *= exp(-0.1) for each cycle (simplified batch decay)
|
|
1287
|
+
conn.execute(
|
|
1288
|
+
"UPDATE fact_retention "
|
|
1289
|
+
"SET retention_score = MAX(0.0, retention_score * 0.9), "
|
|
1290
|
+
" last_computed_at = datetime('now') "
|
|
1291
|
+
"WHERE profile_id = ? "
|
|
1292
|
+
"AND lifecycle_zone NOT IN ('archive', 'forgotten')",
|
|
1293
|
+
(pid,),
|
|
1294
|
+
)
|
|
1295
|
+
updated = conn.total_changes
|
|
1296
|
+
|
|
1297
|
+
# Transition zones based on new retention scores
|
|
1298
|
+
zone_thresholds = [
|
|
1299
|
+
("forgotten", 0.05),
|
|
1300
|
+
("archive", 0.15),
|
|
1301
|
+
("cold", 0.35),
|
|
1302
|
+
("warm", 0.65),
|
|
1303
|
+
]
|
|
1304
|
+
for zone, threshold in zone_thresholds:
|
|
1305
|
+
conn.execute(
|
|
1306
|
+
"UPDATE fact_retention "
|
|
1307
|
+
"SET lifecycle_zone = ? "
|
|
1308
|
+
"WHERE profile_id = ? "
|
|
1309
|
+
"AND retention_score < ? "
|
|
1310
|
+
"AND lifecycle_zone NOT IN ('archive', 'forgotten')",
|
|
1311
|
+
(zone, pid, threshold),
|
|
1312
|
+
)
|
|
1313
|
+
|
|
1314
|
+
# Ensure high-retention facts are active
|
|
1315
|
+
conn.execute(
|
|
1316
|
+
"UPDATE fact_retention "
|
|
1317
|
+
"SET lifecycle_zone = 'active' "
|
|
1318
|
+
"WHERE profile_id = ? AND retention_score >= 0.65 "
|
|
1319
|
+
"AND lifecycle_zone NOT IN ('archive', 'forgotten')",
|
|
1320
|
+
(pid,),
|
|
1321
|
+
)
|
|
1322
|
+
|
|
1323
|
+
conn.commit()
|
|
1324
|
+
except Exception as exc:
|
|
1325
|
+
conn.close()
|
|
1326
|
+
return {"success": False, "error": str(exc)}
|
|
1327
|
+
|
|
1328
|
+
conn.close()
|
|
1329
|
+
return {"success": True, "facts_decayed": updated, "profile": pid}
|
|
1330
|
+
except Exception as e:
|
|
1331
|
+
return JSONResponse({"error": str(e)}, status_code=500)
|
|
1332
|
+
|
|
1333
|
+
|
|
1334
|
+
# ── 1c. GET /api/v3/quantization/stats ──────────────────────
|
|
1335
|
+
|
|
1336
|
+
@router.get("/quantization/stats")
|
|
1337
|
+
async def quantization_stats(request: Request, profile: str = ""):
|
|
1338
|
+
"""Get embedding quantization tier distribution."""
|
|
1339
|
+
try:
|
|
1340
|
+
from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
|
|
1341
|
+
import sqlite3 as _sqlite3
|
|
1342
|
+
pid = profile or get_active_profile()
|
|
1343
|
+
|
|
1344
|
+
tiers = {"float32": 0, "int8": 0, "polar4": 0, "polar2": 0}
|
|
1345
|
+
total = 0
|
|
1346
|
+
compression_ratio = 1.0
|
|
1347
|
+
|
|
1348
|
+
if not DB_PATH.exists():
|
|
1349
|
+
return {"total": total, "tiers": tiers, "compression_ratio": compression_ratio}
|
|
1350
|
+
|
|
1351
|
+
conn = _sqlite3.connect(str(DB_PATH))
|
|
1352
|
+
conn.row_factory = _sqlite3.Row
|
|
1353
|
+
|
|
1354
|
+
try:
|
|
1355
|
+
rows = conn.execute(
|
|
1356
|
+
"SELECT quantization_level, COUNT(*) AS cnt "
|
|
1357
|
+
"FROM embedding_quantization_metadata "
|
|
1358
|
+
"WHERE profile_id = ? "
|
|
1359
|
+
"GROUP BY quantization_level",
|
|
1360
|
+
(pid,),
|
|
1361
|
+
).fetchall()
|
|
1362
|
+
for row in rows:
|
|
1363
|
+
level = dict(row)["quantization_level"]
|
|
1364
|
+
cnt = dict(row)["cnt"]
|
|
1365
|
+
if level in tiers:
|
|
1366
|
+
tiers[level] = cnt
|
|
1367
|
+
total += cnt
|
|
1368
|
+
except Exception:
|
|
1369
|
+
pass
|
|
1370
|
+
|
|
1371
|
+
# Compute compression ratio from actual sizes if available
|
|
1372
|
+
try:
|
|
1373
|
+
size_row = conn.execute(
|
|
1374
|
+
"SELECT "
|
|
1375
|
+
"SUM(CASE WHEN bit_width = 32 THEN 768 * 4 ELSE "
|
|
1376
|
+
" COALESCE(compressed_size_bytes, 768 * bit_width / 8) END) AS actual, "
|
|
1377
|
+
"SUM(768 * 4) AS uncompressed "
|
|
1378
|
+
"FROM embedding_quantization_metadata "
|
|
1379
|
+
"WHERE profile_id = ?",
|
|
1380
|
+
(pid,),
|
|
1381
|
+
).fetchone()
|
|
1382
|
+
if size_row:
|
|
1383
|
+
d = dict(size_row)
|
|
1384
|
+
uncompressed = d.get("uncompressed") or 0
|
|
1385
|
+
actual = d.get("actual") or 0
|
|
1386
|
+
if actual > 0 and uncompressed > 0:
|
|
1387
|
+
compression_ratio = round(uncompressed / actual, 2)
|
|
1388
|
+
except Exception:
|
|
1389
|
+
pass
|
|
1390
|
+
|
|
1391
|
+
conn.close()
|
|
1392
|
+
return {"total": total, "tiers": tiers, "compression_ratio": compression_ratio}
|
|
1393
|
+
except Exception as e:
|
|
1394
|
+
return JSONResponse({"error": str(e)}, status_code=500)
|
|
1395
|
+
|
|
1396
|
+
|
|
1397
|
+
# ── 1d. GET /api/v3/ccq/blocks ──────────────────────────────
|
|
1398
|
+
|
|
1399
|
+
@router.get("/ccq/blocks")
|
|
1400
|
+
async def ccq_blocks(request: Request, profile: str = "", limit: int = 50):
|
|
1401
|
+
"""Get CCQ consolidated blocks."""
|
|
1402
|
+
try:
|
|
1403
|
+
from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
|
|
1404
|
+
import sqlite3 as _sqlite3
|
|
1405
|
+
pid = profile or get_active_profile()
|
|
1406
|
+
|
|
1407
|
+
if not DB_PATH.exists():
|
|
1408
|
+
return {"blocks": [], "total": 0}
|
|
1409
|
+
|
|
1410
|
+
conn = _sqlite3.connect(str(DB_PATH))
|
|
1411
|
+
conn.row_factory = _sqlite3.Row
|
|
1412
|
+
|
|
1413
|
+
blocks = []
|
|
1414
|
+
total = 0
|
|
1415
|
+
try:
|
|
1416
|
+
rows = conn.execute(
|
|
1417
|
+
"SELECT block_id, content, source_fact_ids, char_count, "
|
|
1418
|
+
"compiled_by, cluster_id, created_at "
|
|
1419
|
+
"FROM ccq_consolidated_blocks "
|
|
1420
|
+
"WHERE profile_id = ? "
|
|
1421
|
+
"ORDER BY created_at DESC LIMIT ?",
|
|
1422
|
+
(pid, limit),
|
|
1423
|
+
).fetchall()
|
|
1424
|
+
|
|
1425
|
+
for row in rows:
|
|
1426
|
+
d = dict(row)
|
|
1427
|
+
source_ids = []
|
|
1428
|
+
try:
|
|
1429
|
+
source_ids = json.loads(d.get("source_fact_ids", "[]"))
|
|
1430
|
+
except (json.JSONDecodeError, TypeError):
|
|
1431
|
+
pass
|
|
1432
|
+
blocks.append({
|
|
1433
|
+
"block_id": d["block_id"],
|
|
1434
|
+
"content": d["content"],
|
|
1435
|
+
"source_fact_count": len(source_ids),
|
|
1436
|
+
"char_count": d["char_count"],
|
|
1437
|
+
"compiled_by": d["compiled_by"],
|
|
1438
|
+
"cluster_id": d["cluster_id"],
|
|
1439
|
+
"created_at": d["created_at"],
|
|
1440
|
+
})
|
|
1441
|
+
|
|
1442
|
+
count_row = conn.execute(
|
|
1443
|
+
"SELECT COUNT(*) FROM ccq_consolidated_blocks "
|
|
1444
|
+
"WHERE profile_id = ?",
|
|
1445
|
+
(pid,),
|
|
1446
|
+
).fetchone()
|
|
1447
|
+
total = count_row[0] if count_row else 0
|
|
1448
|
+
except Exception:
|
|
1449
|
+
pass
|
|
1450
|
+
|
|
1451
|
+
conn.close()
|
|
1452
|
+
return {"blocks": blocks, "total": total}
|
|
1453
|
+
except Exception as e:
|
|
1454
|
+
return JSONResponse({"error": str(e)}, status_code=500)
|
|
1455
|
+
|
|
1456
|
+
|
|
1457
|
+
# ── 1e. GET /api/v3/soft-prompts ─────────────────────────────
|
|
1458
|
+
|
|
1459
|
+
@router.get("/soft-prompts")
|
|
1460
|
+
async def get_soft_prompts(request: Request, profile: str = ""):
|
|
1461
|
+
"""Get active soft prompt templates."""
|
|
1462
|
+
try:
|
|
1463
|
+
from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
|
|
1464
|
+
import sqlite3 as _sqlite3
|
|
1465
|
+
pid = profile or get_active_profile()
|
|
1466
|
+
|
|
1467
|
+
if not DB_PATH.exists():
|
|
1468
|
+
return {"prompts": [], "total": 0, "total_tokens": 0}
|
|
1469
|
+
|
|
1470
|
+
conn = _sqlite3.connect(str(DB_PATH))
|
|
1471
|
+
conn.row_factory = _sqlite3.Row
|
|
1472
|
+
|
|
1473
|
+
prompts = []
|
|
1474
|
+
total_tokens = 0
|
|
1475
|
+
try:
|
|
1476
|
+
rows = conn.execute(
|
|
1477
|
+
"SELECT prompt_id, category, content, confidence, "
|
|
1478
|
+
"effectiveness, token_count, retention_score, "
|
|
1479
|
+
"active, version, created_at, updated_at "
|
|
1480
|
+
"FROM soft_prompt_templates "
|
|
1481
|
+
"WHERE profile_id = ? AND active = 1 "
|
|
1482
|
+
"ORDER BY confidence DESC",
|
|
1483
|
+
(pid,),
|
|
1484
|
+
).fetchall()
|
|
1485
|
+
|
|
1486
|
+
for row in rows:
|
|
1487
|
+
d = dict(row)
|
|
1488
|
+
tokens = d.get("token_count", 0)
|
|
1489
|
+
total_tokens += tokens
|
|
1490
|
+
prompts.append({
|
|
1491
|
+
"prompt_id": d["prompt_id"],
|
|
1492
|
+
"category": d["category"],
|
|
1493
|
+
"content": d["content"][:200],
|
|
1494
|
+
"confidence": round(float(d["confidence"]), 3),
|
|
1495
|
+
"effectiveness": round(float(d.get("effectiveness", 0.5)), 3),
|
|
1496
|
+
"token_count": tokens,
|
|
1497
|
+
"retention_score": round(float(d.get("retention_score", 1.0)), 3),
|
|
1498
|
+
"version": d["version"],
|
|
1499
|
+
"created_at": d["created_at"],
|
|
1500
|
+
})
|
|
1501
|
+
except Exception:
|
|
1502
|
+
pass
|
|
1503
|
+
|
|
1504
|
+
conn.close()
|
|
1505
|
+
return {"prompts": prompts, "total": len(prompts), "total_tokens": total_tokens}
|
|
1506
|
+
except Exception as e:
|
|
1507
|
+
return JSONResponse({"error": str(e)}, status_code=500)
|
|
1508
|
+
|
|
1509
|
+
|
|
1510
|
+
# ── 1f. GET /api/v3/health/processes ─────────────────────────
|
|
1511
|
+
|
|
1512
|
+
@router.get("/health/processes")
|
|
1513
|
+
async def process_health(request: Request):
|
|
1514
|
+
"""Get SLM process health status."""
|
|
1515
|
+
try:
|
|
1516
|
+
import os as _os
|
|
1517
|
+
|
|
1518
|
+
processes = {
|
|
1519
|
+
"mcp_server": {"pid": _os.getpid(), "status": "running"},
|
|
1520
|
+
"parent": {"pid": _os.getppid(), "status": "unknown"},
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
# Check parent process
|
|
1524
|
+
try:
|
|
1525
|
+
_os.kill(_os.getppid(), 0)
|
|
1526
|
+
processes["parent"]["status"] = "running"
|
|
1527
|
+
except ProcessLookupError:
|
|
1528
|
+
processes["parent"]["status"] = "dead"
|
|
1529
|
+
except PermissionError:
|
|
1530
|
+
processes["parent"]["status"] = "running"
|
|
1531
|
+
except OSError:
|
|
1532
|
+
processes["parent"]["status"] = "unknown"
|
|
1533
|
+
|
|
1534
|
+
# Check worker pool status
|
|
1535
|
+
worker_status = "unavailable"
|
|
1536
|
+
try:
|
|
1537
|
+
from superlocalmemory.core.worker_pool import WorkerPool
|
|
1538
|
+
pool = WorkerPool.shared()
|
|
1539
|
+
worker_status = "running" if pool else "stopped"
|
|
1540
|
+
except Exception:
|
|
1541
|
+
pass
|
|
1542
|
+
processes["worker_pool"] = {"status": worker_status}
|
|
1543
|
+
|
|
1544
|
+
# Memory usage of current process (approximate)
|
|
1545
|
+
memory_mb = 0.0
|
|
1546
|
+
try:
|
|
1547
|
+
import resource
|
|
1548
|
+
usage = resource.getrusage(resource.RUSAGE_SELF)
|
|
1549
|
+
memory_mb = round(usage.ru_maxrss / (1024 * 1024), 1)
|
|
1550
|
+
except Exception:
|
|
1551
|
+
pass
|
|
1552
|
+
|
|
1553
|
+
return {
|
|
1554
|
+
"processes": processes,
|
|
1555
|
+
"memory_mb": memory_mb,
|
|
1556
|
+
"healthy": processes["parent"]["status"] != "dead",
|
|
1557
|
+
}
|
|
1558
|
+
except Exception as e:
|
|
1559
|
+
return JSONResponse({"error": str(e)}, status_code=500)
|
|
1560
|
+
|
|
1561
|
+
|
|
1562
|
+
# ── 1g. GET /api/v3/v33/overview ─────────────────────────────
|
|
1563
|
+
|
|
1564
|
+
@router.get("/v33/overview")
|
|
1565
|
+
async def v33_overview(request: Request, profile: str = ""):
|
|
1566
|
+
"""Get SLM 3.3 feature overview -- all new capabilities at a glance."""
|
|
1567
|
+
try:
|
|
1568
|
+
from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
|
|
1569
|
+
import sqlite3 as _sqlite3
|
|
1570
|
+
pid = profile or get_active_profile()
|
|
1571
|
+
|
|
1572
|
+
overview: dict = {
|
|
1573
|
+
"version": "3.3",
|
|
1574
|
+
"profile": pid,
|
|
1575
|
+
"forgetting": {"total": 0, "zones": {}},
|
|
1576
|
+
"quantization": {"total": 0, "tiers": {}, "compression_ratio": 1.0},
|
|
1577
|
+
"ccq": {"blocks": 0, "facts_archived": 0},
|
|
1578
|
+
"soft_prompts": {"total": 0, "total_tokens": 0},
|
|
1579
|
+
"hopfield": {
|
|
1580
|
+
"available": False,
|
|
1581
|
+
"description": "Modern Continuous Hopfield Network retrieval channel",
|
|
1582
|
+
},
|
|
1583
|
+
"process_health": {"healthy": True},
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
if not DB_PATH.exists():
|
|
1587
|
+
return overview
|
|
1588
|
+
|
|
1589
|
+
conn = _sqlite3.connect(str(DB_PATH))
|
|
1590
|
+
conn.row_factory = _sqlite3.Row
|
|
1591
|
+
|
|
1592
|
+
# Forgetting stats
|
|
1593
|
+
try:
|
|
1594
|
+
zones = {"active": 0, "warm": 0, "cold": 0, "archive": 0, "forgotten": 0}
|
|
1595
|
+
rows = conn.execute(
|
|
1596
|
+
"SELECT lifecycle_zone, COUNT(*) AS cnt "
|
|
1597
|
+
"FROM fact_retention WHERE profile_id = ? "
|
|
1598
|
+
"GROUP BY lifecycle_zone",
|
|
1599
|
+
(pid,),
|
|
1600
|
+
).fetchall()
|
|
1601
|
+
total_fg = 0
|
|
1602
|
+
for row in rows:
|
|
1603
|
+
d = dict(row)
|
|
1604
|
+
zone = d["lifecycle_zone"]
|
|
1605
|
+
if zone in zones:
|
|
1606
|
+
zones[zone] = d["cnt"]
|
|
1607
|
+
total_fg += d["cnt"]
|
|
1608
|
+
overview["forgetting"] = {"total": total_fg, "zones": zones}
|
|
1609
|
+
except Exception:
|
|
1610
|
+
pass
|
|
1611
|
+
|
|
1612
|
+
# Quantization stats
|
|
1613
|
+
try:
|
|
1614
|
+
tiers = {"float32": 0, "int8": 0, "polar4": 0, "polar2": 0}
|
|
1615
|
+
rows = conn.execute(
|
|
1616
|
+
"SELECT quantization_level, COUNT(*) AS cnt "
|
|
1617
|
+
"FROM embedding_quantization_metadata "
|
|
1618
|
+
"WHERE profile_id = ? GROUP BY quantization_level",
|
|
1619
|
+
(pid,),
|
|
1620
|
+
).fetchall()
|
|
1621
|
+
total_q = 0
|
|
1622
|
+
for row in rows:
|
|
1623
|
+
d = dict(row)
|
|
1624
|
+
level = d["quantization_level"]
|
|
1625
|
+
if level in tiers:
|
|
1626
|
+
tiers[level] = d["cnt"]
|
|
1627
|
+
total_q += d["cnt"]
|
|
1628
|
+
overview["quantization"] = {
|
|
1629
|
+
"total": total_q, "tiers": tiers, "compression_ratio": 1.0,
|
|
1630
|
+
}
|
|
1631
|
+
except Exception:
|
|
1632
|
+
pass
|
|
1633
|
+
|
|
1634
|
+
# CCQ stats
|
|
1635
|
+
try:
|
|
1636
|
+
block_count = conn.execute(
|
|
1637
|
+
"SELECT COUNT(*) FROM ccq_consolidated_blocks "
|
|
1638
|
+
"WHERE profile_id = ?", (pid,),
|
|
1639
|
+
).fetchone()[0]
|
|
1640
|
+
# Count archived facts (lifecycle='archived' from CCQ)
|
|
1641
|
+
archived_count = 0
|
|
1642
|
+
try:
|
|
1643
|
+
archived_count = conn.execute(
|
|
1644
|
+
"SELECT COUNT(*) FROM atomic_facts "
|
|
1645
|
+
"WHERE profile_id = ? AND lifecycle = 'archived'",
|
|
1646
|
+
(pid,),
|
|
1647
|
+
).fetchone()[0]
|
|
1648
|
+
except Exception:
|
|
1649
|
+
pass
|
|
1650
|
+
overview["ccq"] = {
|
|
1651
|
+
"blocks": block_count,
|
|
1652
|
+
"facts_archived": archived_count,
|
|
1653
|
+
}
|
|
1654
|
+
except Exception:
|
|
1655
|
+
pass
|
|
1656
|
+
|
|
1657
|
+
# Soft prompts stats
|
|
1658
|
+
try:
|
|
1659
|
+
prompt_rows = conn.execute(
|
|
1660
|
+
"SELECT COUNT(*) AS cnt, COALESCE(SUM(token_count), 0) AS tokens "
|
|
1661
|
+
"FROM soft_prompt_templates "
|
|
1662
|
+
"WHERE profile_id = ? AND active = 1",
|
|
1663
|
+
(pid,),
|
|
1664
|
+
).fetchone()
|
|
1665
|
+
if prompt_rows:
|
|
1666
|
+
d = dict(prompt_rows)
|
|
1667
|
+
overview["soft_prompts"] = {
|
|
1668
|
+
"total": d["cnt"],
|
|
1669
|
+
"total_tokens": d["tokens"],
|
|
1670
|
+
}
|
|
1671
|
+
except Exception:
|
|
1672
|
+
pass
|
|
1673
|
+
|
|
1674
|
+
# Hopfield channel availability
|
|
1675
|
+
try:
|
|
1676
|
+
from superlocalmemory.retrieval.hopfield_channel import HopfieldChannel # noqa: F401
|
|
1677
|
+
overview["hopfield"]["available"] = True
|
|
1678
|
+
except ImportError:
|
|
1679
|
+
pass
|
|
1680
|
+
|
|
1681
|
+
# Process health
|
|
1682
|
+
try:
|
|
1683
|
+
import os as _os
|
|
1684
|
+
_os.kill(_os.getppid(), 0)
|
|
1685
|
+
overview["process_health"] = {"healthy": True}
|
|
1686
|
+
except ProcessLookupError:
|
|
1687
|
+
overview["process_health"] = {"healthy": False}
|
|
1688
|
+
except (PermissionError, OSError):
|
|
1689
|
+
overview["process_health"] = {"healthy": True}
|
|
1690
|
+
|
|
1691
|
+
conn.close()
|
|
1692
|
+
return overview
|
|
1693
|
+
except Exception as e:
|
|
1694
|
+
return JSONResponse({"error": str(e)}, status_code=500)
|