superlocalmemory 3.4.0 → 3.4.3

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 (63) hide show
  1. package/README.md +7 -8
  2. package/docs/screenshots/01-dashboard-main.png +0 -0
  3. package/docs/screenshots/02-knowledge-graph.png +0 -0
  4. package/docs/screenshots/03-patterns-learning.png +0 -0
  5. package/docs/screenshots/04-learning-dashboard.png +0 -0
  6. package/docs/screenshots/05-behavioral-analysis.png +0 -0
  7. package/docs/screenshots/06-graph-communities.png +0 -0
  8. package/package.json +2 -2
  9. package/pyproject.toml +11 -2
  10. package/scripts/postinstall.js +26 -7
  11. package/src/superlocalmemory/cli/commands.py +42 -60
  12. package/src/superlocalmemory/cli/daemon.py +107 -47
  13. package/src/superlocalmemory/cli/main.py +10 -0
  14. package/src/superlocalmemory/cli/setup_wizard.py +137 -9
  15. package/src/superlocalmemory/core/config.py +28 -0
  16. package/src/superlocalmemory/core/consolidation_engine.py +38 -1
  17. package/src/superlocalmemory/core/engine.py +9 -0
  18. package/src/superlocalmemory/core/engine_wiring.py +5 -1
  19. package/src/superlocalmemory/core/graph_analyzer.py +254 -12
  20. package/src/superlocalmemory/core/health_monitor.py +313 -0
  21. package/src/superlocalmemory/core/reranker_worker.py +19 -5
  22. package/src/superlocalmemory/ingestion/__init__.py +13 -0
  23. package/src/superlocalmemory/ingestion/adapter_manager.py +234 -0
  24. package/src/superlocalmemory/ingestion/base_adapter.py +177 -0
  25. package/src/superlocalmemory/ingestion/calendar_adapter.py +340 -0
  26. package/src/superlocalmemory/ingestion/credentials.py +118 -0
  27. package/src/superlocalmemory/ingestion/gmail_adapter.py +369 -0
  28. package/src/superlocalmemory/ingestion/parsers.py +100 -0
  29. package/src/superlocalmemory/ingestion/transcript_adapter.py +156 -0
  30. package/src/superlocalmemory/learning/consolidation_worker.py +287 -53
  31. package/src/superlocalmemory/learning/entity_compiler.py +377 -0
  32. package/src/superlocalmemory/mesh/__init__.py +12 -0
  33. package/src/superlocalmemory/mesh/broker.py +344 -0
  34. package/src/superlocalmemory/retrieval/entity_channel.py +141 -4
  35. package/src/superlocalmemory/retrieval/spreading_activation.py +45 -0
  36. package/src/superlocalmemory/server/api.py +15 -8
  37. package/src/superlocalmemory/server/routes/behavioral.py +8 -4
  38. package/src/superlocalmemory/server/routes/chat.py +320 -0
  39. package/src/superlocalmemory/server/routes/entity.py +95 -0
  40. package/src/superlocalmemory/server/routes/ingest.py +110 -0
  41. package/src/superlocalmemory/server/routes/insights.py +368 -0
  42. package/src/superlocalmemory/server/routes/learning.py +106 -6
  43. package/src/superlocalmemory/server/routes/memories.py +20 -9
  44. package/src/superlocalmemory/server/routes/mesh.py +186 -0
  45. package/src/superlocalmemory/server/routes/stats.py +25 -3
  46. package/src/superlocalmemory/server/routes/timeline.py +252 -0
  47. package/src/superlocalmemory/server/routes/v3_api.py +161 -0
  48. package/src/superlocalmemory/server/ui.py +8 -0
  49. package/src/superlocalmemory/server/unified_daemon.py +691 -0
  50. package/src/superlocalmemory/storage/schema_v343.py +229 -0
  51. package/src/superlocalmemory/ui/index.html +168 -58
  52. package/src/superlocalmemory/ui/js/graph-event-bus.js +83 -0
  53. package/src/superlocalmemory/ui/js/graph-filters.js +1 -1
  54. package/src/superlocalmemory/ui/js/knowledge-graph.js +942 -0
  55. package/src/superlocalmemory/ui/js/memory-chat.js +344 -0
  56. package/src/superlocalmemory/ui/js/memory-timeline.js +265 -0
  57. package/src/superlocalmemory/ui/js/quick-actions.js +334 -0
  58. package/src/superlocalmemory.egg-info/PKG-INFO +0 -594
  59. package/src/superlocalmemory.egg-info/SOURCES.txt +0 -279
  60. package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
  61. package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
  62. package/src/superlocalmemory.egg-info/requires.txt +0 -47
  63. package/src/superlocalmemory.egg-info/top_level.txt +0 -1
@@ -73,7 +73,38 @@ class ConsolidationWorker:
73
73
  except Exception as exc:
74
74
  logger.debug("Pattern generation failed: %s", exc)
75
75
 
76
- # 4. Check if ranker should retrain
76
+ # 4. Recompute graph intelligence (v3.4.2: wired into learning pipeline)
77
+ try:
78
+ from superlocalmemory.core.graph_analyzer import GraphAnalyzer
79
+ conn_ga = sqlite3.connect(self._memory_db, timeout=10)
80
+ conn_ga.execute("PRAGMA busy_timeout=5000")
81
+ conn_ga.row_factory = sqlite3.Row
82
+
83
+ class _DBProxy:
84
+ """Minimal DB proxy for GraphAnalyzer compatibility."""
85
+ def __init__(self, connection: sqlite3.Connection) -> None:
86
+ self._conn = connection
87
+ def execute(self, sql: str, params: tuple = ()) -> list:
88
+ cursor = self._conn.execute(sql, params)
89
+ if sql.strip().upper().startswith(("INSERT", "UPDATE", "DELETE", "ALTER", "CREATE")):
90
+ self._conn.commit()
91
+ return []
92
+ return cursor.fetchall()
93
+
94
+ ga = GraphAnalyzer(_DBProxy(conn_ga))
95
+ if not dry_run:
96
+ ga_result = ga.compute_and_store(profile_id)
97
+ stats["graph_nodes"] = ga_result.get("node_count", 0)
98
+ stats["graph_communities"] = ga_result.get("community_count", 0)
99
+ logger.info(
100
+ "Graph analysis: %d nodes, %d communities",
101
+ stats["graph_nodes"], stats["graph_communities"],
102
+ )
103
+ conn_ga.close()
104
+ except Exception as exc:
105
+ logger.debug("Graph analysis failed: %s", exc)
106
+
107
+ # 5. Check if ranker should retrain
77
108
  try:
78
109
  from superlocalmemory.learning.feedback import FeedbackCollector
79
110
  collector = FeedbackCollector(Path(self._learning_db))
@@ -88,6 +119,21 @@ class ConsolidationWorker:
88
119
  except Exception as exc:
89
120
  logger.debug("Retrain check failed: %s", exc)
90
121
 
122
+ # 6. Entity compilation (v3.4.3: compiled truth per entity)
123
+ if not dry_run:
124
+ try:
125
+ from superlocalmemory.learning.entity_compiler import EntityCompiler
126
+ from superlocalmemory.core.config import SLMConfig
127
+ config = SLMConfig.load()
128
+ compiler = EntityCompiler(self._memory_db, config)
129
+ ec_result = compiler.compile_all(profile_id)
130
+ stats["entities_compiled"] = ec_result.get("compiled", 0)
131
+ if ec_result["compiled"] > 0:
132
+ logger.info("Entity compilation: %d entities compiled",
133
+ ec_result["compiled"])
134
+ except Exception as exc:
135
+ logger.debug("Entity compilation failed: %s", exc)
136
+
91
137
  return stats
92
138
 
93
139
  def _deduplicate(self, profile_id: str, dry_run: bool) -> int:
@@ -133,127 +179,315 @@ class ConsolidationWorker:
133
179
  return 0
134
180
 
135
181
  def _generate_patterns(self, profile_id: str, dry_run: bool) -> int:
136
- """Mine behavioral patterns from existing memories.
182
+ """Mine behavioral patterns from ALL memory sources.
137
183
 
138
- Scans all facts to detect:
139
- - Tech preferences (language/framework mentions)
140
- - Topic clusters (frequently discussed subjects)
141
- - Temporal patterns (time-of-day activity)
184
+ v3.4.1: Expanded from 3 to 7 pattern types. No 500-fact cap.
185
+ Analyzes: facts, signals, co-retrieval edges, channel credits,
186
+ entities, sessions, graph communities.
142
187
  """
143
188
  try:
144
189
  from superlocalmemory.learning.behavioral import BehavioralPatternStore
145
190
  import re
146
- from collections import Counter
191
+ from collections import Counter, defaultdict
147
192
 
148
193
  conn = sqlite3.connect(self._memory_db, timeout=10)
149
194
  conn.execute("PRAGMA busy_timeout=5000")
150
195
  conn.row_factory = sqlite3.Row
151
196
 
197
+ # v3.4.1: No cap — analyze ALL facts
152
198
  facts = conn.execute(
153
- "SELECT content, created_at FROM atomic_facts "
154
- "WHERE profile_id = ? ORDER BY created_at DESC LIMIT 500",
199
+ "SELECT fact_id, content, fact_type, created_at, session_id, "
200
+ "confidence, canonical_entities_json "
201
+ "FROM atomic_facts "
202
+ "WHERE profile_id = ? AND lifecycle = 'active' "
203
+ "ORDER BY created_at DESC",
155
204
  (profile_id,),
156
205
  ).fetchall()
157
- conn.close()
158
206
 
159
- if len(facts) < 10:
207
+ if len(facts) < 5:
208
+ conn.close()
160
209
  return 0
161
210
 
162
211
  store = BehavioralPatternStore(self._learning_db)
163
212
  generated = 0
164
213
 
165
- # Tech preferences: detect technology mentions
214
+ # ── 1. Tech Preferences (expanded keyword list) ───────────
166
215
  tech_keywords = {
167
- "python": "Python", "javascript": "JavaScript", "typescript": "TypeScript",
168
- "react": "React", "vue": "Vue", "angular": "Angular",
169
- "postgresql": "PostgreSQL", "mysql": "MySQL", "sqlite": "SQLite",
170
- "docker": "Docker", "kubernetes": "Kubernetes", "aws": "AWS",
171
- "azure": "Azure", "gcp": "GCP", "node": "Node.js",
172
- "fastapi": "FastAPI", "django": "Django", "flask": "Flask",
216
+ "python": "Python", "javascript": "JavaScript",
217
+ "typescript": "TypeScript", "react": "React",
218
+ "vue": "Vue", "angular": "Angular",
219
+ "postgresql": "PostgreSQL", "mysql": "MySQL",
220
+ "sqlite": "SQLite", "docker": "Docker",
221
+ "kubernetes": "Kubernetes", "aws": "AWS",
222
+ "azure": "Azure", "gcp": "GCP",
223
+ "node": "Node.js", "fastapi": "FastAPI",
224
+ "django": "Django", "flask": "Flask",
173
225
  "rust": "Rust", "go": "Go", "java": "Java",
174
226
  "git": "Git", "npm": "npm", "pip": "pip",
175
- "langchain": "LangChain", "ollama": "Ollama", "pytorch": "PyTorch",
176
- "claude": "Claude", "openai": "OpenAI", "anthropic": "Anthropic",
227
+ "langchain": "LangChain", "ollama": "Ollama",
228
+ "pytorch": "PyTorch", "claude": "Claude",
229
+ "openai": "OpenAI", "anthropic": "Anthropic",
230
+ "redis": "Redis", "mongodb": "MongoDB",
231
+ "graphql": "GraphQL", "nextjs": "Next.js",
232
+ "terraform": "Terraform", "nginx": "Nginx",
233
+ "linux": "Linux", "macos": "macOS",
234
+ "vscode": "VS Code", "neovim": "Neovim",
177
235
  }
178
236
 
179
- tech_counts = Counter()
237
+ tech_counts: Counter = Counter()
180
238
  for f in facts:
181
239
  content = dict(f)["content"].lower()
182
240
  for keyword, label in tech_keywords.items():
183
241
  if keyword in content:
184
242
  tech_counts[label] += 1
185
243
 
186
- for tech, count in tech_counts.most_common(15):
187
- if count >= 3 and not dry_run:
188
- confidence = min(1.0, count / 20)
244
+ for tech, count in tech_counts.most_common(20):
245
+ if count >= 2 and not dry_run:
246
+ confidence = min(1.0, count / max(len(facts) * 0.1, 10))
189
247
  store.record_pattern(
190
248
  profile_id=profile_id,
191
249
  pattern_type="tech_preference",
192
- data={"topic": tech, "pattern_key": tech, "value": tech,
193
- "key": "tech", "evidence": count},
250
+ data={"topic": tech, "pattern_key": tech,
251
+ "value": tech, "key": "tech",
252
+ "evidence": count},
194
253
  success_rate=confidence,
195
254
  confidence=confidence,
196
255
  )
197
256
  generated += 1
198
257
 
199
- # Topic clusters: most discussed subjects
200
- word_counts = Counter()
258
+ # ── 2. Topic Interests (word frequency) ───────────────────
201
259
  stopwords = frozenset({
202
- "the", "is", "a", "an", "in", "on", "at", "to", "for", "of",
203
- "and", "or", "not", "with", "that", "this", "was", "are", "be",
204
- "has", "had", "have", "from", "by", "it", "its", "as", "but",
260
+ "the", "is", "a", "an", "in", "on", "at", "to", "for",
261
+ "of", "and", "or", "not", "with", "that", "this", "was",
262
+ "are", "be", "has", "had", "have", "from", "by", "it",
263
+ "its", "as", "but", "were", "been", "being", "would",
264
+ "could", "should", "will", "may", "might", "can", "do",
265
+ "does", "did", "about", "into", "over", "after", "before",
266
+ "then", "than", "also", "just", "like", "more", "some",
267
+ "only", "other", "such", "each", "every", "both", "most",
205
268
  })
269
+ word_counts: Counter = Counter()
206
270
  for f in facts:
207
271
  words = re.findall(r'\b[a-zA-Z]{4,}\b', dict(f)["content"].lower())
208
272
  for w in words:
209
273
  if w not in stopwords:
210
274
  word_counts[w] += 1
211
275
 
212
- for topic, count in word_counts.most_common(10):
213
- if count >= 5 and not dry_run:
214
- confidence = min(1.0, count / 30)
276
+ for topic, count in word_counts.most_common(15):
277
+ if count >= 3 and not dry_run:
278
+ confidence = min(1.0, count / max(len(facts) * 0.05, 15))
215
279
  store.record_pattern(
216
280
  profile_id=profile_id,
217
281
  pattern_type="interest",
218
282
  data={"topic": topic, "pattern_key": topic,
219
- "count": count, "evidence": count},
283
+ "count": count, "evidence": count},
220
284
  success_rate=confidence,
221
285
  confidence=confidence,
222
286
  )
223
287
  generated += 1
224
288
 
225
- # Temporal patterns: time-of-day activity
226
- hour_counts = Counter()
289
+ # ── 3. Temporal Activity Patterns ─────────────────────────
290
+ hour_counts: Counter = Counter()
227
291
  for f in facts:
228
292
  created = dict(f).get("created_at", "")
229
- if "T" in created:
230
- try:
293
+ try:
294
+ if "T" in created:
231
295
  hour = int(created.split("T")[1][:2])
232
- period = "morning" if 6 <= hour < 12 else (
233
- "afternoon" if 12 <= hour < 18 else (
234
- "evening" if 18 <= hour < 22 else "night"))
235
- hour_counts[period] += 1
236
- except (ValueError, IndexError):
237
- pass
238
-
296
+ elif " " in created:
297
+ hour = int(created.split(" ")[1][:2])
298
+ else:
299
+ continue
300
+ period = ("morning" if 6 <= hour < 12 else
301
+ "afternoon" if 12 <= hour < 18 else
302
+ "evening" if 18 <= hour < 22 else "night")
303
+ hour_counts[period] += 1
304
+ except (ValueError, IndexError):
305
+ pass
306
+
307
+ total_hours = sum(hour_counts.values())
239
308
  for period, count in hour_counts.most_common():
240
- if count >= 3 and not dry_run:
241
- total = sum(hour_counts.values())
242
- pct = round(count / total * 100)
309
+ if count >= 2 and total_hours > 0 and not dry_run:
310
+ pct = round(count / total_hours * 100)
243
311
  store.record_pattern(
244
312
  profile_id=profile_id,
245
313
  pattern_type="temporal",
246
314
  data={"topic": period, "pattern_key": period,
247
- "value": f"{period} ({pct}%)", "evidence": count,
248
- "key": period, "distribution": dict(hour_counts)},
315
+ "value": f"{period} ({pct}%)",
316
+ "evidence": count, "key": period,
317
+ "distribution": dict(hour_counts)},
249
318
  success_rate=pct / 100,
250
- confidence=min(1.0, count / 20),
319
+ confidence=min(1.0, count / max(total_hours * 0.1, 5)),
320
+ )
321
+ generated += 1
322
+
323
+ # ── 4. Entity Preferences (v3.4.1 NEW) ───────────────────
324
+ import json as _json
325
+ entity_counts: Counter = Counter()
326
+ for f in facts:
327
+ raw = dict(f).get("canonical_entities_json", "")
328
+ if raw:
329
+ try:
330
+ for ent in _json.loads(raw):
331
+ entity_counts[ent] += 1
332
+ except (ValueError, TypeError):
333
+ pass
334
+
335
+ for entity, count in entity_counts.most_common(15):
336
+ if count >= 3 and not dry_run:
337
+ confidence = min(1.0, count / max(len(facts) * 0.05, 10))
338
+ store.record_pattern(
339
+ profile_id=profile_id,
340
+ pattern_type="interest",
341
+ data={"topic": entity, "pattern_key": f"entity:{entity}",
342
+ "value": entity, "evidence": count,
343
+ "source": "entity_frequency"},
344
+ success_rate=confidence,
345
+ confidence=confidence,
346
+ )
347
+ generated += 1
348
+
349
+ # ── 5. Session Activity Patterns (v3.4.1 NEW) ────────────
350
+ session_counts: Counter = Counter()
351
+ for f in facts:
352
+ sid = dict(f).get("session_id", "")
353
+ if sid:
354
+ session_counts[sid] += 1
355
+
356
+ if session_counts:
357
+ avg_facts_per_session = sum(session_counts.values()) / len(session_counts)
358
+ heavy_sessions = [s for s, c in session_counts.items() if c > avg_facts_per_session * 2]
359
+ if heavy_sessions and not dry_run:
360
+ store.record_pattern(
361
+ profile_id=profile_id,
362
+ pattern_type="workflow",
363
+ data={"pattern_key": "heavy_session_usage",
364
+ "value": f"{len(heavy_sessions)} intensive sessions",
365
+ "evidence": len(heavy_sessions),
366
+ "avg_facts": round(avg_facts_per_session, 1),
367
+ "total_sessions": len(session_counts)},
368
+ success_rate=0.8,
369
+ confidence=min(1.0, len(heavy_sessions) / 5),
370
+ )
371
+ generated += 1
372
+
373
+ # ── 6. Fact Type Distribution (v3.4.1 NEW) ────────────────
374
+ type_counts: Counter = Counter()
375
+ for f in facts:
376
+ ft = dict(f).get("fact_type", "semantic")
377
+ type_counts[ft] += 1
378
+
379
+ total_ft = sum(type_counts.values())
380
+ if total_ft > 0 and not dry_run:
381
+ dominant_type = type_counts.most_common(1)[0]
382
+ pct = round(dominant_type[1] / total_ft * 100)
383
+ store.record_pattern(
384
+ profile_id=profile_id,
385
+ pattern_type="style",
386
+ data={"pattern_key": "memory_style",
387
+ "value": f"{dominant_type[0]} dominant ({pct}%)",
388
+ "evidence": dominant_type[1],
389
+ "distribution": dict(type_counts)},
390
+ success_rate=pct / 100,
391
+ confidence=min(1.0, dominant_type[1] / 20),
392
+ )
393
+ generated += 1
394
+
395
+ # ── 7. Channel Performance (v3.4.1 NEW — from signals) ────
396
+ try:
397
+ learn_conn = sqlite3.connect(self._learning_db, timeout=10)
398
+ learn_conn.row_factory = sqlite3.Row
399
+
400
+ # Retrieval usage patterns from learning_feedback
401
+ channel_rows = learn_conn.execute(
402
+ "SELECT channel, COUNT(*) AS cnt, "
403
+ "AVG(signal_value) AS avg_signal "
404
+ "FROM learning_feedback "
405
+ "WHERE profile_id = ? "
406
+ "GROUP BY channel ORDER BY cnt DESC",
407
+ (profile_id,),
408
+ ).fetchall()
409
+
410
+ for row in channel_rows:
411
+ d = dict(row)
412
+ ch = d.get("channel", "unknown")
413
+ cnt = d.get("cnt", 0)
414
+ avg_sig = round(float(d.get("avg_signal", 0) or 0), 3)
415
+ if cnt >= 5 and not dry_run:
416
+ store.record_pattern(
417
+ profile_id=profile_id,
418
+ pattern_type="style",
419
+ data={"pattern_key": f"channel:{ch}",
420
+ "value": f"{ch} ({cnt} hits, {avg_sig} avg)",
421
+ "evidence": cnt,
422
+ "avg_signal": avg_sig},
423
+ success_rate=avg_sig,
424
+ confidence=min(1.0, cnt / 50),
425
+ )
426
+ generated += 1
427
+
428
+ # Co-retrieval cluster patterns
429
+ try:
430
+ coret_rows = learn_conn.execute(
431
+ "SELECT fact_a, fact_b, co_access_count "
432
+ "FROM co_retrieval_edges "
433
+ "WHERE profile_id = ? AND co_access_count >= 3 "
434
+ "ORDER BY co_access_count DESC LIMIT 20",
435
+ (profile_id,),
436
+ ).fetchall()
437
+ if coret_rows and not dry_run:
438
+ store.record_pattern(
439
+ profile_id=profile_id,
440
+ pattern_type="workflow",
441
+ data={"pattern_key": "co_retrieval_clusters",
442
+ "value": f"{len(coret_rows)} strong fact pairs",
443
+ "evidence": len(coret_rows),
444
+ "top_pair_count": dict(coret_rows[0]).get("co_access_count", 0) if coret_rows else 0},
445
+ success_rate=0.7,
446
+ confidence=min(1.0, len(coret_rows) / 10),
447
+ )
448
+ generated += 1
449
+ except Exception:
450
+ pass
451
+
452
+ learn_conn.close()
453
+ except Exception as exc:
454
+ logger.debug("Signal pattern mining failed: %s", exc)
455
+
456
+ # ── 8. Community Membership (v3.4.1 NEW — from graph) ─────
457
+ try:
458
+ comm_rows = conn.execute(
459
+ "SELECT community_id, COUNT(*) AS cnt "
460
+ "FROM fact_importance "
461
+ "WHERE profile_id = ? AND community_id IS NOT NULL "
462
+ "GROUP BY community_id ORDER BY cnt DESC",
463
+ (profile_id,),
464
+ ).fetchall()
465
+ if comm_rows and not dry_run:
466
+ total_comm = sum(dict(r)["cnt"] for r in comm_rows)
467
+ store.record_pattern(
468
+ profile_id=profile_id,
469
+ pattern_type="style",
470
+ data={"pattern_key": "knowledge_structure",
471
+ "value": f"{len(comm_rows)} topic communities, {total_comm} classified facts",
472
+ "evidence": total_comm,
473
+ "community_count": len(comm_rows)},
474
+ success_rate=0.8,
475
+ confidence=min(1.0, len(comm_rows) / 5),
251
476
  )
252
477
  generated += 1
478
+ except Exception:
479
+ pass
253
480
 
481
+ conn.close()
482
+
483
+ logger.info(
484
+ "Pattern mining: %d patterns generated for profile %s "
485
+ "from %d facts",
486
+ generated, profile_id, len(facts),
487
+ )
254
488
  return generated
255
489
  except Exception as exc:
256
- logger.debug("Pattern generation error: %s", exc)
490
+ logger.warning("Pattern generation error: %s", exc)
257
491
  return 0
258
492
 
259
493
  def _retrain_ranker(self, profile_id: str, signal_count: int) -> bool: