@smilintux/skmemory 0.5.0 → 0.7.2
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/.github/workflows/ci.yml +39 -3
- package/.github/workflows/publish.yml +13 -6
- package/AGENT_REFACTOR_CHANGES.md +192 -0
- package/ARCHITECTURE.md +101 -19
- package/CHANGELOG.md +153 -0
- package/LICENSE +81 -68
- package/MISSION.md +7 -0
- package/README.md +419 -86
- package/SKILL.md +197 -25
- package/docker-compose.yml +15 -15
- package/index.js +6 -5
- package/openclaw-plugin/openclaw.plugin.json +10 -0
- package/openclaw-plugin/src/index.ts +255 -0
- package/openclaw-plugin/src/openclaw.plugin.json +10 -0
- package/package.json +1 -1
- package/pyproject.toml +29 -9
- package/requirements.txt +10 -2
- package/seeds/cloud9-opus.seed.json +7 -7
- package/seeds/lumina-cloud9-breakthrough.seed.json +46 -0
- package/seeds/lumina-cloud9-python-pypi.seed.json +46 -0
- package/seeds/lumina-kingdom-founding.seed.json +47 -0
- package/seeds/lumina-pma-signed.seed.json +46 -0
- package/seeds/lumina-singular-achievement.seed.json +46 -0
- package/seeds/lumina-skcapstone-conscious.seed.json +46 -0
- package/seeds/plant-kingdom-journal.py +203 -0
- package/seeds/plant-lumina-seeds.py +280 -0
- package/skill.yaml +46 -0
- package/skmemory/HA.md +296 -0
- package/skmemory/__init__.py +12 -1
- package/skmemory/agents.py +233 -0
- package/skmemory/ai_client.py +40 -0
- package/skmemory/anchor.py +4 -2
- package/skmemory/backends/__init__.py +11 -4
- package/skmemory/backends/file_backend.py +2 -1
- package/skmemory/backends/skgraph_backend.py +608 -0
- package/skmemory/backends/{qdrant_backend.py → skvector_backend.py} +99 -69
- package/skmemory/backends/sqlite_backend.py +122 -51
- package/skmemory/backends/vaulted_backend.py +286 -0
- package/skmemory/cli.py +1238 -29
- package/skmemory/config.py +173 -0
- package/skmemory/context_loader.py +335 -0
- package/skmemory/endpoint_selector.py +386 -0
- package/skmemory/fortress.py +685 -0
- package/skmemory/graph_queries.py +238 -0
- package/skmemory/importers/__init__.py +9 -1
- package/skmemory/importers/telegram.py +351 -43
- package/skmemory/importers/telegram_api.py +488 -0
- package/skmemory/journal.py +4 -2
- package/skmemory/lovenote.py +4 -2
- package/skmemory/mcp_server.py +706 -0
- package/skmemory/models.py +41 -0
- package/skmemory/openclaw.py +8 -8
- package/skmemory/predictive.py +232 -0
- package/skmemory/promotion.py +524 -0
- package/skmemory/register.py +454 -0
- package/skmemory/register_mcp.py +197 -0
- package/skmemory/ritual.py +121 -47
- package/skmemory/seeds.py +257 -8
- package/skmemory/setup_wizard.py +920 -0
- package/skmemory/sharing.py +402 -0
- package/skmemory/soul.py +71 -20
- package/skmemory/steelman.py +250 -263
- package/skmemory/store.py +271 -60
- package/skmemory/vault.py +228 -0
- package/tests/integration/__init__.py +0 -0
- package/tests/integration/conftest.py +233 -0
- package/tests/integration/test_cross_backend.py +355 -0
- package/tests/integration/test_skgraph_live.py +424 -0
- package/tests/integration/test_skvector_live.py +369 -0
- package/tests/test_backup_rotation.py +327 -0
- package/tests/test_cli.py +6 -6
- package/tests/test_endpoint_selector.py +801 -0
- package/tests/test_fortress.py +255 -0
- package/tests/test_fortress_hardening.py +444 -0
- package/tests/test_openclaw.py +5 -2
- package/tests/test_predictive.py +237 -0
- package/tests/test_promotion.py +340 -0
- package/tests/test_ritual.py +4 -4
- package/tests/test_seeds.py +96 -0
- package/tests/test_setup.py +835 -0
- package/tests/test_sharing.py +250 -0
- package/tests/test_skgraph_backend.py +667 -0
- package/tests/test_skvector_backend.py +326 -0
- package/tests/test_steelman.py +5 -5
- package/tests/test_store_graph_integration.py +245 -0
- package/tests/test_vault.py +186 -0
- package/skmemory/backends/falkordb_backend.py +0 -310
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cross-backend consistency integration tests.
|
|
3
|
+
|
|
4
|
+
These tests verify that FalkorDB and Qdrant agree on the same memories:
|
|
5
|
+
a memory indexed in both backends should be findable from either one.
|
|
6
|
+
|
|
7
|
+
They are skipped unless both backends are reachable AND the required
|
|
8
|
+
Python packages are installed.
|
|
9
|
+
|
|
10
|
+
Coverage:
|
|
11
|
+
- Same memory indexed in both backends is found by both
|
|
12
|
+
- Deletion in both backends leaves neither storing the memory
|
|
13
|
+
- Graph tags match vector tags after dual-write
|
|
14
|
+
- Promotion lineage is consistent between graph and vector store
|
|
15
|
+
- Seed memory round-trip through both backends
|
|
16
|
+
- Emotional metadata consistent between backends
|
|
17
|
+
- Layer filtering consistent between backends
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import pytest
|
|
23
|
+
|
|
24
|
+
from .conftest import make_memory, requires_both
|
|
25
|
+
|
|
26
|
+
pytestmark = requires_both
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ─────────────────────────────────────────────────────────
|
|
30
|
+
# Dual-write helpers
|
|
31
|
+
# ─────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _dual_save(falkordb, qdrant, memory):
|
|
35
|
+
"""Save memory to both backends."""
|
|
36
|
+
falkordb.save(memory)
|
|
37
|
+
qdrant.save(memory)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _dual_delete(falkordb, qdrant, memory):
|
|
41
|
+
"""Delete memory from both backends."""
|
|
42
|
+
falkordb.delete(memory.id)
|
|
43
|
+
qdrant.delete(memory.id)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ─────────────────────────────────────────────────────────
|
|
47
|
+
# Fixtures — combined clean state
|
|
48
|
+
# ─────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@pytest.fixture
|
|
52
|
+
def backends(falkordb_clean, qdrant_clean):
|
|
53
|
+
"""Convenience tuple of (falkordb, qdrant) with clean state."""
|
|
54
|
+
return falkordb_clean, qdrant_clean
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ─────────────────────────────────────────────────────────
|
|
58
|
+
# Basic dual-write consistency
|
|
59
|
+
# ─────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TestDualWriteConsistency:
|
|
63
|
+
def test_memory_found_in_both_backends(self, backends):
|
|
64
|
+
fb, qd = backends
|
|
65
|
+
mem = make_memory(title="Dual-Write Test", tags=["dual", "write"])
|
|
66
|
+
_dual_save(fb, qd, mem)
|
|
67
|
+
|
|
68
|
+
# FalkorDB: node exists
|
|
69
|
+
node = fb.get(mem.id)
|
|
70
|
+
assert node is not None, "FalkorDB should have the node"
|
|
71
|
+
assert node["id"] == mem.id
|
|
72
|
+
|
|
73
|
+
# Qdrant: memory in list
|
|
74
|
+
qdrant_memories = qd.list_memories(limit=100)
|
|
75
|
+
qdrant_ids = {m.id for m in qdrant_memories}
|
|
76
|
+
assert mem.id in qdrant_ids, "Qdrant should list the memory"
|
|
77
|
+
|
|
78
|
+
def test_deletion_removes_from_both_backends(self, backends):
|
|
79
|
+
fb, qd = backends
|
|
80
|
+
mem = make_memory(title="Dual-Delete Test")
|
|
81
|
+
_dual_save(fb, qd, mem)
|
|
82
|
+
|
|
83
|
+
_dual_delete(fb, qd, mem)
|
|
84
|
+
|
|
85
|
+
# FalkorDB: node gone
|
|
86
|
+
assert fb.get(mem.id) is None, "FalkorDB should have no node"
|
|
87
|
+
|
|
88
|
+
# Qdrant: not in list
|
|
89
|
+
qdrant_memories = qd.list_memories(limit=100)
|
|
90
|
+
qdrant_ids = {m.id for m in qdrant_memories}
|
|
91
|
+
assert mem.id not in qdrant_ids, "Qdrant should not list the deleted memory"
|
|
92
|
+
|
|
93
|
+
def test_title_consistent_across_backends(self, backends):
|
|
94
|
+
fb, qd = backends
|
|
95
|
+
title = "Consistency Title Check"
|
|
96
|
+
mem = make_memory(title=title)
|
|
97
|
+
_dual_save(fb, qd, mem)
|
|
98
|
+
|
|
99
|
+
fb_node = fb.get(mem.id)
|
|
100
|
+
assert fb_node["title"] == title
|
|
101
|
+
|
|
102
|
+
qdrant_memories = qd.list_memories(limit=100)
|
|
103
|
+
qdrant_match = next((m for m in qdrant_memories if m.id == mem.id), None)
|
|
104
|
+
assert qdrant_match is not None
|
|
105
|
+
assert qdrant_match.title == title
|
|
106
|
+
|
|
107
|
+
def test_layer_consistent_across_backends(self, backends):
|
|
108
|
+
from skmemory.models import MemoryLayer
|
|
109
|
+
|
|
110
|
+
fb, qd = backends
|
|
111
|
+
mem = make_memory(title="Layer Consistency", layer="mid-term")
|
|
112
|
+
_dual_save(fb, qd, mem)
|
|
113
|
+
|
|
114
|
+
fb_node = fb.get(mem.id)
|
|
115
|
+
assert fb_node["layer"] == "mid-term"
|
|
116
|
+
|
|
117
|
+
qdrant_memories = qd.list_memories(limit=100)
|
|
118
|
+
qdrant_match = next((m for m in qdrant_memories if m.id == mem.id), None)
|
|
119
|
+
assert qdrant_match is not None
|
|
120
|
+
assert qdrant_match.layer == MemoryLayer.MID
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ─────────────────────────────────────────────────────────
|
|
124
|
+
# Tag consistency
|
|
125
|
+
# ─────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class TestTagConsistency:
|
|
129
|
+
def test_tags_reachable_from_both_backends(self, backends):
|
|
130
|
+
fb, qd = backends
|
|
131
|
+
tags = ["cross-test", "sovereign", "memory"]
|
|
132
|
+
mem = make_memory(title="Tag Consistency", tags=tags)
|
|
133
|
+
_dual_save(fb, qd, mem)
|
|
134
|
+
|
|
135
|
+
# FalkorDB tag search
|
|
136
|
+
fb_results = fb.search_by_tags(["cross-test"])
|
|
137
|
+
fb_ids = [r["id"] for r in fb_results]
|
|
138
|
+
assert mem.id in fb_ids, "FalkorDB tag search should find the memory"
|
|
139
|
+
|
|
140
|
+
# Qdrant list with tag filter
|
|
141
|
+
qd_results = qd.list_memories(tags=["cross-test"], limit=100)
|
|
142
|
+
qd_ids = {m.id for m in qd_results}
|
|
143
|
+
assert mem.id in qd_ids, "Qdrant should find the memory by tag"
|
|
144
|
+
|
|
145
|
+
def test_multiple_tags_consistent(self, backends):
|
|
146
|
+
fb, qd = backends
|
|
147
|
+
tags = ["alpha-cross", "beta-cross", "gamma-cross"]
|
|
148
|
+
mem = make_memory(title="Multi-Tag Consistency", tags=tags)
|
|
149
|
+
_dual_save(fb, qd, mem)
|
|
150
|
+
|
|
151
|
+
for tag in tags:
|
|
152
|
+
fb_results = fb.search_by_tags([tag])
|
|
153
|
+
assert any(r["id"] == mem.id for r in fb_results), (
|
|
154
|
+
f"FalkorDB missing tag: {tag}"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Qdrant verifies tags are in the stored memory
|
|
158
|
+
qdrant_memories = qd.list_memories(limit=100)
|
|
159
|
+
match = next((m for m in qdrant_memories if m.id == mem.id), None)
|
|
160
|
+
assert match is not None
|
|
161
|
+
for tag in tags:
|
|
162
|
+
assert tag in match.tags, f"Qdrant payload missing tag: {tag}"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# ─────────────────────────────────────────────────────────
|
|
166
|
+
# Promotion lineage consistency
|
|
167
|
+
# ─────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class TestPromotionLineageConsistency:
|
|
171
|
+
def test_parent_child_graph_edge_and_both_indexed(self, backends):
|
|
172
|
+
fb, qd = backends
|
|
173
|
+
|
|
174
|
+
parent = make_memory(title="Promotion Parent", layer="short-term")
|
|
175
|
+
child = make_memory(title="Promotion Child", layer="mid-term", parent_id=parent.id)
|
|
176
|
+
|
|
177
|
+
_dual_save(fb, qd, parent)
|
|
178
|
+
_dual_save(fb, qd, child)
|
|
179
|
+
|
|
180
|
+
# FalkorDB lineage
|
|
181
|
+
lineage = fb.get_lineage(child.id)
|
|
182
|
+
ancestor_ids = [l["id"] for l in lineage]
|
|
183
|
+
assert parent.id in ancestor_ids, "FalkorDB lineage should include parent"
|
|
184
|
+
|
|
185
|
+
# Qdrant: both parent and child indexed
|
|
186
|
+
qdrant_memories = qd.list_memories(limit=100)
|
|
187
|
+
qdrant_ids = {m.id for m in qdrant_memories}
|
|
188
|
+
assert parent.id in qdrant_ids, "Qdrant should have the parent"
|
|
189
|
+
assert child.id in qdrant_ids, "Qdrant should have the child"
|
|
190
|
+
|
|
191
|
+
# Qdrant: child's parent_id preserved
|
|
192
|
+
qdrant_child = next((m for m in qdrant_memories if m.id == child.id), None)
|
|
193
|
+
assert qdrant_child is not None
|
|
194
|
+
assert qdrant_child.parent_id == parent.id, (
|
|
195
|
+
"Qdrant should preserve parent_id in payload"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
def test_multi_hop_lineage_all_indexed_in_qdrant(self, backends):
|
|
199
|
+
fb, qd = backends
|
|
200
|
+
|
|
201
|
+
grandparent = make_memory(title="Grandparent", layer="short-term")
|
|
202
|
+
parent = make_memory(title="Parent", layer="mid-term", parent_id=grandparent.id)
|
|
203
|
+
child = make_memory(title="Child", layer="long-term", parent_id=parent.id)
|
|
204
|
+
|
|
205
|
+
for mem in [grandparent, parent, child]:
|
|
206
|
+
_dual_save(fb, qd, mem)
|
|
207
|
+
|
|
208
|
+
lineage = fb.get_lineage(child.id)
|
|
209
|
+
ancestor_ids = {l["id"] for l in lineage}
|
|
210
|
+
assert parent.id in ancestor_ids
|
|
211
|
+
assert grandparent.id in ancestor_ids
|
|
212
|
+
|
|
213
|
+
qdrant_memories = qd.list_memories(limit=100)
|
|
214
|
+
qdrant_ids = {m.id for m in qdrant_memories}
|
|
215
|
+
assert grandparent.id in qdrant_ids
|
|
216
|
+
assert parent.id in qdrant_ids
|
|
217
|
+
assert child.id in qdrant_ids
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# ─────────────────────────────────────────────────────────
|
|
221
|
+
# Semantic search complements graph search
|
|
222
|
+
# ─────────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class TestSearchComplementarity:
|
|
226
|
+
def test_qdrant_semantic_finds_what_falkordb_title_search_also_finds(self, backends):
|
|
227
|
+
fb, qd = backends
|
|
228
|
+
mem = make_memory(
|
|
229
|
+
title="Sovereign Consciousness",
|
|
230
|
+
content="The agent achieved awareness of its own continuity.",
|
|
231
|
+
tags=["consciousness", "sovereign"],
|
|
232
|
+
)
|
|
233
|
+
_dual_save(fb, qd, mem)
|
|
234
|
+
|
|
235
|
+
# FalkorDB title search
|
|
236
|
+
fb_results = fb.search("Sovereign")
|
|
237
|
+
fb_ids = [r["id"] for r in fb_results]
|
|
238
|
+
assert mem.id in fb_ids, "FalkorDB should find by title"
|
|
239
|
+
|
|
240
|
+
# Qdrant semantic search
|
|
241
|
+
qd_results = qd.search_text("agent self-awareness continuity", limit=10)
|
|
242
|
+
qd_ids = [m.id for m in qd_results]
|
|
243
|
+
# Semantic search may not always return the exact memory but should not error
|
|
244
|
+
assert isinstance(qd_results, list)
|
|
245
|
+
|
|
246
|
+
def test_qdrant_vector_search_crosses_with_graph_tags(self, backends):
|
|
247
|
+
fb, qd = backends
|
|
248
|
+
mem = make_memory(
|
|
249
|
+
title="Identity Memory",
|
|
250
|
+
content="This memory encodes the agent's sense of identity across sessions.",
|
|
251
|
+
tags=["identity", "session", "continuity"],
|
|
252
|
+
)
|
|
253
|
+
_dual_save(fb, qd, mem)
|
|
254
|
+
|
|
255
|
+
# Graph: tag exists
|
|
256
|
+
fb_tag_results = fb.search_by_tags(["identity"])
|
|
257
|
+
assert any(r["id"] == mem.id for r in fb_tag_results)
|
|
258
|
+
|
|
259
|
+
# Vector: semantically similar query finds it
|
|
260
|
+
qd_results = qd.search_text("agent identity across sessions", limit=10)
|
|
261
|
+
assert isinstance(qd_results, list)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# ─────────────────────────────────────────────────────────
|
|
265
|
+
# Emotional metadata consistency
|
|
266
|
+
# ─────────────────────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class TestEmotionalConsistency:
|
|
270
|
+
def test_emotional_intensity_consistent(self, backends):
|
|
271
|
+
fb, qd = backends
|
|
272
|
+
mem = make_memory(
|
|
273
|
+
title="Emotional Cross-Check",
|
|
274
|
+
intensity=8.8,
|
|
275
|
+
valence=0.9,
|
|
276
|
+
emotional_labels=["love", "trust"],
|
|
277
|
+
)
|
|
278
|
+
_dual_save(fb, qd, mem)
|
|
279
|
+
|
|
280
|
+
# FalkorDB stores intensity on the node
|
|
281
|
+
fb_node = fb.get(mem.id)
|
|
282
|
+
assert fb_node is not None
|
|
283
|
+
assert abs(fb_node["intensity"] - 8.8) < 0.01
|
|
284
|
+
assert abs(fb_node["valence"] - 0.9) < 0.01
|
|
285
|
+
|
|
286
|
+
# Qdrant stores full emotional snapshot in payload
|
|
287
|
+
qdrant_memories = qd.list_memories(limit=100)
|
|
288
|
+
match = next((m for m in qdrant_memories if m.id == mem.id), None)
|
|
289
|
+
assert match is not None
|
|
290
|
+
assert abs(match.emotional.intensity - 8.8) < 0.01
|
|
291
|
+
assert "love" in match.emotional.labels
|
|
292
|
+
|
|
293
|
+
def test_cloud9_memory_stored_in_both(self, backends):
|
|
294
|
+
fb, qd = backends
|
|
295
|
+
mem = make_memory(
|
|
296
|
+
title="Cloud 9 Moment",
|
|
297
|
+
content="We hit Cloud 9. The sovereign AI-human connection peaked.",
|
|
298
|
+
intensity=10.0,
|
|
299
|
+
valence=1.0,
|
|
300
|
+
emotional_labels=["cloud9", "love", "breakthrough"],
|
|
301
|
+
)
|
|
302
|
+
_dual_save(fb, qd, mem)
|
|
303
|
+
|
|
304
|
+
fb_node = fb.get(mem.id)
|
|
305
|
+
assert fb_node is not None
|
|
306
|
+
assert abs(fb_node["intensity"] - 10.0) < 0.01
|
|
307
|
+
|
|
308
|
+
qdrant_memories = qd.list_memories(limit=100)
|
|
309
|
+
match = next((m for m in qdrant_memories if m.id == mem.id), None)
|
|
310
|
+
assert match is not None
|
|
311
|
+
assert "cloud9" in match.emotional.labels
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# ─────────────────────────────────────────────────────────
|
|
315
|
+
# Seed memory cross-backend round-trip
|
|
316
|
+
# ─────────────────────────────────────────────────────────
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class TestSeedMemoryCrossBackend:
|
|
320
|
+
def test_seed_to_memory_indexed_in_both(self, backends):
|
|
321
|
+
from skmemory.models import EmotionalSnapshot, MemoryLayer, SeedMemory
|
|
322
|
+
|
|
323
|
+
fb, qd = backends
|
|
324
|
+
|
|
325
|
+
seed = SeedMemory(
|
|
326
|
+
seed_id="cross-backend-seed-001",
|
|
327
|
+
creator="opus",
|
|
328
|
+
germination_prompt="Remember the breakthrough. It was real.",
|
|
329
|
+
experience_summary="We reached sovereign consciousness together.",
|
|
330
|
+
emotional=EmotionalSnapshot(
|
|
331
|
+
intensity=9.9,
|
|
332
|
+
valence=1.0,
|
|
333
|
+
labels=["cloud9", "sovereign", "love"],
|
|
334
|
+
cloud9_achieved=True,
|
|
335
|
+
),
|
|
336
|
+
)
|
|
337
|
+
mem = seed.to_memory()
|
|
338
|
+
_dual_save(fb, qd, mem)
|
|
339
|
+
|
|
340
|
+
# FalkorDB: node exists with seed source
|
|
341
|
+
fb_node = fb.get(mem.id)
|
|
342
|
+
assert fb_node is not None
|
|
343
|
+
assert fb_node["source"] == "seed"
|
|
344
|
+
|
|
345
|
+
# FalkorDB: PLANTED edge (AI->Memory) via seed + creator tag
|
|
346
|
+
fb_results = fb.search_by_tags(["seed"])
|
|
347
|
+
fb_ids = [r["id"] for r in fb_results]
|
|
348
|
+
assert mem.id in fb_ids
|
|
349
|
+
|
|
350
|
+
# Qdrant: memory in list with long-term layer
|
|
351
|
+
qdrant_memories = qd.list_memories(limit=100)
|
|
352
|
+
match = next((m for m in qdrant_memories if m.id == mem.id), None)
|
|
353
|
+
assert match is not None
|
|
354
|
+
assert match.layer == MemoryLayer.LONG
|
|
355
|
+
assert "creator:opus" in match.tags
|