@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,228 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Memory Vault — at-rest encryption for sovereign memories.
|
|
3
|
+
|
|
4
|
+
Encrypts memory JSON files using AES-256-GCM with keys derived from
|
|
5
|
+
the agent's passphrase or CapAuth PGP key. Each memory file gets
|
|
6
|
+
its own random nonce, so identical content produces different ciphertext.
|
|
7
|
+
|
|
8
|
+
Quantum-resistant note: AES-256 requires Grover's algorithm to attack,
|
|
9
|
+
which only reduces effective security from 256 to 128 bits. That's
|
|
10
|
+
still computationally infeasible for the foreseeable future.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
vault = MemoryVault(passphrase="YOUR_PASSPHRASE_HERE")
|
|
14
|
+
encrypted = vault.encrypt(memory_json_bytes)
|
|
15
|
+
decrypted = vault.decrypt(encrypted)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import base64
|
|
21
|
+
import hashlib
|
|
22
|
+
import json
|
|
23
|
+
import logging
|
|
24
|
+
import os
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Optional
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger("skmemory.vault")
|
|
29
|
+
|
|
30
|
+
VAULT_HEADER = b"SKMV1"
|
|
31
|
+
NONCE_SIZE = 12
|
|
32
|
+
TAG_SIZE = 16
|
|
33
|
+
KEY_SIZE = 32
|
|
34
|
+
SALT_SIZE = 16
|
|
35
|
+
KDF_ITERATIONS = 600_000
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _derive_key(passphrase: str, salt: bytes) -> bytes:
|
|
39
|
+
"""Derive a 256-bit AES key from a passphrase using PBKDF2.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
passphrase: The encryption passphrase.
|
|
43
|
+
salt: 16-byte random salt.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
32-byte AES key.
|
|
47
|
+
"""
|
|
48
|
+
return hashlib.pbkdf2_hmac(
|
|
49
|
+
"sha256",
|
|
50
|
+
passphrase.encode("utf-8"),
|
|
51
|
+
salt,
|
|
52
|
+
KDF_ITERATIONS,
|
|
53
|
+
dklen=KEY_SIZE,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class MemoryVault:
|
|
58
|
+
"""Encrypt and decrypt memory files at rest.
|
|
59
|
+
|
|
60
|
+
Uses AES-256-GCM for authenticated encryption. Each encrypt()
|
|
61
|
+
call generates a fresh random nonce and salt, so the same
|
|
62
|
+
plaintext never produces the same ciphertext.
|
|
63
|
+
|
|
64
|
+
File format:
|
|
65
|
+
SKMV1 || salt(16) || nonce(12) || ciphertext || tag(16)
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
passphrase: Secret used to derive the encryption key.
|
|
69
|
+
Can be the agent's CapAuth passphrase or a dedicated vault key.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, passphrase: str) -> None:
|
|
73
|
+
self._passphrase = passphrase
|
|
74
|
+
|
|
75
|
+
def encrypt(self, plaintext: bytes) -> bytes:
|
|
76
|
+
"""Encrypt data with AES-256-GCM.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
plaintext: Raw bytes to encrypt (typically memory JSON).
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Encrypted bytes with header, salt, nonce, ciphertext, and tag.
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
ImportError: If cryptography package is not installed.
|
|
86
|
+
"""
|
|
87
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
88
|
+
|
|
89
|
+
salt = os.urandom(SALT_SIZE)
|
|
90
|
+
nonce = os.urandom(NONCE_SIZE)
|
|
91
|
+
key = _derive_key(self._passphrase, salt)
|
|
92
|
+
|
|
93
|
+
aesgcm = AESGCM(key)
|
|
94
|
+
ciphertext = aesgcm.encrypt(nonce, plaintext, VAULT_HEADER)
|
|
95
|
+
|
|
96
|
+
return VAULT_HEADER + salt + nonce + ciphertext
|
|
97
|
+
|
|
98
|
+
def decrypt(self, encrypted: bytes) -> bytes:
|
|
99
|
+
"""Decrypt data encrypted with encrypt().
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
encrypted: Bytes from encrypt() — header + salt + nonce + ciphertext + tag.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Decrypted plaintext bytes.
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
ValueError: If the header doesn't match or decryption fails.
|
|
109
|
+
ImportError: If cryptography package is not installed.
|
|
110
|
+
"""
|
|
111
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
112
|
+
|
|
113
|
+
header_len = len(VAULT_HEADER)
|
|
114
|
+
if encrypted[:header_len] != VAULT_HEADER:
|
|
115
|
+
raise ValueError("Not an SKMemory vault file (bad header)")
|
|
116
|
+
|
|
117
|
+
offset = header_len
|
|
118
|
+
salt = encrypted[offset : offset + SALT_SIZE]
|
|
119
|
+
offset += SALT_SIZE
|
|
120
|
+
nonce = encrypted[offset : offset + NONCE_SIZE]
|
|
121
|
+
offset += NONCE_SIZE
|
|
122
|
+
ciphertext = encrypted[offset:]
|
|
123
|
+
|
|
124
|
+
key = _derive_key(self._passphrase, salt)
|
|
125
|
+
aesgcm = AESGCM(key)
|
|
126
|
+
|
|
127
|
+
return aesgcm.decrypt(nonce, ciphertext, VAULT_HEADER)
|
|
128
|
+
|
|
129
|
+
def encrypt_file(self, path: Path) -> Path:
|
|
130
|
+
"""Encrypt a file in-place, adding .vault extension.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
path: Path to the plaintext file.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Path to the encrypted file.
|
|
137
|
+
"""
|
|
138
|
+
plaintext = path.read_bytes()
|
|
139
|
+
encrypted = self.encrypt(plaintext)
|
|
140
|
+
vault_path = path.with_suffix(path.suffix + ".vault")
|
|
141
|
+
vault_path.write_bytes(encrypted)
|
|
142
|
+
path.unlink()
|
|
143
|
+
return vault_path
|
|
144
|
+
|
|
145
|
+
def decrypt_file(self, path: Path) -> Path:
|
|
146
|
+
"""Decrypt a .vault file, restoring the original.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
path: Path to the encrypted .vault file.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Path to the decrypted file.
|
|
153
|
+
"""
|
|
154
|
+
encrypted = path.read_bytes()
|
|
155
|
+
plaintext = self.decrypt(encrypted)
|
|
156
|
+
original_path = Path(str(path).replace(".vault", ""))
|
|
157
|
+
original_path.write_bytes(plaintext)
|
|
158
|
+
path.unlink()
|
|
159
|
+
return original_path
|
|
160
|
+
|
|
161
|
+
def is_encrypted(self, path: Path) -> bool:
|
|
162
|
+
"""Check if a file is vault-encrypted.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
path: Path to check.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
True if the file has a valid vault header.
|
|
169
|
+
"""
|
|
170
|
+
try:
|
|
171
|
+
data = path.read_bytes()
|
|
172
|
+
return data[:len(VAULT_HEADER)] == VAULT_HEADER
|
|
173
|
+
except OSError:
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def encrypt_memory_store(
|
|
178
|
+
memory_dir: Path,
|
|
179
|
+
passphrase: str,
|
|
180
|
+
) -> int:
|
|
181
|
+
"""Encrypt all memory JSON files in a directory tree.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
memory_dir: Root memory directory (e.g., ~/.skcapstone/memories/).
|
|
185
|
+
passphrase: Encryption passphrase.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Number of files encrypted.
|
|
189
|
+
"""
|
|
190
|
+
vault = MemoryVault(passphrase)
|
|
191
|
+
count = 0
|
|
192
|
+
|
|
193
|
+
for json_file in memory_dir.rglob("*.json"):
|
|
194
|
+
if json_file.suffix == ".vault":
|
|
195
|
+
continue
|
|
196
|
+
try:
|
|
197
|
+
vault.encrypt_file(json_file)
|
|
198
|
+
count += 1
|
|
199
|
+
except Exception as exc:
|
|
200
|
+
logger.warning("Failed to encrypt %s: %s", json_file, exc)
|
|
201
|
+
|
|
202
|
+
return count
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def decrypt_memory_store(
|
|
206
|
+
memory_dir: Path,
|
|
207
|
+
passphrase: str,
|
|
208
|
+
) -> int:
|
|
209
|
+
"""Decrypt all vault files in a directory tree.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
memory_dir: Root memory directory.
|
|
213
|
+
passphrase: Decryption passphrase.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Number of files decrypted.
|
|
217
|
+
"""
|
|
218
|
+
vault = MemoryVault(passphrase)
|
|
219
|
+
count = 0
|
|
220
|
+
|
|
221
|
+
for vault_file in memory_dir.rglob("*.vault"):
|
|
222
|
+
try:
|
|
223
|
+
vault.decrypt_file(vault_file)
|
|
224
|
+
count += 1
|
|
225
|
+
except Exception as exc:
|
|
226
|
+
logger.warning("Failed to decrypt %s: %s", vault_file, exc)
|
|
227
|
+
|
|
228
|
+
return count
|
|
File without changes
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Integration test fixtures for SKMemory live backends.
|
|
3
|
+
|
|
4
|
+
Fixtures skip automatically when SKGraph or SKVector are unreachable.
|
|
5
|
+
Set env vars to point at non-default endpoints:
|
|
6
|
+
|
|
7
|
+
SKMEMORY_SKGRAPH_URL=redis://localhost:6379
|
|
8
|
+
SKMEMORY_SKVECTOR_URL=http://localhost:6333
|
|
9
|
+
|
|
10
|
+
A dedicated test graph and collection are used so production data is
|
|
11
|
+
never touched. Both are torn down after the test session.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import uuid
|
|
18
|
+
|
|
19
|
+
import pytest
|
|
20
|
+
|
|
21
|
+
# ─────────────────────────────────────────────────────────
|
|
22
|
+
# Connection constants
|
|
23
|
+
# ─────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
SKGRAPH_URL = os.environ.get("SKMEMORY_SKGRAPH_URL", "redis://localhost:6379")
|
|
26
|
+
SKVECTOR_URL = os.environ.get("SKMEMORY_SKVECTOR_URL", "http://localhost:6333")
|
|
27
|
+
SKVECTOR_KEY = os.environ.get("SKMEMORY_SKVECTOR_KEY")
|
|
28
|
+
|
|
29
|
+
# Isolated names so tests never collide with production data
|
|
30
|
+
TEST_GRAPH_NAME = "skmemory_integration_test"
|
|
31
|
+
TEST_COLLECTION_NAME = "skmemory_integration_test"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ─────────────────────────────────────────────────────────
|
|
35
|
+
# Availability checks
|
|
36
|
+
# ─────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _skgraph_available() -> bool:
|
|
40
|
+
"""Return True if a SKGraph (FalkorDB/Redis) server is reachable."""
|
|
41
|
+
try:
|
|
42
|
+
from falkordb import FalkorDB # type: ignore[import]
|
|
43
|
+
except ImportError:
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
db = FalkorDB.from_url(SKGRAPH_URL)
|
|
48
|
+
g = db.select_graph("__ping__")
|
|
49
|
+
g.query("RETURN 1")
|
|
50
|
+
return True
|
|
51
|
+
except Exception:
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _skvector_available() -> bool:
|
|
56
|
+
"""Return True if SKVector (Qdrant) is reachable and qdrant-client is installed."""
|
|
57
|
+
try:
|
|
58
|
+
from qdrant_client import QdrantClient # type: ignore[import]
|
|
59
|
+
except ImportError:
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
client = QdrantClient(url=SKVECTOR_URL, api_key=SKVECTOR_KEY)
|
|
64
|
+
client.get_collections()
|
|
65
|
+
return True
|
|
66
|
+
except Exception:
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _sentence_transformers_available() -> bool:
|
|
71
|
+
try:
|
|
72
|
+
import sentence_transformers # noqa: F401 # type: ignore[import]
|
|
73
|
+
return True
|
|
74
|
+
except ImportError:
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
SKGRAPH_AVAILABLE = _skgraph_available()
|
|
79
|
+
SKVECTOR_AVAILABLE = _skvector_available()
|
|
80
|
+
SENTENCE_TRANSFORMERS_AVAILABLE = _sentence_transformers_available()
|
|
81
|
+
|
|
82
|
+
# Composite flag: SKVector tests also require the embedding model
|
|
83
|
+
SKVECTOR_FULL_AVAILABLE = SKVECTOR_AVAILABLE and SENTENCE_TRANSFORMERS_AVAILABLE
|
|
84
|
+
|
|
85
|
+
requires_skgraph = pytest.mark.skipif(
|
|
86
|
+
not SKGRAPH_AVAILABLE,
|
|
87
|
+
reason="SKGraph unreachable (set SKMEMORY_SKGRAPH_URL or start Redis+FalkorDB)",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
requires_skvector = pytest.mark.skipif(
|
|
91
|
+
not SKVECTOR_FULL_AVAILABLE,
|
|
92
|
+
reason=(
|
|
93
|
+
"SKVector unreachable or sentence-transformers not installed "
|
|
94
|
+
"(set SKMEMORY_SKVECTOR_URL or pip install qdrant-client sentence-transformers)"
|
|
95
|
+
),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
requires_both = pytest.mark.skipif(
|
|
99
|
+
not (SKGRAPH_AVAILABLE and SKVECTOR_FULL_AVAILABLE),
|
|
100
|
+
reason="Both SKGraph and SKVector must be reachable for cross-backend tests",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ─────────────────────────────────────────────────────────
|
|
105
|
+
# SKGraph fixtures
|
|
106
|
+
# ─────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@pytest.fixture(scope="session")
|
|
110
|
+
def falkordb_backend():
|
|
111
|
+
"""Live SKGraphBackend pointed at the test graph.
|
|
112
|
+
|
|
113
|
+
The test graph is deleted at session teardown.
|
|
114
|
+
"""
|
|
115
|
+
pytest.importorskip("falkordb", reason="falkordb not installed")
|
|
116
|
+
if not SKGRAPH_AVAILABLE:
|
|
117
|
+
pytest.skip("SKGraph unreachable")
|
|
118
|
+
|
|
119
|
+
from skmemory.backends.skgraph_backend import SKGraphBackend
|
|
120
|
+
|
|
121
|
+
backend = SKGraphBackend(url=SKGRAPH_URL, graph_name=TEST_GRAPH_NAME)
|
|
122
|
+
assert backend._ensure_initialized(), "SKGraph backend failed to initialize"
|
|
123
|
+
|
|
124
|
+
yield backend
|
|
125
|
+
|
|
126
|
+
# Teardown: drop the test graph
|
|
127
|
+
try:
|
|
128
|
+
from falkordb import FalkorDB # type: ignore[import]
|
|
129
|
+
db = FalkorDB.from_url(SKGRAPH_URL)
|
|
130
|
+
db.select_graph(TEST_GRAPH_NAME).delete()
|
|
131
|
+
except Exception:
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@pytest.fixture
|
|
136
|
+
def falkordb_clean(falkordb_backend):
|
|
137
|
+
"""SKGraphBackend with test graph wiped before each test."""
|
|
138
|
+
# Clear all nodes so tests are independent
|
|
139
|
+
try:
|
|
140
|
+
falkordb_backend._graph.query("MATCH (n) DETACH DELETE n")
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
143
|
+
return falkordb_backend
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ─────────────────────────────────────────────────────────
|
|
147
|
+
# SKVector fixtures
|
|
148
|
+
# ─────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@pytest.fixture(scope="session")
|
|
152
|
+
def qdrant_backend():
|
|
153
|
+
"""Live SKVectorBackend pointed at the test collection.
|
|
154
|
+
|
|
155
|
+
The test collection is deleted at session teardown.
|
|
156
|
+
"""
|
|
157
|
+
pytest.importorskip("qdrant_client", reason="qdrant-client not installed")
|
|
158
|
+
pytest.importorskip("sentence_transformers", reason="sentence-transformers not installed")
|
|
159
|
+
if not SKVECTOR_AVAILABLE:
|
|
160
|
+
pytest.skip("SKVector unreachable")
|
|
161
|
+
|
|
162
|
+
from skmemory.backends.skvector_backend import SKVectorBackend
|
|
163
|
+
|
|
164
|
+
backend = SKVectorBackend(
|
|
165
|
+
url=SKVECTOR_URL,
|
|
166
|
+
api_key=SKVECTOR_KEY,
|
|
167
|
+
collection=TEST_COLLECTION_NAME,
|
|
168
|
+
)
|
|
169
|
+
assert backend._ensure_initialized(), "SKVector backend failed to initialize"
|
|
170
|
+
|
|
171
|
+
yield backend
|
|
172
|
+
|
|
173
|
+
# Teardown: delete test collection
|
|
174
|
+
try:
|
|
175
|
+
backend._client.delete_collection(TEST_COLLECTION_NAME)
|
|
176
|
+
except Exception:
|
|
177
|
+
pass
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@pytest.fixture
|
|
181
|
+
def qdrant_clean(qdrant_backend):
|
|
182
|
+
"""SKVectorBackend with collection wiped before each test."""
|
|
183
|
+
try:
|
|
184
|
+
from qdrant_client.models import Distance, VectorParams
|
|
185
|
+
from skmemory.backends.skvector_backend import VECTOR_DIM
|
|
186
|
+
|
|
187
|
+
qdrant_backend._client.delete_collection(TEST_COLLECTION_NAME)
|
|
188
|
+
qdrant_backend._client.create_collection(
|
|
189
|
+
collection_name=TEST_COLLECTION_NAME,
|
|
190
|
+
vectors_config=VectorParams(size=VECTOR_DIM, distance=Distance.COSINE),
|
|
191
|
+
)
|
|
192
|
+
except Exception:
|
|
193
|
+
pass
|
|
194
|
+
return qdrant_backend
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ─────────────────────────────────────────────────────────
|
|
198
|
+
# Shared memory factory
|
|
199
|
+
# ─────────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def make_memory(
|
|
203
|
+
title: str = "Test Memory",
|
|
204
|
+
content: str = "Integration test content.",
|
|
205
|
+
tags: list[str] | None = None,
|
|
206
|
+
source: str = "integration-test",
|
|
207
|
+
layer: str = "short-term",
|
|
208
|
+
intensity: float = 5.0,
|
|
209
|
+
valence: float = 0.5,
|
|
210
|
+
emotional_labels: list[str] | None = None,
|
|
211
|
+
parent_id: str | None = None,
|
|
212
|
+
related_ids: list[str] | None = None,
|
|
213
|
+
):
|
|
214
|
+
"""Factory for Memory objects in integration tests."""
|
|
215
|
+
from skmemory.models import EmotionalSnapshot, Memory, MemoryLayer
|
|
216
|
+
|
|
217
|
+
layer_enum = MemoryLayer(layer)
|
|
218
|
+
emotional = EmotionalSnapshot(
|
|
219
|
+
intensity=intensity,
|
|
220
|
+
valence=valence,
|
|
221
|
+
labels=emotional_labels or [],
|
|
222
|
+
)
|
|
223
|
+
return Memory(
|
|
224
|
+
id=str(uuid.uuid4()),
|
|
225
|
+
title=title,
|
|
226
|
+
content=content,
|
|
227
|
+
tags=tags or [],
|
|
228
|
+
source=source,
|
|
229
|
+
layer=layer_enum,
|
|
230
|
+
emotional=emotional,
|
|
231
|
+
parent_id=parent_id,
|
|
232
|
+
related_ids=related_ids or [],
|
|
233
|
+
)
|