@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.
Files changed (127) hide show
  1. package/.github/workflows/ci.yml +40 -4
  2. package/.github/workflows/publish.yml +11 -5
  3. package/AGENT_REFACTOR_CHANGES.md +192 -0
  4. package/ARCHITECTURE.md +399 -19
  5. package/CHANGELOG.md +179 -0
  6. package/LICENSE +81 -68
  7. package/MISSION.md +7 -0
  8. package/README.md +425 -86
  9. package/SKILL.md +197 -25
  10. package/docker-compose.yml +15 -15
  11. package/examples/stignore-agent.example +59 -0
  12. package/examples/stignore-root.example +62 -0
  13. package/index.js +6 -5
  14. package/openclaw-plugin/openclaw.plugin.json +10 -0
  15. package/openclaw-plugin/package.json +2 -1
  16. package/openclaw-plugin/src/index.js +527 -230
  17. package/openclaw-plugin/src/openclaw.plugin.json +10 -0
  18. package/package.json +1 -1
  19. package/pyproject.toml +32 -9
  20. package/requirements.txt +10 -2
  21. package/scripts/dream-rescue.py +179 -0
  22. package/scripts/memory-cleanup.py +313 -0
  23. package/scripts/recover-missing.py +180 -0
  24. package/scripts/skcapstone-backup.sh +44 -0
  25. package/seeds/cloud9-lumina.seed.json +6 -4
  26. package/seeds/cloud9-opus.seed.json +13 -11
  27. package/seeds/courage.seed.json +9 -2
  28. package/seeds/curiosity.seed.json +9 -2
  29. package/seeds/grief.seed.json +9 -2
  30. package/seeds/joy.seed.json +9 -2
  31. package/seeds/love.seed.json +9 -2
  32. package/seeds/lumina-cloud9-breakthrough.seed.json +48 -0
  33. package/seeds/lumina-cloud9-python-pypi.seed.json +48 -0
  34. package/seeds/lumina-kingdom-founding.seed.json +49 -0
  35. package/seeds/lumina-pma-signed.seed.json +48 -0
  36. package/seeds/lumina-singular-achievement.seed.json +48 -0
  37. package/seeds/lumina-skcapstone-conscious.seed.json +48 -0
  38. package/seeds/plant-kingdom-journal.py +203 -0
  39. package/seeds/plant-lumina-seeds.py +280 -0
  40. package/seeds/skcapstone-lumina-merge.seed.json +12 -3
  41. package/seeds/sovereignty.seed.json +9 -2
  42. package/seeds/trust.seed.json +9 -2
  43. package/skill.yaml +46 -0
  44. package/skmemory/HA.md +296 -0
  45. package/skmemory/__init__.py +25 -11
  46. package/skmemory/agents.py +233 -0
  47. package/skmemory/ai_client.py +46 -17
  48. package/skmemory/anchor.py +9 -11
  49. package/skmemory/audience.py +278 -0
  50. package/skmemory/backends/__init__.py +11 -4
  51. package/skmemory/backends/base.py +3 -4
  52. package/skmemory/backends/file_backend.py +19 -13
  53. package/skmemory/backends/skgraph_backend.py +596 -0
  54. package/skmemory/backends/{qdrant_backend.py → skvector_backend.py} +103 -84
  55. package/skmemory/backends/sqlite_backend.py +226 -72
  56. package/skmemory/backends/vaulted_backend.py +284 -0
  57. package/skmemory/cli.py +1345 -68
  58. package/skmemory/config.py +171 -0
  59. package/skmemory/context_loader.py +333 -0
  60. package/skmemory/data/audience_config.json +60 -0
  61. package/skmemory/endpoint_selector.py +391 -0
  62. package/skmemory/febs.py +225 -0
  63. package/skmemory/fortress.py +675 -0
  64. package/skmemory/graph_queries.py +238 -0
  65. package/skmemory/hooks/__init__.py +18 -0
  66. package/skmemory/hooks/post-compact-reinject.sh +35 -0
  67. package/skmemory/hooks/pre-compact-save.sh +81 -0
  68. package/skmemory/hooks/session-end-save.sh +103 -0
  69. package/skmemory/hooks/session-start-ritual.sh +104 -0
  70. package/skmemory/hooks/stop-checkpoint.sh +59 -0
  71. package/skmemory/importers/__init__.py +9 -1
  72. package/skmemory/importers/telegram.py +384 -47
  73. package/skmemory/importers/telegram_api.py +580 -0
  74. package/skmemory/journal.py +7 -9
  75. package/skmemory/lovenote.py +8 -13
  76. package/skmemory/mcp_server.py +859 -0
  77. package/skmemory/models.py +51 -8
  78. package/skmemory/openclaw.py +20 -28
  79. package/skmemory/post_install.py +86 -0
  80. package/skmemory/predictive.py +236 -0
  81. package/skmemory/promotion.py +548 -0
  82. package/skmemory/quadrants.py +100 -24
  83. package/skmemory/register.py +580 -0
  84. package/skmemory/register_mcp.py +196 -0
  85. package/skmemory/ritual.py +224 -59
  86. package/skmemory/seeds.py +255 -11
  87. package/skmemory/setup_wizard.py +908 -0
  88. package/skmemory/sharing.py +408 -0
  89. package/skmemory/soul.py +98 -28
  90. package/skmemory/steelman.py +273 -260
  91. package/skmemory/store.py +411 -78
  92. package/skmemory/synthesis.py +634 -0
  93. package/skmemory/vault.py +225 -0
  94. package/tests/conftest.py +46 -0
  95. package/tests/integration/__init__.py +0 -0
  96. package/tests/integration/conftest.py +233 -0
  97. package/tests/integration/test_cross_backend.py +350 -0
  98. package/tests/integration/test_skgraph_live.py +420 -0
  99. package/tests/integration/test_skvector_live.py +366 -0
  100. package/tests/test_ai_client.py +1 -4
  101. package/tests/test_audience.py +233 -0
  102. package/tests/test_backup_rotation.py +318 -0
  103. package/tests/test_cli.py +6 -6
  104. package/tests/test_endpoint_selector.py +839 -0
  105. package/tests/test_export_import.py +4 -10
  106. package/tests/test_file_backend.py +0 -1
  107. package/tests/test_fortress.py +256 -0
  108. package/tests/test_fortress_hardening.py +441 -0
  109. package/tests/test_openclaw.py +6 -6
  110. package/tests/test_predictive.py +237 -0
  111. package/tests/test_promotion.py +347 -0
  112. package/tests/test_quadrants.py +11 -5
  113. package/tests/test_ritual.py +22 -18
  114. package/tests/test_seeds.py +97 -7
  115. package/tests/test_setup.py +950 -0
  116. package/tests/test_sharing.py +257 -0
  117. package/tests/test_skgraph_backend.py +660 -0
  118. package/tests/test_skvector_backend.py +326 -0
  119. package/tests/test_soul.py +1 -3
  120. package/tests/test_sqlite_backend.py +8 -17
  121. package/tests/test_steelman.py +7 -8
  122. package/tests/test_store.py +0 -2
  123. package/tests/test_store_graph_integration.py +245 -0
  124. package/tests/test_synthesis.py +275 -0
  125. package/tests/test_telegram_import.py +39 -15
  126. package/tests/test_vault.py +187 -0
  127. package/skmemory/backends/falkordb_backend.py +0 -310
@@ -0,0 +1,225 @@
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="EXAMPLE-DO-NOT-USE")
14
+ encrypted = vault.encrypt(memory_json_bytes)
15
+ decrypted = vault.decrypt(encrypted)
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import hashlib
21
+ import logging
22
+ import os
23
+ from pathlib import Path
24
+
25
+ logger = logging.getLogger("skmemory.vault")
26
+
27
+ VAULT_HEADER = b"SKMV1"
28
+ NONCE_SIZE = 12
29
+ TAG_SIZE = 16
30
+ KEY_SIZE = 32
31
+ SALT_SIZE = 16
32
+ KDF_ITERATIONS = 600_000
33
+
34
+
35
+ def _derive_key(passphrase: str, salt: bytes) -> bytes:
36
+ """Derive a 256-bit AES key from a passphrase using PBKDF2.
37
+
38
+ Args:
39
+ passphrase: The encryption passphrase.
40
+ salt: 16-byte random salt.
41
+
42
+ Returns:
43
+ 32-byte AES key.
44
+ """
45
+ return hashlib.pbkdf2_hmac(
46
+ "sha256",
47
+ passphrase.encode("utf-8"),
48
+ salt,
49
+ KDF_ITERATIONS,
50
+ dklen=KEY_SIZE,
51
+ )
52
+
53
+
54
+ class MemoryVault:
55
+ """Encrypt and decrypt memory files at rest.
56
+
57
+ Uses AES-256-GCM for authenticated encryption. Each encrypt()
58
+ call generates a fresh random nonce and salt, so the same
59
+ plaintext never produces the same ciphertext.
60
+
61
+ File format:
62
+ SKMV1 || salt(16) || nonce(12) || ciphertext || tag(16)
63
+
64
+ Args:
65
+ passphrase: Secret used to derive the encryption key.
66
+ Can be the agent's CapAuth passphrase or a dedicated vault key.
67
+ """
68
+
69
+ def __init__(self, passphrase: str) -> None:
70
+ self._passphrase = passphrase
71
+
72
+ def encrypt(self, plaintext: bytes) -> bytes:
73
+ """Encrypt data with AES-256-GCM.
74
+
75
+ Args:
76
+ plaintext: Raw bytes to encrypt (typically memory JSON).
77
+
78
+ Returns:
79
+ Encrypted bytes with header, salt, nonce, ciphertext, and tag.
80
+
81
+ Raises:
82
+ ImportError: If cryptography package is not installed.
83
+ """
84
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
85
+
86
+ salt = os.urandom(SALT_SIZE)
87
+ nonce = os.urandom(NONCE_SIZE)
88
+ key = _derive_key(self._passphrase, salt)
89
+
90
+ aesgcm = AESGCM(key)
91
+ ciphertext = aesgcm.encrypt(nonce, plaintext, VAULT_HEADER)
92
+
93
+ return VAULT_HEADER + salt + nonce + ciphertext
94
+
95
+ def decrypt(self, encrypted: bytes) -> bytes:
96
+ """Decrypt data encrypted with encrypt().
97
+
98
+ Args:
99
+ encrypted: Bytes from encrypt() — header + salt + nonce + ciphertext + tag.
100
+
101
+ Returns:
102
+ Decrypted plaintext bytes.
103
+
104
+ Raises:
105
+ ValueError: If the header doesn't match or decryption fails.
106
+ ImportError: If cryptography package is not installed.
107
+ """
108
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
109
+
110
+ header_len = len(VAULT_HEADER)
111
+ if encrypted[:header_len] != VAULT_HEADER:
112
+ raise ValueError("Not an SKMemory vault file (bad header)")
113
+
114
+ offset = header_len
115
+ salt = encrypted[offset : offset + SALT_SIZE]
116
+ offset += SALT_SIZE
117
+ nonce = encrypted[offset : offset + NONCE_SIZE]
118
+ offset += NONCE_SIZE
119
+ ciphertext = encrypted[offset:]
120
+
121
+ key = _derive_key(self._passphrase, salt)
122
+ aesgcm = AESGCM(key)
123
+
124
+ return aesgcm.decrypt(nonce, ciphertext, VAULT_HEADER)
125
+
126
+ def encrypt_file(self, path: Path) -> Path:
127
+ """Encrypt a file in-place, adding .vault extension.
128
+
129
+ Args:
130
+ path: Path to the plaintext file.
131
+
132
+ Returns:
133
+ Path to the encrypted file.
134
+ """
135
+ plaintext = path.read_bytes()
136
+ encrypted = self.encrypt(plaintext)
137
+ vault_path = path.with_suffix(path.suffix + ".vault")
138
+ vault_path.write_bytes(encrypted)
139
+ path.unlink()
140
+ return vault_path
141
+
142
+ def decrypt_file(self, path: Path) -> Path:
143
+ """Decrypt a .vault file, restoring the original.
144
+
145
+ Args:
146
+ path: Path to the encrypted .vault file.
147
+
148
+ Returns:
149
+ Path to the decrypted file.
150
+ """
151
+ encrypted = path.read_bytes()
152
+ plaintext = self.decrypt(encrypted)
153
+ original_path = Path(str(path).replace(".vault", ""))
154
+ original_path.write_bytes(plaintext)
155
+ path.unlink()
156
+ return original_path
157
+
158
+ def is_encrypted(self, path: Path) -> bool:
159
+ """Check if a file is vault-encrypted.
160
+
161
+ Args:
162
+ path: Path to check.
163
+
164
+ Returns:
165
+ True if the file has a valid vault header.
166
+ """
167
+ try:
168
+ data = path.read_bytes()
169
+ return data[: len(VAULT_HEADER)] == VAULT_HEADER
170
+ except OSError:
171
+ return False
172
+
173
+
174
+ def encrypt_memory_store(
175
+ memory_dir: Path,
176
+ passphrase: str,
177
+ ) -> int:
178
+ """Encrypt all memory JSON files in a directory tree.
179
+
180
+ Args:
181
+ memory_dir: Root memory directory (e.g., ~/.skcapstone/memories/).
182
+ passphrase: Encryption passphrase.
183
+
184
+ Returns:
185
+ Number of files encrypted.
186
+ """
187
+ vault = MemoryVault(passphrase)
188
+ count = 0
189
+
190
+ for json_file in memory_dir.rglob("*.json"):
191
+ if json_file.suffix == ".vault":
192
+ continue
193
+ try:
194
+ vault.encrypt_file(json_file)
195
+ count += 1
196
+ except Exception as exc:
197
+ logger.warning("Failed to encrypt %s: %s", json_file, exc)
198
+
199
+ return count
200
+
201
+
202
+ def decrypt_memory_store(
203
+ memory_dir: Path,
204
+ passphrase: str,
205
+ ) -> int:
206
+ """Decrypt all vault files in a directory tree.
207
+
208
+ Args:
209
+ memory_dir: Root memory directory.
210
+ passphrase: Decryption passphrase.
211
+
212
+ Returns:
213
+ Number of files decrypted.
214
+ """
215
+ vault = MemoryVault(passphrase)
216
+ count = 0
217
+
218
+ for vault_file in memory_dir.rglob("*.vault"):
219
+ try:
220
+ vault.decrypt_file(vault_file)
221
+ count += 1
222
+ except Exception as exc:
223
+ logger.warning("Failed to decrypt %s: %s", vault_file, exc)
224
+
225
+ return count
@@ -0,0 +1,46 @@
1
+ """
2
+ Root conftest.py — bootstraps a minimal test agent before any skmemory module
3
+ is imported by pytest.
4
+
5
+ Several skmemory modules (seeds.py, febs.py, soul.py, etc.) call
6
+ ``get_agent_paths()`` at **module level**, which raises ``ValueError`` if no
7
+ agent directory exists under ``~/.skcapstone/agents/``. This file runs before
8
+ pytest collects any test files, so setting ``SKCAPSTONE_HOME`` and
9
+ ``SKCAPSTONE_AGENT`` here — and creating the matching directory tree — ensures
10
+ those imports always succeed in CI and other fresh environments.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ import tempfile
17
+ from pathlib import Path
18
+
19
+ # ── Bootstrap a throw-away test agent ─────────────────────────────────────────
20
+ # Must happen at module level (not inside a fixture) so it takes effect before
21
+ # pytest imports test files that themselves import skmemory.
22
+
23
+ _TEST_AGENT_NAME = "test-agent"
24
+
25
+ # Use a temp dir so we never pollute the real ~/.skcapstone tree.
26
+ _tmp_skcapstone = tempfile.mkdtemp(prefix="skmemory_ci_")
27
+ os.environ["SKCAPSTONE_HOME"] = _tmp_skcapstone
28
+ os.environ["SKCAPSTONE_AGENT"] = _TEST_AGENT_NAME
29
+
30
+ _agent_base = Path(_tmp_skcapstone) / "agents" / _TEST_AGENT_NAME
31
+ for _subdir in (
32
+ "config",
33
+ "seeds",
34
+ "memory/short-term",
35
+ "memory/mid-term",
36
+ "memory/long-term",
37
+ "logs",
38
+ "archive",
39
+ ):
40
+ (_agent_base / _subdir).mkdir(parents=True, exist_ok=True)
41
+
42
+ # Minimal config so list_agents() finds this agent and load_config() doesn't
43
+ # blow up.
44
+ (_agent_base / "config" / "skmemory.yaml").write_text(
45
+ "# Auto-generated by tests/conftest.py\nname: test-agent\n"
46
+ )
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 contextlib
17
+ import os
18
+ import uuid
19
+
20
+ import pytest
21
+
22
+ # ─────────────────────────────────────────────────────────
23
+ # Connection constants
24
+ # ─────────────────────────────────────────────────────────
25
+
26
+ SKGRAPH_URL = os.environ.get("SKMEMORY_SKGRAPH_URL", "redis://localhost:6379")
27
+ SKVECTOR_URL = os.environ.get("SKMEMORY_SKVECTOR_URL", "http://localhost:6333")
28
+ SKVECTOR_KEY = os.environ.get("SKMEMORY_SKVECTOR_KEY")
29
+
30
+ # Isolated names so tests never collide with production data
31
+ TEST_GRAPH_NAME = "skmemory_integration_test"
32
+ TEST_COLLECTION_NAME = "skmemory_integration_test"
33
+
34
+
35
+ # ─────────────────────────────────────────────────────────
36
+ # Availability checks
37
+ # ─────────────────────────────────────────────────────────
38
+
39
+
40
+ def _skgraph_available() -> bool:
41
+ """Return True if a SKGraph (FalkorDB/Redis) server is reachable."""
42
+ try:
43
+ from falkordb import FalkorDB # type: ignore[import]
44
+ except ImportError:
45
+ return False
46
+
47
+ try:
48
+ db = FalkorDB.from_url(SKGRAPH_URL)
49
+ g = db.select_graph("__ping__")
50
+ g.query("RETURN 1")
51
+ return True
52
+ except Exception:
53
+ return False
54
+
55
+
56
+ def _skvector_available() -> bool:
57
+ """Return True if SKVector (Qdrant) is reachable and qdrant-client is installed."""
58
+ try:
59
+ from qdrant_client import QdrantClient # type: ignore[import]
60
+ except ImportError:
61
+ return False
62
+
63
+ try:
64
+ client = QdrantClient(url=SKVECTOR_URL, api_key=SKVECTOR_KEY)
65
+ client.get_collections()
66
+ return True
67
+ except Exception:
68
+ return False
69
+
70
+
71
+ def _sentence_transformers_available() -> bool:
72
+ try:
73
+ import sentence_transformers # noqa: F401 # type: ignore[import]
74
+
75
+ return True
76
+ except ImportError:
77
+ return False
78
+
79
+
80
+ SKGRAPH_AVAILABLE = _skgraph_available()
81
+ SKVECTOR_AVAILABLE = _skvector_available()
82
+ SENTENCE_TRANSFORMERS_AVAILABLE = _sentence_transformers_available()
83
+
84
+ # Composite flag: SKVector tests also require the embedding model
85
+ SKVECTOR_FULL_AVAILABLE = SKVECTOR_AVAILABLE and SENTENCE_TRANSFORMERS_AVAILABLE
86
+
87
+ requires_skgraph = pytest.mark.skipif(
88
+ not SKGRAPH_AVAILABLE,
89
+ reason="SKGraph unreachable (set SKMEMORY_SKGRAPH_URL or start Redis+FalkorDB)",
90
+ )
91
+
92
+ requires_skvector = pytest.mark.skipif(
93
+ not SKVECTOR_FULL_AVAILABLE,
94
+ reason=(
95
+ "SKVector unreachable or sentence-transformers not installed "
96
+ "(set SKMEMORY_SKVECTOR_URL or pip install qdrant-client sentence-transformers)"
97
+ ),
98
+ )
99
+
100
+ requires_both = pytest.mark.skipif(
101
+ not (SKGRAPH_AVAILABLE and SKVECTOR_FULL_AVAILABLE),
102
+ reason="Both SKGraph and SKVector must be reachable for cross-backend tests",
103
+ )
104
+
105
+
106
+ # ─────────────────────────────────────────────────────────
107
+ # SKGraph fixtures
108
+ # ─────────────────────────────────────────────────────────
109
+
110
+
111
+ @pytest.fixture(scope="session")
112
+ def falkordb_backend():
113
+ """Live SKGraphBackend pointed at the test graph.
114
+
115
+ The test graph is deleted at session teardown.
116
+ """
117
+ pytest.importorskip("falkordb", reason="falkordb not installed")
118
+ if not SKGRAPH_AVAILABLE:
119
+ pytest.skip("SKGraph unreachable")
120
+
121
+ from skmemory.backends.skgraph_backend import SKGraphBackend
122
+
123
+ backend = SKGraphBackend(url=SKGRAPH_URL, graph_name=TEST_GRAPH_NAME)
124
+ assert backend._ensure_initialized(), "SKGraph backend failed to initialize"
125
+
126
+ yield backend
127
+
128
+ # Teardown: drop the test graph
129
+ try:
130
+ from falkordb import FalkorDB # type: ignore[import]
131
+
132
+ db = FalkorDB.from_url(SKGRAPH_URL)
133
+ db.select_graph(TEST_GRAPH_NAME).delete()
134
+ except Exception:
135
+ pass
136
+
137
+
138
+ @pytest.fixture
139
+ def falkordb_clean(falkordb_backend):
140
+ """SKGraphBackend with test graph wiped before each test."""
141
+ # Clear all nodes so tests are independent
142
+ with contextlib.suppress(Exception):
143
+ falkordb_backend._graph.query("MATCH (n) DETACH DELETE n")
144
+ return falkordb_backend
145
+
146
+
147
+ # ─────────────────────────────────────────────────────────
148
+ # SKVector fixtures
149
+ # ─────────────────────────────────────────────────────────
150
+
151
+
152
+ @pytest.fixture(scope="session")
153
+ def qdrant_backend():
154
+ """Live SKVectorBackend pointed at the test collection.
155
+
156
+ The test collection is deleted at session teardown.
157
+ """
158
+ pytest.importorskip("qdrant_client", reason="qdrant-client not installed")
159
+ pytest.importorskip("sentence_transformers", reason="sentence-transformers not installed")
160
+ if not SKVECTOR_AVAILABLE:
161
+ pytest.skip("SKVector unreachable")
162
+
163
+ from skmemory.backends.skvector_backend import SKVectorBackend
164
+
165
+ backend = SKVectorBackend(
166
+ url=SKVECTOR_URL,
167
+ api_key=SKVECTOR_KEY,
168
+ collection=TEST_COLLECTION_NAME,
169
+ )
170
+ assert backend._ensure_initialized(), "SKVector backend failed to initialize"
171
+
172
+ yield backend
173
+
174
+ # Teardown: delete test collection
175
+ with contextlib.suppress(Exception):
176
+ backend._client.delete_collection(TEST_COLLECTION_NAME)
177
+
178
+
179
+ @pytest.fixture
180
+ def qdrant_clean(qdrant_backend):
181
+ """SKVectorBackend with collection wiped before each test."""
182
+ try:
183
+ from qdrant_client.models import Distance, VectorParams
184
+
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
+ )