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.
- package/CHANGELOG.md +120 -155
- package/README.md +115 -89
- package/api_server.py +2 -12
- package/docs/PATTERN-LEARNING.md +64 -199
- package/docs/example_graph_usage.py +4 -6
- package/install.sh +59 -0
- package/mcp_server.py +83 -7
- package/package.json +1 -8
- package/scripts/generate-thumbnails.py +3 -5
- package/skills/slm-build-graph/SKILL.md +1 -1
- package/skills/slm-list-recent/SKILL.md +1 -1
- package/skills/slm-recall/SKILL.md +1 -1
- package/skills/slm-remember/SKILL.md +1 -1
- package/skills/slm-show-patterns/SKILL.md +1 -1
- package/skills/slm-status/SKILL.md +1 -1
- package/skills/slm-switch-profile/SKILL.md +1 -1
- package/src/agent_registry.py +7 -18
- package/src/auth_middleware.py +3 -5
- package/src/auto_backup.py +3 -7
- package/src/behavioral/__init__.py +49 -0
- package/src/behavioral/behavioral_listener.py +203 -0
- package/src/behavioral/behavioral_patterns.py +275 -0
- package/src/behavioral/cross_project_transfer.py +206 -0
- package/src/behavioral/outcome_inference.py +194 -0
- package/src/behavioral/outcome_tracker.py +193 -0
- package/src/behavioral/tests/__init__.py +4 -0
- package/src/behavioral/tests/test_behavioral_integration.py +108 -0
- package/src/behavioral/tests/test_behavioral_patterns.py +150 -0
- package/src/behavioral/tests/test_cross_project_transfer.py +142 -0
- package/src/behavioral/tests/test_mcp_behavioral.py +139 -0
- package/src/behavioral/tests/test_mcp_report_outcome.py +117 -0
- package/src/behavioral/tests/test_outcome_inference.py +107 -0
- package/src/behavioral/tests/test_outcome_tracker.py +96 -0
- package/src/cache_manager.py +4 -6
- package/src/compliance/__init__.py +48 -0
- package/src/compliance/abac_engine.py +149 -0
- package/src/compliance/abac_middleware.py +116 -0
- package/src/compliance/audit_db.py +215 -0
- package/src/compliance/audit_logger.py +148 -0
- package/src/compliance/retention_manager.py +289 -0
- package/src/compliance/retention_scheduler.py +186 -0
- package/src/compliance/tests/__init__.py +4 -0
- package/src/compliance/tests/test_abac_enforcement.py +95 -0
- package/src/compliance/tests/test_abac_engine.py +124 -0
- package/src/compliance/tests/test_abac_mcp_integration.py +118 -0
- package/src/compliance/tests/test_audit_db.py +123 -0
- package/src/compliance/tests/test_audit_logger.py +98 -0
- package/src/compliance/tests/test_mcp_audit.py +128 -0
- package/src/compliance/tests/test_mcp_retention_policy.py +125 -0
- package/src/compliance/tests/test_retention_manager.py +131 -0
- package/src/compliance/tests/test_retention_scheduler.py +99 -0
- package/src/db_connection_manager.py +2 -12
- package/src/embedding_engine.py +61 -669
- package/src/embeddings/__init__.py +47 -0
- package/src/embeddings/cache.py +70 -0
- package/src/embeddings/cli.py +113 -0
- package/src/embeddings/constants.py +47 -0
- package/src/embeddings/database.py +91 -0
- package/src/embeddings/engine.py +247 -0
- package/src/embeddings/model_loader.py +145 -0
- package/src/event_bus.py +3 -13
- package/src/graph/__init__.py +36 -0
- package/src/graph/build_helpers.py +74 -0
- package/src/graph/cli.py +87 -0
- package/src/graph/cluster_builder.py +188 -0
- package/src/graph/cluster_summary.py +148 -0
- package/src/graph/constants.py +47 -0
- package/src/graph/edge_builder.py +162 -0
- package/src/graph/entity_extractor.py +95 -0
- package/src/graph/graph_core.py +226 -0
- package/src/graph/graph_search.py +231 -0
- package/src/graph/hierarchical.py +207 -0
- package/src/graph/schema.py +99 -0
- package/src/graph_engine.py +45 -1451
- package/src/hnsw_index.py +3 -7
- package/src/hybrid_search.py +36 -683
- package/src/learning/__init__.py +27 -12
- package/src/learning/adaptive_ranker.py +50 -12
- package/src/learning/cross_project_aggregator.py +2 -12
- package/src/learning/engagement_tracker.py +2 -12
- package/src/learning/feature_extractor.py +175 -43
- package/src/learning/feedback_collector.py +7 -12
- package/src/learning/learning_db.py +180 -12
- package/src/learning/project_context_manager.py +2 -12
- package/src/learning/source_quality_scorer.py +2 -12
- package/src/learning/synthetic_bootstrap.py +2 -12
- package/src/learning/tests/__init__.py +2 -0
- package/src/learning/tests/test_adaptive_ranker.py +2 -6
- package/src/learning/tests/test_adaptive_ranker_v28.py +60 -0
- package/src/learning/tests/test_aggregator.py +2 -6
- package/src/learning/tests/test_auto_retrain_v28.py +35 -0
- package/src/learning/tests/test_e2e_ranking_v28.py +82 -0
- package/src/learning/tests/test_feature_extractor_v28.py +93 -0
- package/src/learning/tests/test_feedback_collector.py +2 -6
- package/src/learning/tests/test_learning_db.py +2 -6
- package/src/learning/tests/test_learning_db_v28.py +110 -0
- package/src/learning/tests/test_learning_init_v28.py +48 -0
- package/src/learning/tests/test_outcome_signals.py +48 -0
- package/src/learning/tests/test_project_context.py +2 -6
- package/src/learning/tests/test_schema_migration.py +319 -0
- package/src/learning/tests/test_signal_inference.py +11 -13
- package/src/learning/tests/test_source_quality.py +2 -6
- package/src/learning/tests/test_synthetic_bootstrap.py +3 -7
- package/src/learning/tests/test_workflow_miner.py +2 -6
- package/src/learning/workflow_pattern_miner.py +2 -12
- package/src/lifecycle/__init__.py +54 -0
- package/src/lifecycle/bounded_growth.py +239 -0
- package/src/lifecycle/compaction_engine.py +226 -0
- package/src/lifecycle/lifecycle_engine.py +302 -0
- package/src/lifecycle/lifecycle_evaluator.py +225 -0
- package/src/lifecycle/lifecycle_scheduler.py +130 -0
- package/src/lifecycle/retention_policy.py +285 -0
- package/src/lifecycle/tests/__init__.py +4 -0
- package/src/lifecycle/tests/test_bounded_growth.py +193 -0
- package/src/lifecycle/tests/test_compaction.py +179 -0
- package/src/lifecycle/tests/test_lifecycle_engine.py +137 -0
- package/src/lifecycle/tests/test_lifecycle_evaluation.py +177 -0
- package/src/lifecycle/tests/test_lifecycle_scheduler.py +127 -0
- package/src/lifecycle/tests/test_lifecycle_search.py +109 -0
- package/src/lifecycle/tests/test_mcp_compact.py +149 -0
- package/src/lifecycle/tests/test_mcp_lifecycle_status.py +114 -0
- package/src/lifecycle/tests/test_retention_policy.py +162 -0
- package/src/mcp_tools_v28.py +280 -0
- package/src/memory-profiles.py +2 -12
- package/src/memory-reset.py +2 -12
- package/src/memory_compression.py +2 -12
- package/src/memory_store_v2.py +76 -20
- package/src/migrate_v1_to_v2.py +2 -12
- package/src/pattern_learner.py +29 -975
- package/src/patterns/__init__.py +24 -0
- package/src/patterns/analyzers.py +247 -0
- package/src/patterns/learner.py +267 -0
- package/src/patterns/scoring.py +167 -0
- package/src/patterns/store.py +223 -0
- package/src/patterns/terminology.py +138 -0
- package/src/provenance_tracker.py +4 -14
- package/src/query_optimizer.py +4 -6
- package/src/rate_limiter.py +2 -6
- package/src/search/__init__.py +20 -0
- package/src/search/cli.py +77 -0
- package/src/search/constants.py +26 -0
- package/src/search/engine.py +239 -0
- package/src/search/fusion.py +122 -0
- package/src/search/index_loader.py +112 -0
- package/src/search/methods.py +162 -0
- package/src/search_engine_v2.py +4 -6
- package/src/setup_validator.py +7 -13
- package/src/subscription_manager.py +2 -12
- package/src/tree/__init__.py +59 -0
- package/src/tree/builder.py +183 -0
- package/src/tree/nodes.py +196 -0
- package/src/tree/queries.py +252 -0
- package/src/tree/schema.py +76 -0
- package/src/tree_manager.py +10 -711
- package/src/trust/__init__.py +45 -0
- package/src/trust/constants.py +66 -0
- package/src/trust/queries.py +157 -0
- package/src/trust/schema.py +95 -0
- package/src/trust/scorer.py +299 -0
- package/src/trust/signals.py +95 -0
- package/src/trust_scorer.py +39 -697
- package/src/webhook_dispatcher.py +2 -12
- package/ui/app.js +1 -1
- package/ui/js/agents.js +1 -1
- package/ui_server.py +2 -14
- package/ATTRIBUTION.md +0 -140
- package/docs/ARCHITECTURE-V2.5.md +0 -190
- package/docs/GRAPH-ENGINE.md +0 -503
- package/docs/architecture-diagram.drawio +0 -405
- package/docs/plans/2026-02-13-benchmark-suite.md +0 -1349
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
|
|
4
|
+
"""Individual search methods (BM25, semantic, graph) for hybrid search.
|
|
5
|
+
"""
|
|
6
|
+
from typing import List, Tuple
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SearchMethodsMixin:
|
|
10
|
+
"""
|
|
11
|
+
Mixin providing individual search method implementations.
|
|
12
|
+
|
|
13
|
+
Expects the host class to have:
|
|
14
|
+
- self.bm25: BM25SearchEngine
|
|
15
|
+
- self.optimizer: QueryOptimizer
|
|
16
|
+
- self._tfidf_vectorizer
|
|
17
|
+
- self._tfidf_vectors
|
|
18
|
+
- self._memory_ids: list
|
|
19
|
+
- self._load_graph_engine() method
|
|
20
|
+
- self.search_bm25() method (for graph seed)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def search_bm25(
|
|
24
|
+
self,
|
|
25
|
+
query: str,
|
|
26
|
+
limit: int = 10,
|
|
27
|
+
score_threshold: float = 0.0
|
|
28
|
+
) -> List[Tuple[int, float]]:
|
|
29
|
+
"""
|
|
30
|
+
Search using BM25 keyword matching.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
query: Search query
|
|
34
|
+
limit: Maximum results
|
|
35
|
+
score_threshold: Minimum score threshold
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
List of (memory_id, score) tuples
|
|
39
|
+
"""
|
|
40
|
+
# Optimize query
|
|
41
|
+
optimized = self.optimizer.optimize(
|
|
42
|
+
query,
|
|
43
|
+
enable_spell_correction=True,
|
|
44
|
+
enable_expansion=False # Expansion can hurt precision
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Search with BM25
|
|
48
|
+
results = self.bm25.search(optimized, limit, score_threshold)
|
|
49
|
+
|
|
50
|
+
return results
|
|
51
|
+
|
|
52
|
+
def search_semantic(
|
|
53
|
+
self,
|
|
54
|
+
query: str,
|
|
55
|
+
limit: int = 10,
|
|
56
|
+
score_threshold: float = 0.05
|
|
57
|
+
) -> List[Tuple[int, float]]:
|
|
58
|
+
"""
|
|
59
|
+
Search using TF-IDF semantic similarity.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
query: Search query
|
|
63
|
+
limit: Maximum results
|
|
64
|
+
score_threshold: Minimum similarity threshold
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
List of (memory_id, score) tuples
|
|
68
|
+
"""
|
|
69
|
+
if self._tfidf_vectorizer is None or self._tfidf_vectors is None:
|
|
70
|
+
return []
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
from sklearn.metrics.pairwise import cosine_similarity
|
|
74
|
+
import numpy as np
|
|
75
|
+
|
|
76
|
+
# Vectorize query
|
|
77
|
+
query_vec = self._tfidf_vectorizer.transform([query])
|
|
78
|
+
|
|
79
|
+
# Calculate similarities
|
|
80
|
+
similarities = cosine_similarity(query_vec, self._tfidf_vectors).flatten()
|
|
81
|
+
|
|
82
|
+
# Get top results above threshold
|
|
83
|
+
results = []
|
|
84
|
+
for idx, score in enumerate(similarities):
|
|
85
|
+
if score >= score_threshold:
|
|
86
|
+
memory_id = self._memory_ids[idx]
|
|
87
|
+
results.append((memory_id, float(score)))
|
|
88
|
+
|
|
89
|
+
# Sort by score and limit
|
|
90
|
+
results.sort(key=lambda x: x[1], reverse=True)
|
|
91
|
+
return results[:limit]
|
|
92
|
+
|
|
93
|
+
except Exception as e:
|
|
94
|
+
# Fallback gracefully
|
|
95
|
+
return []
|
|
96
|
+
|
|
97
|
+
def search_graph(
|
|
98
|
+
self,
|
|
99
|
+
query: str,
|
|
100
|
+
limit: int = 10,
|
|
101
|
+
max_depth: int = 2
|
|
102
|
+
) -> List[Tuple[int, float]]:
|
|
103
|
+
"""
|
|
104
|
+
Search using graph traversal from initial matches.
|
|
105
|
+
|
|
106
|
+
Strategy:
|
|
107
|
+
1. Get seed memories from BM25
|
|
108
|
+
2. Traverse graph to find related memories
|
|
109
|
+
3. Score by distance from seed nodes
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
query: Search query
|
|
113
|
+
limit: Maximum results
|
|
114
|
+
max_depth: Maximum graph traversal depth
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
List of (memory_id, score) tuples
|
|
118
|
+
"""
|
|
119
|
+
graph = self._load_graph_engine()
|
|
120
|
+
if graph is None:
|
|
121
|
+
return []
|
|
122
|
+
|
|
123
|
+
# Get seed memories from BM25
|
|
124
|
+
seed_results = self.search_bm25(query, limit=5)
|
|
125
|
+
if not seed_results:
|
|
126
|
+
return []
|
|
127
|
+
|
|
128
|
+
seed_ids = [mem_id for mem_id, _ in seed_results]
|
|
129
|
+
|
|
130
|
+
# Traverse graph from seed nodes
|
|
131
|
+
visited = set(seed_ids)
|
|
132
|
+
results = []
|
|
133
|
+
|
|
134
|
+
# BFS traversal
|
|
135
|
+
queue = [(mem_id, 1.0, 0) for mem_id in seed_ids] # (id, score, depth)
|
|
136
|
+
|
|
137
|
+
while queue and len(results) < limit:
|
|
138
|
+
current_id, current_score, depth = queue.pop(0)
|
|
139
|
+
|
|
140
|
+
if depth > max_depth:
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
# Add to results
|
|
144
|
+
if current_id not in [r[0] for r in results]:
|
|
145
|
+
results.append((current_id, current_score))
|
|
146
|
+
|
|
147
|
+
# Get related memories from graph
|
|
148
|
+
try:
|
|
149
|
+
related = graph.get_related_memories(current_id, limit=5)
|
|
150
|
+
|
|
151
|
+
for rel_id, similarity in related:
|
|
152
|
+
if rel_id not in visited:
|
|
153
|
+
visited.add(rel_id)
|
|
154
|
+
# Decay score by depth
|
|
155
|
+
new_score = current_score * similarity * (0.7 ** depth)
|
|
156
|
+
queue.append((rel_id, new_score, depth + 1))
|
|
157
|
+
|
|
158
|
+
except Exception:
|
|
159
|
+
# Graph operation failed - skip
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
return results[:limit]
|
package/src/search_engine_v2.py
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
|
|
4
|
+
"""SuperLocalMemory V2 - BM25 Search Engine
|
|
4
5
|
|
|
5
|
-
Copyright (c) 2026 Varun Pratap Bhardwaj
|
|
6
6
|
Solution Architect & Original Creator
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
Repository: https://github.com/varun369/SuperLocalMemoryV2
|
|
8
|
+
(see LICENSE file)
|
|
10
9
|
|
|
11
10
|
ATTRIBUTION REQUIRED: This notice must be preserved in all copies.
|
|
12
11
|
"""
|
|
13
|
-
|
|
14
12
|
"""
|
|
15
13
|
BM25 Search Engine - Pure Python Implementation
|
|
16
14
|
|
package/src/setup_validator.py
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Licensed under MIT License
|
|
6
|
-
|
|
7
|
-
Repository: https://github.com/varun369/SuperLocalMemoryV2
|
|
8
|
-
Author: Varun Pratap Bhardwaj (Solution Architect)
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
|
|
4
|
+
"""SuperLocalMemory V2 - Setup Validator
|
|
9
5
|
|
|
10
6
|
First-run setup validator that ensures all dependencies are installed
|
|
11
7
|
and the database is properly initialized.
|
|
12
8
|
"""
|
|
13
|
-
|
|
14
9
|
import sys
|
|
15
10
|
import os
|
|
16
11
|
import sqlite3
|
|
@@ -41,9 +36,8 @@ def print_banner():
|
|
|
41
36
|
"""Print product banner."""
|
|
42
37
|
print("""
|
|
43
38
|
╔══════════════════════════════════════════════════════════════╗
|
|
44
|
-
║ SuperLocalMemory
|
|
45
|
-
║
|
|
46
|
-
║ https://github.com/varun369/SuperLocalMemoryV2 ║
|
|
39
|
+
║ SuperLocalMemory - Setup Validator ║
|
|
40
|
+
║ https://superlocalmemory.com ║
|
|
47
41
|
╚══════════════════════════════════════════════════════════════╝
|
|
48
42
|
""")
|
|
49
43
|
|
|
@@ -338,8 +332,8 @@ def initialize_database() -> Tuple[bool, str]:
|
|
|
338
332
|
# Add system watermark
|
|
339
333
|
cursor.execute('''
|
|
340
334
|
INSERT OR REPLACE INTO system_metadata (key, value) VALUES
|
|
341
|
-
('product', 'SuperLocalMemory
|
|
342
|
-
('
|
|
335
|
+
('product', 'SuperLocalMemory'),
|
|
336
|
+
('website', 'https://superlocalmemory.com'),
|
|
343
337
|
('repository', 'https://github.com/varun369/SuperLocalMemoryV2'),
|
|
344
338
|
('license', 'MIT'),
|
|
345
339
|
('schema_version', '2.0.0')
|
|
@@ -1,16 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
Copyright (c) 2026 Varun Pratap Bhardwaj
|
|
5
|
-
Licensed under MIT License
|
|
6
|
-
|
|
7
|
-
Repository: https://github.com/varun369/SuperLocalMemoryV2
|
|
8
|
-
Author: Varun Pratap Bhardwaj (Solution Architect)
|
|
9
|
-
|
|
10
|
-
NOTICE: This software is protected by MIT License.
|
|
11
|
-
Attribution must be preserved in all copies or derivatives.
|
|
12
|
-
"""
|
|
13
|
-
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
|
|
14
4
|
"""
|
|
15
5
|
SubscriptionManager — Manages durable and ephemeral event subscriptions.
|
|
16
6
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
|
|
3
|
+
"""Tree — Hierarchical Memory Tree Management.
|
|
4
|
+
|
|
5
|
+
Composes the TreeManager class from focused mixin modules:
|
|
6
|
+
- schema.py : DB initialization and root-node bootstrap
|
|
7
|
+
- nodes.py : Node CRUD and count aggregation
|
|
8
|
+
- queries.py : Read-only tree traversal and statistics
|
|
9
|
+
- builder.py : Full tree construction from memories table
|
|
10
|
+
"""
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from .schema import TreeSchemaMixin, MEMORY_DIR, DB_PATH
|
|
15
|
+
from .nodes import TreeNodesMixin
|
|
16
|
+
from .queries import TreeQueriesMixin
|
|
17
|
+
from .builder import TreeBuilderMixin
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TreeManager(TreeSchemaMixin, TreeNodesMixin, TreeQueriesMixin, TreeBuilderMixin):
|
|
21
|
+
"""
|
|
22
|
+
Manages hierarchical tree structure for memory navigation.
|
|
23
|
+
|
|
24
|
+
Tree Structure:
|
|
25
|
+
Root
|
|
26
|
+
+-- Project: NextJS-App
|
|
27
|
+
| +-- Category: Frontend
|
|
28
|
+
| | +-- Memory: React Components
|
|
29
|
+
| | +-- Memory: State Management
|
|
30
|
+
| +-- Category: Backend
|
|
31
|
+
| +-- Memory: API Routes
|
|
32
|
+
+-- Project: Python-ML
|
|
33
|
+
|
|
34
|
+
Materialized Path Format:
|
|
35
|
+
- Root: "1"
|
|
36
|
+
- Project: "1.2"
|
|
37
|
+
- Category: "1.2.3"
|
|
38
|
+
- Memory: "1.2.3.4"
|
|
39
|
+
|
|
40
|
+
Benefits:
|
|
41
|
+
- Fast subtree queries: WHERE tree_path LIKE '1.2.%'
|
|
42
|
+
- O(1) depth calculation: count dots in path
|
|
43
|
+
- O(1) parent lookup: parse path
|
|
44
|
+
- No recursive CTEs needed
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, db_path: Optional[Path] = None):
|
|
48
|
+
"""
|
|
49
|
+
Initialize TreeManager.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
db_path: Optional custom database path
|
|
53
|
+
"""
|
|
54
|
+
self.db_path = db_path or DB_PATH
|
|
55
|
+
self._init_db()
|
|
56
|
+
self.root_id = self._ensure_root()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
__all__ = ['TreeManager', 'MEMORY_DIR', 'DB_PATH']
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
|
|
3
|
+
"""Tree Builder — Constructs the full tree from the memories table.
|
|
4
|
+
|
|
5
|
+
Provides TreeBuilderMixin with build_tree, plus the CLI entry point.
|
|
6
|
+
"""
|
|
7
|
+
import sqlite3
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TreeBuilderMixin:
|
|
11
|
+
"""Builds the hierarchical tree from flat memory records."""
|
|
12
|
+
|
|
13
|
+
def build_tree(self):
|
|
14
|
+
"""
|
|
15
|
+
Build complete tree structure from memories table.
|
|
16
|
+
|
|
17
|
+
Process:
|
|
18
|
+
1. Clear existing tree (except root)
|
|
19
|
+
2. Group memories by project
|
|
20
|
+
3. Group by category within projects
|
|
21
|
+
4. Link individual memories as leaf nodes
|
|
22
|
+
5. Update aggregated counts
|
|
23
|
+
"""
|
|
24
|
+
conn = sqlite3.connect(self.db_path)
|
|
25
|
+
cursor = conn.cursor()
|
|
26
|
+
|
|
27
|
+
# Clear existing tree (keep root)
|
|
28
|
+
cursor.execute('DELETE FROM memory_tree WHERE node_type != ?', ('root',))
|
|
29
|
+
|
|
30
|
+
# Step 1: Create project nodes
|
|
31
|
+
cursor.execute('''
|
|
32
|
+
SELECT DISTINCT project_path, project_name
|
|
33
|
+
FROM memories
|
|
34
|
+
WHERE project_path IS NOT NULL
|
|
35
|
+
ORDER BY project_path
|
|
36
|
+
''')
|
|
37
|
+
projects = cursor.fetchall()
|
|
38
|
+
|
|
39
|
+
project_map = {} # project_path -> node_id
|
|
40
|
+
|
|
41
|
+
for project_path, project_name in projects:
|
|
42
|
+
name = project_name or project_path.split('/')[-1]
|
|
43
|
+
node_id = self.add_node('project', name, self.root_id, description=project_path)
|
|
44
|
+
project_map[project_path] = node_id
|
|
45
|
+
|
|
46
|
+
# Step 2: Create category nodes within projects
|
|
47
|
+
cursor.execute('''
|
|
48
|
+
SELECT DISTINCT project_path, category
|
|
49
|
+
FROM memories
|
|
50
|
+
WHERE project_path IS NOT NULL AND category IS NOT NULL
|
|
51
|
+
ORDER BY project_path, category
|
|
52
|
+
''')
|
|
53
|
+
categories = cursor.fetchall()
|
|
54
|
+
|
|
55
|
+
category_map = {} # (project_path, category) -> node_id
|
|
56
|
+
|
|
57
|
+
for project_path, category in categories:
|
|
58
|
+
parent_id = project_map.get(project_path)
|
|
59
|
+
if parent_id:
|
|
60
|
+
node_id = self.add_node('category', category, parent_id)
|
|
61
|
+
category_map[(project_path, category)] = node_id
|
|
62
|
+
|
|
63
|
+
# Step 3: Link memories as leaf nodes
|
|
64
|
+
cursor.execute('''
|
|
65
|
+
SELECT id, content, summary, project_path, category, importance, created_at
|
|
66
|
+
FROM memories
|
|
67
|
+
ORDER BY created_at DESC
|
|
68
|
+
''')
|
|
69
|
+
memories = cursor.fetchall()
|
|
70
|
+
|
|
71
|
+
for mem_id, content, summary, project_path, category, importance, created_at in memories:
|
|
72
|
+
# Determine parent node
|
|
73
|
+
if project_path and category and (project_path, category) in category_map:
|
|
74
|
+
parent_id = category_map[(project_path, category)]
|
|
75
|
+
elif project_path and project_path in project_map:
|
|
76
|
+
parent_id = project_map[project_path]
|
|
77
|
+
else:
|
|
78
|
+
parent_id = self.root_id
|
|
79
|
+
|
|
80
|
+
# Create memory node
|
|
81
|
+
name = summary or content[:60].replace('\n', ' ')
|
|
82
|
+
self.add_node('memory', name, parent_id, memory_id=mem_id, description=content[:200])
|
|
83
|
+
|
|
84
|
+
# Step 4: Update aggregated counts
|
|
85
|
+
self._update_all_counts()
|
|
86
|
+
|
|
87
|
+
conn.commit()
|
|
88
|
+
conn.close()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def run_cli():
|
|
92
|
+
"""CLI entry point for tree_manager."""
|
|
93
|
+
import sys
|
|
94
|
+
import json
|
|
95
|
+
from src.tree import TreeManager
|
|
96
|
+
|
|
97
|
+
tree_mgr = TreeManager()
|
|
98
|
+
|
|
99
|
+
if len(sys.argv) < 2:
|
|
100
|
+
print("TreeManager CLI")
|
|
101
|
+
print("\nCommands:")
|
|
102
|
+
print(" python tree_manager.py build # Build tree from memories")
|
|
103
|
+
print(" python tree_manager.py show [project] [depth] # Show tree structure")
|
|
104
|
+
print(" python tree_manager.py subtree <node_id> # Get subtree")
|
|
105
|
+
print(" python tree_manager.py path <node_id> # Get path to root")
|
|
106
|
+
print(" python tree_manager.py stats # Show statistics")
|
|
107
|
+
print(" python tree_manager.py add <type> <name> <parent_id> # Add node")
|
|
108
|
+
print(" python tree_manager.py delete <node_id> # Delete node")
|
|
109
|
+
sys.exit(0)
|
|
110
|
+
|
|
111
|
+
command = sys.argv[1]
|
|
112
|
+
|
|
113
|
+
if command == "build":
|
|
114
|
+
print("Building tree from memories...")
|
|
115
|
+
tree_mgr.build_tree()
|
|
116
|
+
stats = tree_mgr.get_stats()
|
|
117
|
+
print(f"Tree built: {stats['total_nodes']} nodes, {stats['total_memories']} memories")
|
|
118
|
+
|
|
119
|
+
elif command == "show":
|
|
120
|
+
project = sys.argv[2] if len(sys.argv) > 2 else None
|
|
121
|
+
max_depth = int(sys.argv[3]) if len(sys.argv) > 3 else None
|
|
122
|
+
|
|
123
|
+
tree = tree_mgr.get_tree(project, max_depth)
|
|
124
|
+
|
|
125
|
+
def print_tree(node, indent=0):
|
|
126
|
+
if 'error' in node:
|
|
127
|
+
print(node['error'])
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
prefix = " " * indent
|
|
131
|
+
icon = {"root": "\U0001f333", "project": "\U0001f4c1", "category": "\U0001f4c2", "memory": "\U0001f4c4"}.get(node['type'], "\u2022")
|
|
132
|
+
|
|
133
|
+
print(f"{prefix}{icon} {node['name']} (id={node['id']}, memories={node['memory_count']})")
|
|
134
|
+
|
|
135
|
+
for child in node.get('children', []):
|
|
136
|
+
print_tree(child, indent + 1)
|
|
137
|
+
|
|
138
|
+
print_tree(tree)
|
|
139
|
+
|
|
140
|
+
elif command == "subtree" and len(sys.argv) >= 3:
|
|
141
|
+
node_id = int(sys.argv[2])
|
|
142
|
+
nodes = tree_mgr.get_subtree(node_id)
|
|
143
|
+
|
|
144
|
+
if not nodes:
|
|
145
|
+
print(f"No subtree found for node {node_id}")
|
|
146
|
+
else:
|
|
147
|
+
print(f"Subtree of node {node_id}:")
|
|
148
|
+
for node in nodes:
|
|
149
|
+
indent = " " * (node['depth'] - nodes[0]['depth'] + 1)
|
|
150
|
+
print(f"{indent}- {node['name']} (id={node['id']})")
|
|
151
|
+
|
|
152
|
+
elif command == "path" and len(sys.argv) >= 3:
|
|
153
|
+
node_id = int(sys.argv[2])
|
|
154
|
+
path = tree_mgr.get_path_to_root(node_id)
|
|
155
|
+
|
|
156
|
+
if not path:
|
|
157
|
+
print(f"Node {node_id} not found")
|
|
158
|
+
else:
|
|
159
|
+
print("Path to root:")
|
|
160
|
+
print(" > ".join([f"{n['name']} (id={n['id']})" for n in path]))
|
|
161
|
+
|
|
162
|
+
elif command == "stats":
|
|
163
|
+
stats = tree_mgr.get_stats()
|
|
164
|
+
print(json.dumps(stats, indent=2))
|
|
165
|
+
|
|
166
|
+
elif command == "add" and len(sys.argv) >= 5:
|
|
167
|
+
node_type = sys.argv[2]
|
|
168
|
+
name = sys.argv[3]
|
|
169
|
+
parent_id = int(sys.argv[4])
|
|
170
|
+
|
|
171
|
+
node_id = tree_mgr.add_node(node_type, name, parent_id)
|
|
172
|
+
print(f"Node created with ID: {node_id}")
|
|
173
|
+
|
|
174
|
+
elif command == "delete" and len(sys.argv) >= 3:
|
|
175
|
+
node_id = int(sys.argv[2])
|
|
176
|
+
if tree_mgr.delete_node(node_id):
|
|
177
|
+
print(f"Node {node_id} deleted")
|
|
178
|
+
else:
|
|
179
|
+
print(f"Node {node_id} not found")
|
|
180
|
+
|
|
181
|
+
else:
|
|
182
|
+
print(f"Unknown command: {command}")
|
|
183
|
+
sys.exit(1)
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
|
|
3
|
+
"""Tree Nodes — CRUD operations and count aggregation.
|
|
4
|
+
|
|
5
|
+
Provides TreeNodesMixin with add_node, delete_node, update_counts,
|
|
6
|
+
and internal helper methods for materialized-path management.
|
|
7
|
+
"""
|
|
8
|
+
import sqlite3
|
|
9
|
+
from typing import Optional, Dict, Any
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TreeNodesMixin:
|
|
14
|
+
"""Node CRUD and aggregation logic for the memory tree."""
|
|
15
|
+
|
|
16
|
+
def add_node(
|
|
17
|
+
self,
|
|
18
|
+
node_type: str,
|
|
19
|
+
name: str,
|
|
20
|
+
parent_id: int,
|
|
21
|
+
description: Optional[str] = None,
|
|
22
|
+
memory_id: Optional[int] = None
|
|
23
|
+
) -> int:
|
|
24
|
+
"""
|
|
25
|
+
Add a new node to the tree.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
node_type: Type of node ('root', 'project', 'category', 'memory')
|
|
29
|
+
name: Display name
|
|
30
|
+
parent_id: Parent node ID
|
|
31
|
+
description: Optional description
|
|
32
|
+
memory_id: Link to memories table (for leaf nodes)
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
New node ID
|
|
36
|
+
"""
|
|
37
|
+
conn = sqlite3.connect(self.db_path)
|
|
38
|
+
cursor = conn.cursor()
|
|
39
|
+
|
|
40
|
+
# Get parent path and depth
|
|
41
|
+
cursor.execute('SELECT tree_path, depth FROM memory_tree WHERE id = ?', (parent_id,))
|
|
42
|
+
result = cursor.fetchone()
|
|
43
|
+
|
|
44
|
+
if not result:
|
|
45
|
+
raise ValueError(f"Parent node {parent_id} not found")
|
|
46
|
+
|
|
47
|
+
parent_path, parent_depth = result
|
|
48
|
+
|
|
49
|
+
# Calculate new node position
|
|
50
|
+
depth = parent_depth + 1
|
|
51
|
+
|
|
52
|
+
cursor.execute('''
|
|
53
|
+
INSERT INTO memory_tree (
|
|
54
|
+
node_type, name, description,
|
|
55
|
+
parent_id, tree_path, depth,
|
|
56
|
+
memory_id, last_updated
|
|
57
|
+
)
|
|
58
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
59
|
+
''', (
|
|
60
|
+
node_type,
|
|
61
|
+
name,
|
|
62
|
+
description,
|
|
63
|
+
parent_id,
|
|
64
|
+
'', # Placeholder, updated below
|
|
65
|
+
depth,
|
|
66
|
+
memory_id,
|
|
67
|
+
datetime.now().isoformat()
|
|
68
|
+
))
|
|
69
|
+
|
|
70
|
+
node_id = cursor.lastrowid
|
|
71
|
+
|
|
72
|
+
# Update tree_path with actual node_id
|
|
73
|
+
tree_path = f"{parent_path}.{node_id}"
|
|
74
|
+
cursor.execute('UPDATE memory_tree SET tree_path = ? WHERE id = ?', (tree_path, node_id))
|
|
75
|
+
|
|
76
|
+
conn.commit()
|
|
77
|
+
conn.close()
|
|
78
|
+
|
|
79
|
+
return node_id
|
|
80
|
+
|
|
81
|
+
def delete_node(self, node_id: int) -> bool:
|
|
82
|
+
"""
|
|
83
|
+
Delete a node and all its descendants.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
node_id: Node ID to delete
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
True if deleted, False if not found
|
|
90
|
+
"""
|
|
91
|
+
if node_id == self.root_id:
|
|
92
|
+
raise ValueError("Cannot delete root node")
|
|
93
|
+
|
|
94
|
+
conn = sqlite3.connect(self.db_path)
|
|
95
|
+
cursor = conn.cursor()
|
|
96
|
+
|
|
97
|
+
# Get tree_path
|
|
98
|
+
cursor.execute('SELECT tree_path, parent_id FROM memory_tree WHERE id = ?', (node_id,))
|
|
99
|
+
result = cursor.fetchone()
|
|
100
|
+
|
|
101
|
+
if not result:
|
|
102
|
+
conn.close()
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
tree_path, parent_id = result
|
|
106
|
+
|
|
107
|
+
# Delete node and all descendants (CASCADE handles children)
|
|
108
|
+
cursor.execute('DELETE FROM memory_tree WHERE id = ? OR tree_path LIKE ?',
|
|
109
|
+
(node_id, f"{tree_path}.%"))
|
|
110
|
+
|
|
111
|
+
deleted = cursor.rowcount > 0
|
|
112
|
+
conn.commit()
|
|
113
|
+
conn.close()
|
|
114
|
+
|
|
115
|
+
# Update parent counts
|
|
116
|
+
if deleted and parent_id:
|
|
117
|
+
self.update_counts(parent_id)
|
|
118
|
+
|
|
119
|
+
return deleted
|
|
120
|
+
|
|
121
|
+
def update_counts(self, node_id: int):
|
|
122
|
+
"""
|
|
123
|
+
Update aggregated counts for a node (memory_count, total_size).
|
|
124
|
+
Recursively updates all ancestors.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
node_id: Node ID to update
|
|
128
|
+
"""
|
|
129
|
+
conn = sqlite3.connect(self.db_path)
|
|
130
|
+
cursor = conn.cursor()
|
|
131
|
+
|
|
132
|
+
# Get all descendant memory nodes
|
|
133
|
+
cursor.execute('SELECT tree_path FROM memory_tree WHERE id = ?', (node_id,))
|
|
134
|
+
result = cursor.fetchone()
|
|
135
|
+
|
|
136
|
+
if not result:
|
|
137
|
+
conn.close()
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
tree_path = result[0]
|
|
141
|
+
|
|
142
|
+
# Count memories in subtree
|
|
143
|
+
cursor.execute('''
|
|
144
|
+
SELECT COUNT(*), COALESCE(SUM(LENGTH(m.content)), 0)
|
|
145
|
+
FROM memory_tree t
|
|
146
|
+
LEFT JOIN memories m ON t.memory_id = m.id
|
|
147
|
+
WHERE t.tree_path LIKE ? AND t.memory_id IS NOT NULL
|
|
148
|
+
''', (f"{tree_path}%",))
|
|
149
|
+
|
|
150
|
+
memory_count, total_size = cursor.fetchone()
|
|
151
|
+
|
|
152
|
+
# Update node
|
|
153
|
+
cursor.execute('''
|
|
154
|
+
UPDATE memory_tree
|
|
155
|
+
SET memory_count = ?, total_size = ?, last_updated = ?
|
|
156
|
+
WHERE id = ?
|
|
157
|
+
''', (memory_count, total_size, datetime.now().isoformat(), node_id))
|
|
158
|
+
|
|
159
|
+
# Update all ancestors
|
|
160
|
+
path_ids = [int(x) for x in tree_path.split('.')]
|
|
161
|
+
for ancestor_id in path_ids[:-1]: # Exclude current node
|
|
162
|
+
self.update_counts(ancestor_id)
|
|
163
|
+
|
|
164
|
+
conn.commit()
|
|
165
|
+
conn.close()
|
|
166
|
+
|
|
167
|
+
def _update_all_counts(self):
|
|
168
|
+
"""Update counts for all nodes (used after build_tree)."""
|
|
169
|
+
conn = sqlite3.connect(self.db_path)
|
|
170
|
+
cursor = conn.cursor()
|
|
171
|
+
|
|
172
|
+
# Get all nodes in reverse depth order (leaves first)
|
|
173
|
+
cursor.execute('''
|
|
174
|
+
SELECT id FROM memory_tree
|
|
175
|
+
ORDER BY depth DESC
|
|
176
|
+
''')
|
|
177
|
+
|
|
178
|
+
node_ids = [row[0] for row in cursor.fetchall()]
|
|
179
|
+
conn.close()
|
|
180
|
+
|
|
181
|
+
# Update each node (will cascade to parents)
|
|
182
|
+
processed = set()
|
|
183
|
+
for node_id in node_ids:
|
|
184
|
+
if node_id not in processed:
|
|
185
|
+
self.update_counts(node_id)
|
|
186
|
+
processed.add(node_id)
|
|
187
|
+
|
|
188
|
+
def _generate_tree_path(self, parent_path: str, node_id: int) -> str:
|
|
189
|
+
"""Generate tree_path for a new node."""
|
|
190
|
+
if parent_path:
|
|
191
|
+
return f"{parent_path}.{node_id}"
|
|
192
|
+
return str(node_id)
|
|
193
|
+
|
|
194
|
+
def _calculate_depth(self, tree_path: str) -> int:
|
|
195
|
+
"""Calculate depth from tree_path (count dots)."""
|
|
196
|
+
return tree_path.count('.')
|