superlocalmemory 2.7.6 → 2.8.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.
Files changed (170) hide show
  1. package/CHANGELOG.md +120 -155
  2. package/README.md +115 -89
  3. package/api_server.py +2 -12
  4. package/docs/PATTERN-LEARNING.md +64 -199
  5. package/docs/example_graph_usage.py +4 -6
  6. package/install.sh +59 -0
  7. package/mcp_server.py +83 -7
  8. package/package.json +1 -8
  9. package/scripts/generate-thumbnails.py +3 -5
  10. package/skills/slm-build-graph/SKILL.md +1 -1
  11. package/skills/slm-list-recent/SKILL.md +1 -1
  12. package/skills/slm-recall/SKILL.md +1 -1
  13. package/skills/slm-remember/SKILL.md +1 -1
  14. package/skills/slm-show-patterns/SKILL.md +1 -1
  15. package/skills/slm-status/SKILL.md +1 -1
  16. package/skills/slm-switch-profile/SKILL.md +1 -1
  17. package/src/agent_registry.py +7 -18
  18. package/src/auth_middleware.py +3 -5
  19. package/src/auto_backup.py +3 -7
  20. package/src/behavioral/__init__.py +49 -0
  21. package/src/behavioral/behavioral_listener.py +203 -0
  22. package/src/behavioral/behavioral_patterns.py +275 -0
  23. package/src/behavioral/cross_project_transfer.py +206 -0
  24. package/src/behavioral/outcome_inference.py +194 -0
  25. package/src/behavioral/outcome_tracker.py +193 -0
  26. package/src/behavioral/tests/__init__.py +4 -0
  27. package/src/behavioral/tests/test_behavioral_integration.py +108 -0
  28. package/src/behavioral/tests/test_behavioral_patterns.py +150 -0
  29. package/src/behavioral/tests/test_cross_project_transfer.py +142 -0
  30. package/src/behavioral/tests/test_mcp_behavioral.py +139 -0
  31. package/src/behavioral/tests/test_mcp_report_outcome.py +117 -0
  32. package/src/behavioral/tests/test_outcome_inference.py +107 -0
  33. package/src/behavioral/tests/test_outcome_tracker.py +96 -0
  34. package/src/cache_manager.py +4 -6
  35. package/src/compliance/__init__.py +48 -0
  36. package/src/compliance/abac_engine.py +149 -0
  37. package/src/compliance/abac_middleware.py +116 -0
  38. package/src/compliance/audit_db.py +215 -0
  39. package/src/compliance/audit_logger.py +148 -0
  40. package/src/compliance/retention_manager.py +289 -0
  41. package/src/compliance/retention_scheduler.py +186 -0
  42. package/src/compliance/tests/__init__.py +4 -0
  43. package/src/compliance/tests/test_abac_enforcement.py +95 -0
  44. package/src/compliance/tests/test_abac_engine.py +124 -0
  45. package/src/compliance/tests/test_abac_mcp_integration.py +118 -0
  46. package/src/compliance/tests/test_audit_db.py +123 -0
  47. package/src/compliance/tests/test_audit_logger.py +98 -0
  48. package/src/compliance/tests/test_mcp_audit.py +128 -0
  49. package/src/compliance/tests/test_mcp_retention_policy.py +125 -0
  50. package/src/compliance/tests/test_retention_manager.py +131 -0
  51. package/src/compliance/tests/test_retention_scheduler.py +99 -0
  52. package/src/db_connection_manager.py +2 -12
  53. package/src/embedding_engine.py +61 -669
  54. package/src/embeddings/__init__.py +47 -0
  55. package/src/embeddings/cache.py +70 -0
  56. package/src/embeddings/cli.py +113 -0
  57. package/src/embeddings/constants.py +47 -0
  58. package/src/embeddings/database.py +91 -0
  59. package/src/embeddings/engine.py +247 -0
  60. package/src/embeddings/model_loader.py +145 -0
  61. package/src/event_bus.py +3 -13
  62. package/src/graph/__init__.py +36 -0
  63. package/src/graph/build_helpers.py +74 -0
  64. package/src/graph/cli.py +87 -0
  65. package/src/graph/cluster_builder.py +188 -0
  66. package/src/graph/cluster_summary.py +148 -0
  67. package/src/graph/constants.py +47 -0
  68. package/src/graph/edge_builder.py +162 -0
  69. package/src/graph/entity_extractor.py +95 -0
  70. package/src/graph/graph_core.py +226 -0
  71. package/src/graph/graph_search.py +231 -0
  72. package/src/graph/hierarchical.py +207 -0
  73. package/src/graph/schema.py +99 -0
  74. package/src/graph_engine.py +45 -1451
  75. package/src/hnsw_index.py +3 -7
  76. package/src/hybrid_search.py +36 -683
  77. package/src/learning/__init__.py +27 -12
  78. package/src/learning/adaptive_ranker.py +50 -12
  79. package/src/learning/cross_project_aggregator.py +2 -12
  80. package/src/learning/engagement_tracker.py +2 -12
  81. package/src/learning/feature_extractor.py +175 -43
  82. package/src/learning/feedback_collector.py +7 -12
  83. package/src/learning/learning_db.py +180 -12
  84. package/src/learning/project_context_manager.py +2 -12
  85. package/src/learning/source_quality_scorer.py +2 -12
  86. package/src/learning/synthetic_bootstrap.py +2 -12
  87. package/src/learning/tests/__init__.py +2 -0
  88. package/src/learning/tests/test_adaptive_ranker.py +2 -6
  89. package/src/learning/tests/test_adaptive_ranker_v28.py +60 -0
  90. package/src/learning/tests/test_aggregator.py +2 -6
  91. package/src/learning/tests/test_auto_retrain_v28.py +35 -0
  92. package/src/learning/tests/test_e2e_ranking_v28.py +82 -0
  93. package/src/learning/tests/test_feature_extractor_v28.py +93 -0
  94. package/src/learning/tests/test_feedback_collector.py +2 -6
  95. package/src/learning/tests/test_learning_db.py +2 -6
  96. package/src/learning/tests/test_learning_db_v28.py +110 -0
  97. package/src/learning/tests/test_learning_init_v28.py +48 -0
  98. package/src/learning/tests/test_outcome_signals.py +48 -0
  99. package/src/learning/tests/test_project_context.py +2 -6
  100. package/src/learning/tests/test_schema_migration.py +319 -0
  101. package/src/learning/tests/test_signal_inference.py +11 -13
  102. package/src/learning/tests/test_source_quality.py +2 -6
  103. package/src/learning/tests/test_synthetic_bootstrap.py +3 -7
  104. package/src/learning/tests/test_workflow_miner.py +2 -6
  105. package/src/learning/workflow_pattern_miner.py +2 -12
  106. package/src/lifecycle/__init__.py +54 -0
  107. package/src/lifecycle/bounded_growth.py +239 -0
  108. package/src/lifecycle/compaction_engine.py +226 -0
  109. package/src/lifecycle/lifecycle_engine.py +302 -0
  110. package/src/lifecycle/lifecycle_evaluator.py +225 -0
  111. package/src/lifecycle/lifecycle_scheduler.py +130 -0
  112. package/src/lifecycle/retention_policy.py +285 -0
  113. package/src/lifecycle/tests/__init__.py +4 -0
  114. package/src/lifecycle/tests/test_bounded_growth.py +193 -0
  115. package/src/lifecycle/tests/test_compaction.py +179 -0
  116. package/src/lifecycle/tests/test_lifecycle_engine.py +137 -0
  117. package/src/lifecycle/tests/test_lifecycle_evaluation.py +177 -0
  118. package/src/lifecycle/tests/test_lifecycle_scheduler.py +127 -0
  119. package/src/lifecycle/tests/test_lifecycle_search.py +109 -0
  120. package/src/lifecycle/tests/test_mcp_compact.py +149 -0
  121. package/src/lifecycle/tests/test_mcp_lifecycle_status.py +114 -0
  122. package/src/lifecycle/tests/test_retention_policy.py +162 -0
  123. package/src/mcp_tools_v28.py +280 -0
  124. package/src/memory-profiles.py +2 -12
  125. package/src/memory-reset.py +2 -12
  126. package/src/memory_compression.py +2 -12
  127. package/src/memory_store_v2.py +76 -20
  128. package/src/migrate_v1_to_v2.py +2 -12
  129. package/src/pattern_learner.py +29 -975
  130. package/src/patterns/__init__.py +24 -0
  131. package/src/patterns/analyzers.py +247 -0
  132. package/src/patterns/learner.py +267 -0
  133. package/src/patterns/scoring.py +167 -0
  134. package/src/patterns/store.py +223 -0
  135. package/src/patterns/terminology.py +138 -0
  136. package/src/provenance_tracker.py +4 -14
  137. package/src/query_optimizer.py +4 -6
  138. package/src/rate_limiter.py +2 -6
  139. package/src/search/__init__.py +20 -0
  140. package/src/search/cli.py +77 -0
  141. package/src/search/constants.py +26 -0
  142. package/src/search/engine.py +239 -0
  143. package/src/search/fusion.py +122 -0
  144. package/src/search/index_loader.py +112 -0
  145. package/src/search/methods.py +162 -0
  146. package/src/search_engine_v2.py +4 -6
  147. package/src/setup_validator.py +7 -13
  148. package/src/subscription_manager.py +2 -12
  149. package/src/tree/__init__.py +59 -0
  150. package/src/tree/builder.py +183 -0
  151. package/src/tree/nodes.py +196 -0
  152. package/src/tree/queries.py +252 -0
  153. package/src/tree/schema.py +76 -0
  154. package/src/tree_manager.py +10 -711
  155. package/src/trust/__init__.py +45 -0
  156. package/src/trust/constants.py +66 -0
  157. package/src/trust/queries.py +157 -0
  158. package/src/trust/schema.py +95 -0
  159. package/src/trust/scorer.py +299 -0
  160. package/src/trust/signals.py +95 -0
  161. package/src/trust_scorer.py +39 -697
  162. package/src/webhook_dispatcher.py +2 -12
  163. package/ui/app.js +1 -1
  164. package/ui/js/agents.js +1 -1
  165. package/ui_server.py +2 -14
  166. package/ATTRIBUTION.md +0 -140
  167. package/docs/ARCHITECTURE-V2.5.md +0 -190
  168. package/docs/GRAPH-ENGINE.md +0 -503
  169. package/docs/architecture-diagram.drawio +0 -405
  170. package/docs/plans/2026-02-13-benchmark-suite.md +0 -1349
@@ -0,0 +1,47 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
3
+ """embeddings package - Local Embedding Generation for SuperLocalMemory V2
4
+
5
+ Re-exports all public classes and constants so that
6
+ ``from embeddings import EmbeddingEngine`` works.
7
+ """
8
+ from embeddings.constants import (
9
+ MAX_BATCH_SIZE,
10
+ MAX_TEXT_LENGTH,
11
+ CACHE_MAX_SIZE,
12
+ SENTENCE_TRANSFORMERS_AVAILABLE,
13
+ SKLEARN_AVAILABLE,
14
+ TORCH_AVAILABLE,
15
+ CUDA_AVAILABLE,
16
+ MPS_AVAILABLE,
17
+ MEMORY_DIR,
18
+ EMBEDDING_CACHE_PATH,
19
+ MODEL_CACHE_PATH,
20
+ )
21
+ from embeddings.cache import LRUCache
22
+ from embeddings.model_loader import ModelLoaderMixin
23
+ from embeddings.engine import EmbeddingEngine
24
+ from embeddings.database import add_embeddings_to_database
25
+ from embeddings.cli import main
26
+
27
+ __all__ = [
28
+ # Constants
29
+ "MAX_BATCH_SIZE",
30
+ "MAX_TEXT_LENGTH",
31
+ "CACHE_MAX_SIZE",
32
+ "SENTENCE_TRANSFORMERS_AVAILABLE",
33
+ "SKLEARN_AVAILABLE",
34
+ "TORCH_AVAILABLE",
35
+ "CUDA_AVAILABLE",
36
+ "MPS_AVAILABLE",
37
+ "MEMORY_DIR",
38
+ "EMBEDDING_CACHE_PATH",
39
+ "MODEL_CACHE_PATH",
40
+ # Classes
41
+ "LRUCache",
42
+ "ModelLoaderMixin",
43
+ "EmbeddingEngine",
44
+ # Functions
45
+ "add_embeddings_to_database",
46
+ "main",
47
+ ]
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: MIT
3
+ # Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
4
+ """LRU cache for embedding vectors.
5
+ """
6
+ import json
7
+ import logging
8
+ from pathlib import Path
9
+ from typing import Optional
10
+ from collections import OrderedDict
11
+
12
+ import numpy as np
13
+
14
+ from embeddings.constants import CACHE_MAX_SIZE
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class LRUCache:
20
+ """Simple LRU cache for embeddings."""
21
+
22
+ def __init__(self, max_size: int = CACHE_MAX_SIZE):
23
+ self.cache = OrderedDict()
24
+ self.max_size = max_size
25
+
26
+ def get(self, key: str) -> Optional[np.ndarray]:
27
+ """Get item from cache, moving to end (most recent)."""
28
+ if key not in self.cache:
29
+ return None
30
+
31
+ # Move to end (most recently used)
32
+ self.cache.move_to_end(key)
33
+ return np.array(self.cache[key])
34
+
35
+ def set(self, key: str, value: np.ndarray):
36
+ """Set item in cache, evicting oldest if full."""
37
+ if key in self.cache:
38
+ # Update existing
39
+ self.cache.move_to_end(key)
40
+ self.cache[key] = value.tolist()
41
+ else:
42
+ # Add new
43
+ if len(self.cache) >= self.max_size:
44
+ # Evict oldest
45
+ self.cache.popitem(last=False)
46
+ self.cache[key] = value.tolist()
47
+
48
+ def save(self, path: Path):
49
+ """Save cache to disk."""
50
+ try:
51
+ path.parent.mkdir(parents=True, exist_ok=True)
52
+ with open(path, 'w') as f:
53
+ json.dump(dict(self.cache), f)
54
+ logger.debug(f"Saved {len(self.cache)} cached embeddings")
55
+ except Exception as e:
56
+ logger.error(f"Failed to save embedding cache: {e}")
57
+
58
+ def load(self, path: Path):
59
+ """Load cache from disk."""
60
+ if not path.exists():
61
+ return
62
+
63
+ try:
64
+ with open(path, 'r') as f:
65
+ data = json.load(f)
66
+ self.cache = OrderedDict(data)
67
+ logger.info(f"Loaded {len(self.cache)} cached embeddings")
68
+ except Exception as e:
69
+ logger.error(f"Failed to load embedding cache: {e}")
70
+ self.cache = OrderedDict()
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: MIT
3
+ # Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
4
+ """CLI interface for testing the embedding engine.
5
+ """
6
+ import sys
7
+ import json
8
+ import time
9
+ import logging
10
+
11
+ from embeddings.constants import MEMORY_DIR
12
+ from embeddings.engine import EmbeddingEngine
13
+ from embeddings.database import add_embeddings_to_database
14
+
15
+
16
+ def main():
17
+ """Run the embedding engine CLI."""
18
+ # Configure logging
19
+ logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
20
+
21
+ if len(sys.argv) < 2:
22
+ print("EmbeddingEngine CLI - Local Embedding Generation")
23
+ print("\nCommands:")
24
+ print(" python embedding_engine.py stats # Show engine statistics")
25
+ print(" python embedding_engine.py generate # Generate embeddings for database")
26
+ print(" python embedding_engine.py test # Run performance test")
27
+ print(" python embedding_engine.py clear-cache # Clear embedding cache")
28
+ sys.exit(0)
29
+
30
+ command = sys.argv[1]
31
+
32
+ if command == "stats":
33
+ engine = EmbeddingEngine()
34
+ stats = engine.get_stats()
35
+ print(json.dumps(stats, indent=2))
36
+
37
+ elif command == "generate":
38
+ db_path = MEMORY_DIR / "memory.db"
39
+ if not db_path.exists():
40
+ print(f"Database not found at {db_path}")
41
+ sys.exit(1)
42
+
43
+ print("Generating embeddings for all memories...")
44
+ engine = EmbeddingEngine()
45
+ add_embeddings_to_database(engine, db_path)
46
+ print("Generation complete!")
47
+ print(json.dumps(engine.get_stats(), indent=2))
48
+
49
+ elif command == "clear-cache":
50
+ engine = EmbeddingEngine()
51
+ engine.clear_cache()
52
+ engine.save_cache()
53
+ print("Cache cleared!")
54
+
55
+ elif command == "test":
56
+ print("Running embedding performance test...")
57
+
58
+ engine = EmbeddingEngine()
59
+
60
+ # Test single encoding
61
+ print("\nTest 1: Single text encoding")
62
+ text = "This is a test sentence for embedding generation."
63
+ start = time.time()
64
+ embedding = engine.encode(text)
65
+ elapsed = time.time() - start
66
+ print(f" Time: {elapsed*1000:.2f}ms")
67
+ print(f" Dimension: {len(embedding)}")
68
+ print(f" Sample values: {embedding[:5]}")
69
+
70
+ # Test batch encoding
71
+ print("\nTest 2: Batch encoding (100 texts)")
72
+ texts = [f"This is test sentence number {i} with some content." for i in range(100)]
73
+ start = time.time()
74
+ embeddings = engine.encode(texts, batch_size=32)
75
+ elapsed = time.time() - start
76
+ print(f" Time: {elapsed*1000:.2f}ms ({100/elapsed:.0f} texts/sec)")
77
+ print(f" Shape: {embeddings.shape}")
78
+
79
+ # Test cache
80
+ print("\nTest 3: Cache performance")
81
+ start = time.time()
82
+ embedding_cached = engine.encode(text)
83
+ elapsed = time.time() - start
84
+ print(f" Cache hit time: {elapsed*1000:.4f}ms")
85
+ print(f" Speedup: {(elapsed*1000):.0f}x faster")
86
+
87
+ # Test similarity
88
+ print("\nTest 4: Similarity computation")
89
+ text1 = "The weather is nice today."
90
+ text2 = "It's a beautiful day outside."
91
+ text3 = "Python is a programming language."
92
+
93
+ emb1 = engine.encode(text1)
94
+ emb2 = engine.encode(text2)
95
+ emb3 = engine.encode(text3)
96
+
97
+ sim_12 = engine.similarity(emb1, emb2)
98
+ sim_13 = engine.similarity(emb1, emb3)
99
+
100
+ print(f" Similarity (weather vs beautiful day): {sim_12:.3f}")
101
+ print(f" Similarity (weather vs programming): {sim_13:.3f}")
102
+
103
+ # Print stats
104
+ print("\nEngine statistics:")
105
+ print(json.dumps(engine.get_stats(), indent=2))
106
+
107
+ # Save cache
108
+ engine.save_cache()
109
+ print("\nCache saved!")
110
+
111
+ else:
112
+ print(f"Unknown command: {command}")
113
+ print("Run without arguments to see available commands.")
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: MIT
3
+ # Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
4
+ """Shared constants and feature-detection for the embeddings package.
5
+ """
6
+ import logging
7
+ from pathlib import Path
8
+
9
+ # SECURITY: Embedding generation limits to prevent resource exhaustion
10
+ MAX_BATCH_SIZE = 128
11
+ MAX_TEXT_LENGTH = 10_000
12
+ CACHE_MAX_SIZE = 10_000
13
+
14
+ # Optional sentence-transformers dependency
15
+ SENTENCE_TRANSFORMERS_AVAILABLE = False
16
+ try:
17
+ from sentence_transformers import SentenceTransformer # noqa: F401
18
+ SENTENCE_TRANSFORMERS_AVAILABLE = True
19
+ except ImportError:
20
+ SENTENCE_TRANSFORMERS_AVAILABLE = False
21
+
22
+ # Fallback: TF-IDF vectorization
23
+ SKLEARN_AVAILABLE = False
24
+ try:
25
+ from sklearn.feature_extraction.text import TfidfVectorizer # noqa: F401
26
+ SKLEARN_AVAILABLE = True
27
+ except ImportError:
28
+ SKLEARN_AVAILABLE = False
29
+
30
+ # GPU detection
31
+ TORCH_AVAILABLE = False
32
+ CUDA_AVAILABLE = False
33
+ MPS_AVAILABLE = False # Apple Silicon
34
+
35
+ try:
36
+ import torch
37
+ TORCH_AVAILABLE = True
38
+ CUDA_AVAILABLE = torch.cuda.is_available()
39
+ MPS_AVAILABLE = hasattr(torch.backends, 'mps') and torch.backends.mps.is_available()
40
+ except ImportError:
41
+ pass
42
+
43
+ MEMORY_DIR = Path.home() / ".claude-memory"
44
+ EMBEDDING_CACHE_PATH = MEMORY_DIR / "embedding_cache.json"
45
+ MODEL_CACHE_PATH = MEMORY_DIR / "models" # Local model storage
46
+
47
+ logger = logging.getLogger(__name__)
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: MIT
3
+ # Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
4
+ """Database integration for batch embedding generation.
5
+ """
6
+ import json
7
+ import sqlite3
8
+ import logging
9
+ from pathlib import Path
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def add_embeddings_to_database(
15
+ engine,
16
+ db_path: Path,
17
+ embedding_column: str = 'embedding',
18
+ batch_size: int = 32
19
+ ):
20
+ """
21
+ Generate embeddings for all memories in database.
22
+
23
+ Args:
24
+ engine: An EmbeddingEngine instance
25
+ db_path: Path to SQLite database
26
+ embedding_column: Column name to store embeddings
27
+ batch_size: Batch size for processing
28
+ """
29
+ conn = sqlite3.connect(db_path)
30
+ cursor = conn.cursor()
31
+
32
+ try:
33
+ # Check if embedding column exists
34
+ cursor.execute("PRAGMA table_info(memories)")
35
+ columns = {row[1] for row in cursor.fetchall()}
36
+
37
+ if embedding_column not in columns:
38
+ # Add column
39
+ logger.info(f"Adding '{embedding_column}' column to database")
40
+ cursor.execute(f'ALTER TABLE memories ADD COLUMN {embedding_column} TEXT')
41
+ conn.commit()
42
+
43
+ # Get memories without embeddings
44
+ cursor.execute(f'''
45
+ SELECT id, content, summary
46
+ FROM memories
47
+ WHERE {embedding_column} IS NULL OR {embedding_column} = ''
48
+ ''')
49
+ rows = cursor.fetchall()
50
+
51
+ if not rows:
52
+ logger.info("All memories already have embeddings")
53
+ conn.close()
54
+ return
55
+
56
+ logger.info(f"Generating embeddings for {len(rows)} memories...")
57
+
58
+ # Process in batches
59
+ for i in range(0, len(rows), batch_size):
60
+ batch = rows[i:i+batch_size]
61
+ memory_ids = [row[0] for row in batch]
62
+
63
+ # Combine content and summary
64
+ texts = []
65
+ for row in batch:
66
+ content = row[1] or ""
67
+ summary = row[2] or ""
68
+ text = f"{content} {summary}".strip()
69
+ texts.append(text)
70
+
71
+ # Generate embeddings
72
+ embeddings = engine.encode(texts, batch_size=batch_size)
73
+
74
+ # Store in database
75
+ for mem_id, embedding in zip(memory_ids, embeddings):
76
+ embedding_json = json.dumps(embedding.tolist())
77
+ cursor.execute(
78
+ f'UPDATE memories SET {embedding_column} = ? WHERE id = ?',
79
+ (embedding_json, mem_id)
80
+ )
81
+
82
+ conn.commit()
83
+ logger.info(f"Processed {min(i+batch_size, len(rows))}/{len(rows)} memories")
84
+
85
+ # Save cache
86
+ engine.save_cache()
87
+
88
+ logger.info(f"Successfully generated embeddings for {len(rows)} memories")
89
+
90
+ finally:
91
+ conn.close()
@@ -0,0 +1,247 @@
1
+ #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: MIT
3
+ # Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
4
+ """EmbeddingEngine - Core encoding logic for local embedding generation.
5
+ """
6
+ import hashlib
7
+ import logging
8
+ from pathlib import Path
9
+ from typing import Any, Dict, List, Optional, Union
10
+
11
+ import numpy as np
12
+
13
+ from embeddings.constants import (
14
+ MAX_BATCH_SIZE,
15
+ MAX_TEXT_LENGTH,
16
+ CACHE_MAX_SIZE,
17
+ SENTENCE_TRANSFORMERS_AVAILABLE,
18
+ SKLEARN_AVAILABLE,
19
+ TORCH_AVAILABLE,
20
+ CUDA_AVAILABLE,
21
+ MPS_AVAILABLE,
22
+ EMBEDDING_CACHE_PATH,
23
+ MODEL_CACHE_PATH,
24
+ )
25
+ from embeddings.cache import LRUCache
26
+ from embeddings.model_loader import ModelLoaderMixin
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ class EmbeddingEngine(ModelLoaderMixin):
32
+ """
33
+ Local embedding generation using sentence-transformers.
34
+
35
+ Features:
36
+ - all-MiniLM-L6-v2 model (384 dimensions, 80MB, fast)
37
+ - Batch processing for efficiency (up to 128 texts)
38
+ - GPU acceleration (CUDA/MPS) with automatic detection
39
+ - LRU cache for repeated queries (10K entries)
40
+ - Graceful fallback to TF-IDF if dependencies unavailable
41
+
42
+ Performance:
43
+ - CPU: ~100 embeddings/sec
44
+ - GPU (CUDA): ~1000 embeddings/sec
45
+ - Apple Silicon (MPS): ~500 embeddings/sec
46
+ - Cache hit: ~0.001ms
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ model_name: str = "all-MiniLM-L6-v2",
52
+ device: Optional[str] = None,
53
+ cache_path: Optional[Path] = None,
54
+ model_cache_path: Optional[Path] = None,
55
+ use_cache: bool = True
56
+ ):
57
+ """
58
+ Initialize embedding engine.
59
+
60
+ Args:
61
+ model_name: Sentence transformer model name
62
+ device: Device to use ('cuda', 'mps', 'cpu', or None for auto)
63
+ cache_path: Custom path for embedding cache
64
+ model_cache_path: Custom path for model storage
65
+ use_cache: Whether to use LRU cache
66
+ """
67
+ self.model_name = model_name
68
+ self.cache_path = cache_path or EMBEDDING_CACHE_PATH
69
+ self.model_cache_path = model_cache_path or MODEL_CACHE_PATH
70
+ self.use_cache = use_cache
71
+
72
+ # Auto-detect device
73
+ if device is None:
74
+ if CUDA_AVAILABLE:
75
+ device = 'cuda'
76
+ logger.info("Using CUDA GPU acceleration")
77
+ elif MPS_AVAILABLE:
78
+ device = 'mps'
79
+ logger.info("Using Apple Silicon (MPS) GPU acceleration")
80
+ else:
81
+ device = 'cpu'
82
+ logger.info("Using CPU (consider GPU for faster processing)")
83
+ self.device = device
84
+
85
+ # Initialize model
86
+ self.model = None
87
+ self.dimension = 384 # Default for all-MiniLM-L6-v2
88
+ self.use_transformers = SENTENCE_TRANSFORMERS_AVAILABLE
89
+
90
+ # Initialize cache
91
+ self.cache = LRUCache(max_size=CACHE_MAX_SIZE) if use_cache else None
92
+ if self.cache:
93
+ self.cache.load(self.cache_path)
94
+
95
+ # Fallback: TF-IDF vectorizer
96
+ self.tfidf_vectorizer = None
97
+ self.tfidf_fitted = False
98
+
99
+ # Load model (from ModelLoaderMixin)
100
+ self._load_model()
101
+
102
+ def _get_cache_key(self, text: str) -> str:
103
+ """Generate cache key for text."""
104
+ return hashlib.sha256(text.encode('utf-8')).hexdigest()[:32]
105
+
106
+ def encode(
107
+ self,
108
+ texts: Union[str, List[str]],
109
+ batch_size: int = 32,
110
+ show_progress: bool = False,
111
+ normalize: bool = True
112
+ ) -> np.ndarray:
113
+ """
114
+ Generate embeddings for text(s).
115
+
116
+ Args:
117
+ texts: Single text or list of texts
118
+ batch_size: Batch size for processing (max: 128)
119
+ show_progress: Show progress bar for large batches
120
+ normalize: Normalize embeddings to unit length
121
+
122
+ Returns:
123
+ Array of shape (n_texts, dimension) or (dimension,) for single text
124
+ """
125
+ single_input = isinstance(texts, str)
126
+ if single_input:
127
+ texts = [texts]
128
+
129
+ if len(texts) == 0:
130
+ return np.array([])
131
+
132
+ batch_size = min(batch_size, MAX_BATCH_SIZE)
133
+
134
+ # Validate text length
135
+ for i, text in enumerate(texts):
136
+ if not isinstance(text, str):
137
+ raise ValueError(f"Text at index {i} is not a string")
138
+ if len(text) > MAX_TEXT_LENGTH:
139
+ logger.warning(f"Text {i} truncated from {len(text)} to {MAX_TEXT_LENGTH} chars")
140
+ texts[i] = text[:MAX_TEXT_LENGTH]
141
+
142
+ # Check cache for hits
143
+ embeddings = []
144
+ uncached_texts = []
145
+ uncached_indices = []
146
+
147
+ if self.cache:
148
+ for i, text in enumerate(texts):
149
+ cache_key = self._get_cache_key(text)
150
+ cached = self.cache.get(cache_key)
151
+ if cached is not None:
152
+ embeddings.append((i, cached))
153
+ else:
154
+ uncached_texts.append(text)
155
+ uncached_indices.append(i)
156
+ else:
157
+ uncached_texts = texts
158
+ uncached_indices = list(range(len(texts)))
159
+
160
+ # Generate embeddings for uncached texts
161
+ if uncached_texts:
162
+ if self.use_transformers and self.model:
163
+ uncached_embeddings = self._encode_transformer(
164
+ uncached_texts, batch_size=batch_size, show_progress=show_progress
165
+ )
166
+ elif self.tfidf_vectorizer:
167
+ uncached_embeddings = self._encode_tfidf(uncached_texts)
168
+ else:
169
+ raise RuntimeError("No embedding method available")
170
+
171
+ for i, text, embedding in zip(uncached_indices, uncached_texts, uncached_embeddings):
172
+ if self.cache:
173
+ cache_key = self._get_cache_key(text)
174
+ self.cache.set(cache_key, embedding)
175
+ embeddings.append((i, embedding))
176
+
177
+ # Sort by original index and extract embeddings
178
+ embeddings.sort(key=lambda x: x[0])
179
+ result = np.array([emb for _, emb in embeddings])
180
+
181
+ if normalize and len(result) > 0:
182
+ norms = np.linalg.norm(result, axis=1, keepdims=True)
183
+ norms[norms == 0] = 1
184
+ result = result / norms
185
+
186
+ if single_input:
187
+ return result[0]
188
+ return result
189
+
190
+ def encode_batch(
191
+ self,
192
+ texts: List[str],
193
+ batch_size: int = 32,
194
+ show_progress: bool = True
195
+ ) -> np.ndarray:
196
+ """Convenience method for batch encoding with progress."""
197
+ return self.encode(texts, batch_size=batch_size, show_progress=show_progress)
198
+
199
+ def similarity(self, embedding1: np.ndarray, embedding2: np.ndarray) -> float:
200
+ """Compute cosine similarity between two embeddings. Returns [0, 1]."""
201
+ emb1 = embedding1 / (np.linalg.norm(embedding1) + 1e-8)
202
+ emb2 = embedding2 / (np.linalg.norm(embedding2) + 1e-8)
203
+ similarity = np.dot(emb1, emb2)
204
+ return float(max(0.0, min(1.0, similarity)))
205
+
206
+ def save_cache(self):
207
+ """Save embedding cache to disk."""
208
+ if self.cache:
209
+ self.cache.save(self.cache_path)
210
+
211
+ def clear_cache(self):
212
+ """Clear embedding cache."""
213
+ if self.cache:
214
+ self.cache.cache.clear()
215
+ logger.info("Cleared embedding cache")
216
+
217
+ def get_stats(self) -> Dict[str, Any]:
218
+ """Get embedding engine statistics."""
219
+ return {
220
+ 'sentence_transformers_available': SENTENCE_TRANSFORMERS_AVAILABLE,
221
+ 'use_transformers': self.use_transformers,
222
+ 'sklearn_available': SKLEARN_AVAILABLE,
223
+ 'torch_available': TORCH_AVAILABLE,
224
+ 'cuda_available': CUDA_AVAILABLE,
225
+ 'mps_available': MPS_AVAILABLE,
226
+ 'device': self.device,
227
+ 'model_name': self.model_name,
228
+ 'dimension': self.dimension,
229
+ 'cache_enabled': self.cache is not None,
230
+ 'cache_size': len(self.cache.cache) if self.cache else 0,
231
+ 'cache_max_size': CACHE_MAX_SIZE,
232
+ 'model_loaded': self.model is not None or self.tfidf_vectorizer is not None
233
+ }
234
+
235
+ def add_to_database(
236
+ self,
237
+ db_path,
238
+ embedding_column: str = 'embedding',
239
+ batch_size: int = 32
240
+ ):
241
+ """
242
+ Generate embeddings for all memories in database.
243
+
244
+ Delegates to :func:`embeddings.database.add_embeddings_to_database`.
245
+ """
246
+ from embeddings.database import add_embeddings_to_database
247
+ add_embeddings_to_database(self, db_path, embedding_column, batch_size)