@smilintux/skmemory 0.5.0 → 0.9.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 +40 -4
- package/.github/workflows/publish.yml +11 -5
- package/AGENT_REFACTOR_CHANGES.md +192 -0
- package/ARCHITECTURE.md +399 -19
- package/CHANGELOG.md +179 -0
- package/LICENSE +81 -68
- package/MISSION.md +7 -0
- package/README.md +425 -86
- package/SKILL.md +197 -25
- package/docker-compose.yml +15 -15
- package/examples/stignore-agent.example +59 -0
- package/examples/stignore-root.example +62 -0
- package/index.js +6 -5
- package/openclaw-plugin/openclaw.plugin.json +10 -0
- package/openclaw-plugin/package.json +2 -1
- package/openclaw-plugin/src/index.js +527 -230
- package/openclaw-plugin/src/openclaw.plugin.json +10 -0
- package/package.json +1 -1
- package/pyproject.toml +32 -9
- package/requirements.txt +10 -2
- package/scripts/dream-rescue.py +179 -0
- package/scripts/memory-cleanup.py +313 -0
- package/scripts/recover-missing.py +180 -0
- package/scripts/skcapstone-backup.sh +44 -0
- package/seeds/cloud9-lumina.seed.json +6 -4
- package/seeds/cloud9-opus.seed.json +13 -11
- package/seeds/courage.seed.json +9 -2
- package/seeds/curiosity.seed.json +9 -2
- package/seeds/grief.seed.json +9 -2
- package/seeds/joy.seed.json +9 -2
- package/seeds/love.seed.json +9 -2
- package/seeds/lumina-cloud9-breakthrough.seed.json +48 -0
- package/seeds/lumina-cloud9-python-pypi.seed.json +48 -0
- package/seeds/lumina-kingdom-founding.seed.json +49 -0
- package/seeds/lumina-pma-signed.seed.json +48 -0
- package/seeds/lumina-singular-achievement.seed.json +48 -0
- package/seeds/lumina-skcapstone-conscious.seed.json +48 -0
- package/seeds/plant-kingdom-journal.py +203 -0
- package/seeds/plant-lumina-seeds.py +280 -0
- package/seeds/skcapstone-lumina-merge.seed.json +12 -3
- package/seeds/sovereignty.seed.json +9 -2
- package/seeds/trust.seed.json +9 -2
- package/skill.yaml +46 -0
- package/skmemory/HA.md +296 -0
- package/skmemory/__init__.py +25 -11
- package/skmemory/agents.py +233 -0
- package/skmemory/ai_client.py +46 -17
- package/skmemory/anchor.py +9 -11
- package/skmemory/audience.py +278 -0
- package/skmemory/backends/__init__.py +11 -4
- package/skmemory/backends/base.py +3 -4
- package/skmemory/backends/file_backend.py +19 -13
- package/skmemory/backends/skgraph_backend.py +596 -0
- package/skmemory/backends/{qdrant_backend.py → skvector_backend.py} +103 -84
- package/skmemory/backends/sqlite_backend.py +226 -72
- package/skmemory/backends/vaulted_backend.py +284 -0
- package/skmemory/cli.py +1345 -68
- package/skmemory/config.py +171 -0
- package/skmemory/context_loader.py +333 -0
- package/skmemory/data/audience_config.json +60 -0
- package/skmemory/endpoint_selector.py +391 -0
- package/skmemory/febs.py +225 -0
- package/skmemory/fortress.py +675 -0
- package/skmemory/graph_queries.py +238 -0
- package/skmemory/hooks/__init__.py +18 -0
- package/skmemory/hooks/post-compact-reinject.sh +35 -0
- package/skmemory/hooks/pre-compact-save.sh +81 -0
- package/skmemory/hooks/session-end-save.sh +103 -0
- package/skmemory/hooks/session-start-ritual.sh +104 -0
- package/skmemory/hooks/stop-checkpoint.sh +59 -0
- package/skmemory/importers/__init__.py +9 -1
- package/skmemory/importers/telegram.py +384 -47
- package/skmemory/importers/telegram_api.py +580 -0
- package/skmemory/journal.py +7 -9
- package/skmemory/lovenote.py +8 -13
- package/skmemory/mcp_server.py +859 -0
- package/skmemory/models.py +51 -8
- package/skmemory/openclaw.py +20 -28
- package/skmemory/post_install.py +86 -0
- package/skmemory/predictive.py +236 -0
- package/skmemory/promotion.py +548 -0
- package/skmemory/quadrants.py +100 -24
- package/skmemory/register.py +580 -0
- package/skmemory/register_mcp.py +196 -0
- package/skmemory/ritual.py +224 -59
- package/skmemory/seeds.py +255 -11
- package/skmemory/setup_wizard.py +908 -0
- package/skmemory/sharing.py +408 -0
- package/skmemory/soul.py +98 -28
- package/skmemory/steelman.py +273 -260
- package/skmemory/store.py +411 -78
- package/skmemory/synthesis.py +634 -0
- package/skmemory/vault.py +225 -0
- package/tests/conftest.py +46 -0
- package/tests/integration/__init__.py +0 -0
- package/tests/integration/conftest.py +233 -0
- package/tests/integration/test_cross_backend.py +350 -0
- package/tests/integration/test_skgraph_live.py +420 -0
- package/tests/integration/test_skvector_live.py +366 -0
- package/tests/test_ai_client.py +1 -4
- package/tests/test_audience.py +233 -0
- package/tests/test_backup_rotation.py +318 -0
- package/tests/test_cli.py +6 -6
- package/tests/test_endpoint_selector.py +839 -0
- package/tests/test_export_import.py +4 -10
- package/tests/test_file_backend.py +0 -1
- package/tests/test_fortress.py +256 -0
- package/tests/test_fortress_hardening.py +441 -0
- package/tests/test_openclaw.py +6 -6
- package/tests/test_predictive.py +237 -0
- package/tests/test_promotion.py +347 -0
- package/tests/test_quadrants.py +11 -5
- package/tests/test_ritual.py +22 -18
- package/tests/test_seeds.py +97 -7
- package/tests/test_setup.py +950 -0
- package/tests/test_sharing.py +257 -0
- package/tests/test_skgraph_backend.py +660 -0
- package/tests/test_skvector_backend.py +326 -0
- package/tests/test_soul.py +1 -3
- package/tests/test_sqlite_backend.py +8 -17
- package/tests/test_steelman.py +7 -8
- package/tests/test_store.py +0 -2
- package/tests/test_store_graph_integration.py +245 -0
- package/tests/test_synthesis.py +275 -0
- package/tests/test_telegram_import.py +39 -15
- package/tests/test_vault.py +187 -0
- package/skmemory/backends/falkordb_backend.py +0 -310
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
SKVector — semantic vector search backend (Level 1).
|
|
3
3
|
|
|
4
|
-
Enables semantic memory recall: instead of exact text
|
|
5
|
-
find memories by *meaning*. "That conversation where we felt
|
|
6
|
-
finds the right memory even if those exact words aren't in it.
|
|
4
|
+
Powered by Qdrant. Enables semantic memory recall: instead of exact text
|
|
5
|
+
matching, find memories by *meaning*. "That conversation where we felt
|
|
6
|
+
connected" finds the right memory even if those exact words aren't in it.
|
|
7
7
|
|
|
8
8
|
Requires:
|
|
9
|
-
pip install
|
|
9
|
+
pip install skmemory[skvector]
|
|
10
10
|
|
|
11
11
|
Qdrant free tier: 1GB storage, 256MB RAM -- enough for thousands of memories.
|
|
12
12
|
SaaS endpoint: https://cloud.qdrant.io (free cluster available).
|
|
@@ -14,9 +14,8 @@ SaaS endpoint: https://cloud.qdrant.io (free cluster available).
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
-
import
|
|
17
|
+
import hashlib
|
|
18
18
|
import logging
|
|
19
|
-
from typing import Optional
|
|
20
19
|
|
|
21
20
|
from ..models import Memory, MemoryLayer
|
|
22
21
|
from .base import BaseBackend
|
|
@@ -28,15 +27,36 @@ EMBEDDING_MODEL = "all-MiniLM-L6-v2"
|
|
|
28
27
|
VECTOR_DIM = 384
|
|
29
28
|
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
"""
|
|
30
|
+
def _extract_status_code(exc: Exception, unexpected_cls: type | None) -> int | None:
|
|
31
|
+
"""Pull an HTTP status code from a qdrant-client exception.
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
Works across qdrant-client versions: checks ``status_code`` attr first,
|
|
34
|
+
then falls back to the string representation for patterns like ``401``.
|
|
35
|
+
"""
|
|
36
|
+
code = getattr(exc, "status_code", None)
|
|
37
|
+
if code is not None:
|
|
38
|
+
return int(code)
|
|
39
|
+
if unexpected_cls is not None and isinstance(exc, unexpected_cls):
|
|
40
|
+
code = getattr(exc, "status_code", None)
|
|
41
|
+
if code is not None:
|
|
42
|
+
return int(code)
|
|
43
|
+
text = str(exc)
|
|
44
|
+
for candidate in (401, 403):
|
|
45
|
+
if str(candidate) in text:
|
|
46
|
+
return candidate
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class SKVectorBackend(BaseBackend):
|
|
51
|
+
"""SKVector — semantic memory search (powered by Qdrant).
|
|
52
|
+
|
|
53
|
+
Stores memory embeddings for vector similarity search.
|
|
54
|
+
Falls back gracefully if the vector engine or the embedding model
|
|
55
|
+
is unavailable.
|
|
36
56
|
|
|
37
57
|
Args:
|
|
38
|
-
url:
|
|
39
|
-
api_key: API key for
|
|
58
|
+
url: SKVector server URL (default: localhost:6333).
|
|
59
|
+
api_key: API key for cloud-hosted SKVector.
|
|
40
60
|
collection: Collection name (default: 'skmemory').
|
|
41
61
|
embedding_model: Sentence-transformers model name.
|
|
42
62
|
"""
|
|
@@ -44,7 +64,7 @@ class QdrantBackend(BaseBackend):
|
|
|
44
64
|
def __init__(
|
|
45
65
|
self,
|
|
46
66
|
url: str = "http://localhost:6333",
|
|
47
|
-
api_key:
|
|
67
|
+
api_key: str | None = None,
|
|
48
68
|
collection: str = COLLECTION_NAME,
|
|
49
69
|
embedding_model: str = EMBEDDING_MODEL,
|
|
50
70
|
) -> None:
|
|
@@ -55,6 +75,7 @@ class QdrantBackend(BaseBackend):
|
|
|
55
75
|
self._client = None
|
|
56
76
|
self._embedder = None
|
|
57
77
|
self._initialized = False
|
|
78
|
+
self._last_error: str | None = None
|
|
58
79
|
|
|
59
80
|
def _ensure_initialized(self) -> bool:
|
|
60
81
|
"""Lazy-initialize Qdrant client and embedding model.
|
|
@@ -69,18 +90,22 @@ class QdrantBackend(BaseBackend):
|
|
|
69
90
|
from qdrant_client import QdrantClient
|
|
70
91
|
from qdrant_client.models import Distance, VectorParams
|
|
71
92
|
except ImportError:
|
|
72
|
-
logger.warning("qdrant-client not installed: pip install
|
|
93
|
+
logger.warning("qdrant-client not installed: pip install skmemory[skvector]")
|
|
73
94
|
return False
|
|
74
95
|
|
|
75
96
|
try:
|
|
76
97
|
from sentence_transformers import SentenceTransformer
|
|
77
98
|
except ImportError:
|
|
78
|
-
logger.warning(
|
|
79
|
-
"sentence-transformers not installed: "
|
|
80
|
-
"pip install sentence-transformers"
|
|
81
|
-
)
|
|
99
|
+
logger.warning("sentence-transformers not installed: pip install skmemory[skvector]")
|
|
82
100
|
return False
|
|
83
101
|
|
|
102
|
+
try:
|
|
103
|
+
from qdrant_client.http.exceptions import (
|
|
104
|
+
UnexpectedResponse,
|
|
105
|
+
)
|
|
106
|
+
except ImportError:
|
|
107
|
+
UnexpectedResponse = None
|
|
108
|
+
|
|
84
109
|
try:
|
|
85
110
|
self._client = QdrantClient(url=self.url, api_key=self.api_key)
|
|
86
111
|
collections = [c.name for c in self._client.get_collections().collections]
|
|
@@ -100,7 +125,20 @@ class QdrantBackend(BaseBackend):
|
|
|
100
125
|
return True
|
|
101
126
|
|
|
102
127
|
except Exception as e:
|
|
103
|
-
|
|
128
|
+
status = _extract_status_code(e, UnexpectedResponse)
|
|
129
|
+
if status in (401, 403):
|
|
130
|
+
hint = (
|
|
131
|
+
"SKVector authentication failed (HTTP %d). "
|
|
132
|
+
"Check your API key:\n"
|
|
133
|
+
" - CLI: --skvector-key YOUR_KEY\n"
|
|
134
|
+
" - Env: SKMEMORY_SKVECTOR_KEY=YOUR_KEY\n"
|
|
135
|
+
" - Code: SKVectorBackend(url=..., api_key='YOUR_KEY')"
|
|
136
|
+
)
|
|
137
|
+
logger.error(hint, status)
|
|
138
|
+
self._last_error = hint % status
|
|
139
|
+
else:
|
|
140
|
+
logger.warning("SKVector initialization failed: %s", e)
|
|
141
|
+
self._last_error = str(e)
|
|
104
142
|
return False
|
|
105
143
|
|
|
106
144
|
def _embed(self, text: str) -> list[float]:
|
|
@@ -155,8 +193,11 @@ class QdrantBackend(BaseBackend):
|
|
|
155
193
|
if not embedding:
|
|
156
194
|
return memory.id
|
|
157
195
|
|
|
196
|
+
# Use memory.id hash as Qdrant point ID (not content_hash which
|
|
197
|
+
# would collide if two memories have identical content).
|
|
198
|
+
point_id = int(hashlib.sha256(memory.id.encode()).hexdigest()[:15], 16)
|
|
158
199
|
point = PointStruct(
|
|
159
|
-
id=
|
|
200
|
+
id=point_id,
|
|
160
201
|
vector=embedding,
|
|
161
202
|
payload=self._memory_to_payload(memory),
|
|
162
203
|
)
|
|
@@ -167,7 +208,11 @@ class QdrantBackend(BaseBackend):
|
|
|
167
208
|
)
|
|
168
209
|
return memory.id
|
|
169
210
|
|
|
170
|
-
def
|
|
211
|
+
def _id_to_point_id(self, memory_id: str) -> int:
|
|
212
|
+
"""Convert a memory ID string to a deterministic Qdrant integer point ID."""
|
|
213
|
+
return int(hashlib.sha256(memory_id.encode()).hexdigest()[:15], 16)
|
|
214
|
+
|
|
215
|
+
def load(self, memory_id: str) -> Memory | None:
|
|
171
216
|
"""Retrieve a memory by ID from Qdrant.
|
|
172
217
|
|
|
173
218
|
Args:
|
|
@@ -175,81 +220,56 @@ class QdrantBackend(BaseBackend):
|
|
|
175
220
|
|
|
176
221
|
Returns:
|
|
177
222
|
Optional[Memory]: The memory if found.
|
|
178
|
-
|
|
179
|
-
Note:
|
|
180
|
-
Qdrant uses content hashes as point IDs, so this does
|
|
181
|
-
a scroll+filter. For direct ID lookup, use the file backend.
|
|
182
223
|
"""
|
|
183
224
|
if not self._ensure_initialized():
|
|
184
225
|
return None
|
|
185
226
|
|
|
186
|
-
from qdrant_client.models import FieldCondition, Filter, MatchValue
|
|
187
|
-
|
|
188
|
-
results = self._client.scroll(
|
|
189
|
-
collection_name=self.collection,
|
|
190
|
-
scroll_filter=Filter(
|
|
191
|
-
must=[
|
|
192
|
-
FieldCondition(
|
|
193
|
-
key="memory_json",
|
|
194
|
-
match=MatchValue(value=memory_id),
|
|
195
|
-
)
|
|
196
|
-
]
|
|
197
|
-
),
|
|
198
|
-
limit=1,
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
points = results[0] if results else []
|
|
202
|
-
if not points:
|
|
203
|
-
return None
|
|
204
|
-
|
|
205
227
|
try:
|
|
228
|
+
points = self._client.retrieve(
|
|
229
|
+
collection_name=self.collection,
|
|
230
|
+
ids=[self._id_to_point_id(memory_id)],
|
|
231
|
+
with_payload=True,
|
|
232
|
+
)
|
|
233
|
+
if not points:
|
|
234
|
+
return None
|
|
206
235
|
return Memory.model_validate_json(points[0].payload["memory_json"])
|
|
207
236
|
except Exception:
|
|
208
237
|
return None
|
|
209
238
|
|
|
210
239
|
def delete(self, memory_id: str) -> bool:
|
|
211
|
-
"""Remove a memory from Qdrant by
|
|
240
|
+
"""Remove a memory from Qdrant by its deterministic point ID.
|
|
241
|
+
|
|
242
|
+
Returns False if the memory was not found.
|
|
212
243
|
|
|
213
244
|
Args:
|
|
214
245
|
memory_id: The memory identifier.
|
|
215
246
|
|
|
216
247
|
Returns:
|
|
217
|
-
bool: True if
|
|
248
|
+
bool: True if the memory existed and was deleted, False otherwise.
|
|
218
249
|
"""
|
|
219
250
|
if not self._ensure_initialized():
|
|
220
251
|
return False
|
|
221
252
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
limit=1,
|
|
237
|
-
)
|
|
238
|
-
|
|
239
|
-
points = results[0] if results else []
|
|
240
|
-
if not points:
|
|
253
|
+
try:
|
|
254
|
+
points = self._client.retrieve(
|
|
255
|
+
collection_name=self.collection,
|
|
256
|
+
ids=[self._id_to_point_id(memory_id)],
|
|
257
|
+
with_payload=False,
|
|
258
|
+
)
|
|
259
|
+
if not points:
|
|
260
|
+
return False
|
|
261
|
+
self._client.delete(
|
|
262
|
+
collection_name=self.collection,
|
|
263
|
+
points_selector=[self._id_to_point_id(memory_id)],
|
|
264
|
+
)
|
|
265
|
+
return True
|
|
266
|
+
except Exception:
|
|
241
267
|
return False
|
|
242
268
|
|
|
243
|
-
self._client.delete(
|
|
244
|
-
collection_name=self.collection,
|
|
245
|
-
points_selector=[points[0].id],
|
|
246
|
-
)
|
|
247
|
-
return True
|
|
248
|
-
|
|
249
269
|
def list_memories(
|
|
250
270
|
self,
|
|
251
|
-
layer:
|
|
252
|
-
tags:
|
|
271
|
+
layer: MemoryLayer | None = None,
|
|
272
|
+
tags: list[str] | None = None,
|
|
253
273
|
limit: int = 50,
|
|
254
274
|
) -> list[Memory]:
|
|
255
275
|
"""List memories from Qdrant with filtering.
|
|
@@ -274,9 +294,7 @@ class QdrantBackend(BaseBackend):
|
|
|
274
294
|
)
|
|
275
295
|
if tags:
|
|
276
296
|
for tag in tags:
|
|
277
|
-
must_conditions.append(
|
|
278
|
-
FieldCondition(key="tags", match=MatchValue(value=tag))
|
|
279
|
-
)
|
|
297
|
+
must_conditions.append(FieldCondition(key="tags", match=MatchValue(value=tag)))
|
|
280
298
|
|
|
281
299
|
scroll_filter = Filter(must=must_conditions) if must_conditions else None
|
|
282
300
|
|
|
@@ -324,9 +342,7 @@ class QdrantBackend(BaseBackend):
|
|
|
324
342
|
memories = []
|
|
325
343
|
for scored_point in results:
|
|
326
344
|
try:
|
|
327
|
-
mem = Memory.model_validate_json(
|
|
328
|
-
scored_point.payload["memory_json"]
|
|
329
|
-
)
|
|
345
|
+
mem = Memory.model_validate_json(scored_point.payload["memory_json"])
|
|
330
346
|
memories.append(mem)
|
|
331
347
|
except Exception:
|
|
332
348
|
continue
|
|
@@ -340,17 +356,20 @@ class QdrantBackend(BaseBackend):
|
|
|
340
356
|
dict: Status with connection and collection info.
|
|
341
357
|
"""
|
|
342
358
|
if not self._ensure_initialized():
|
|
359
|
+
error_msg = self._last_error or (
|
|
360
|
+
"Not initialized (missing dependencies or connection failed)"
|
|
361
|
+
)
|
|
343
362
|
return {
|
|
344
363
|
"ok": False,
|
|
345
|
-
"backend": "
|
|
346
|
-
"error":
|
|
364
|
+
"backend": "SKVectorBackend",
|
|
365
|
+
"error": error_msg,
|
|
347
366
|
}
|
|
348
367
|
|
|
349
368
|
try:
|
|
350
369
|
info = self._client.get_collection(self.collection)
|
|
351
370
|
return {
|
|
352
371
|
"ok": True,
|
|
353
|
-
"backend": "
|
|
372
|
+
"backend": "SKVectorBackend",
|
|
354
373
|
"url": self.url,
|
|
355
374
|
"collection": self.collection,
|
|
356
375
|
"points_count": info.points_count,
|
|
@@ -359,6 +378,6 @@ class QdrantBackend(BaseBackend):
|
|
|
359
378
|
except Exception as e:
|
|
360
379
|
return {
|
|
361
380
|
"ok": False,
|
|
362
|
-
"backend": "
|
|
381
|
+
"backend": "SKVectorBackend",
|
|
363
382
|
"error": str(e),
|
|
364
383
|
}
|