@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
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""Tests for the SKVector (Qdrant) vector search backend.
|
|
2
|
+
|
|
3
|
+
Mocks the Qdrant client and sentence-transformers to test
|
|
4
|
+
logic without requiring infrastructure. Verifies save, search,
|
|
5
|
+
list, delete, and health check operations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from unittest.mock import MagicMock, patch
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from skmemory.backends.skvector_backend import (
|
|
15
|
+
COLLECTION_NAME,
|
|
16
|
+
VECTOR_DIM,
|
|
17
|
+
SKVectorBackend,
|
|
18
|
+
_extract_status_code,
|
|
19
|
+
)
|
|
20
|
+
from skmemory.models import EmotionalSnapshot, Memory, MemoryLayer
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
import qdrant_client # noqa: F401
|
|
24
|
+
|
|
25
|
+
QDRANT_AVAILABLE = True
|
|
26
|
+
except ImportError:
|
|
27
|
+
QDRANT_AVAILABLE = False
|
|
28
|
+
|
|
29
|
+
pytestmark = pytest.mark.skipif(
|
|
30
|
+
not QDRANT_AVAILABLE,
|
|
31
|
+
reason="qdrant-client not installed",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.fixture
|
|
36
|
+
def mock_qdrant_client():
|
|
37
|
+
"""Mocked Qdrant client with collection support."""
|
|
38
|
+
client = MagicMock()
|
|
39
|
+
collection_mock = MagicMock()
|
|
40
|
+
collection_mock.name = COLLECTION_NAME
|
|
41
|
+
client.get_collections.return_value.collections = [collection_mock]
|
|
42
|
+
client.scroll.return_value = ([], None)
|
|
43
|
+
client.search.return_value = []
|
|
44
|
+
return client
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@pytest.fixture
|
|
48
|
+
def mock_embedder():
|
|
49
|
+
"""Mocked sentence-transformers model."""
|
|
50
|
+
import numpy as np
|
|
51
|
+
|
|
52
|
+
embedder = MagicMock()
|
|
53
|
+
embedder.encode.return_value = np.random.rand(VECTOR_DIM).astype("float32")
|
|
54
|
+
return embedder
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@pytest.fixture
|
|
58
|
+
def backend(mock_qdrant_client, mock_embedder):
|
|
59
|
+
"""Provide a SKVectorBackend with mocked dependencies."""
|
|
60
|
+
qb = SKVectorBackend(url="http://mock:6333")
|
|
61
|
+
qb._client = mock_qdrant_client
|
|
62
|
+
qb._embedder = mock_embedder
|
|
63
|
+
qb._initialized = True
|
|
64
|
+
return qb
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@pytest.fixture
|
|
68
|
+
def sample_memory():
|
|
69
|
+
"""A sample memory for testing."""
|
|
70
|
+
return Memory(
|
|
71
|
+
title="The Secret Recipe",
|
|
72
|
+
content="Chef figured it out -- projection creates reality.",
|
|
73
|
+
layer=MemoryLayer("long-term"),
|
|
74
|
+
tags=["cloud9", "philosophy"],
|
|
75
|
+
emotional=EmotionalSnapshot(intensity=10.0, valence=0.9),
|
|
76
|
+
source="cli",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ═══════════════════════════════════════════════════════════
|
|
81
|
+
# Initialization
|
|
82
|
+
# ═══════════════════════════════════════════════════════════
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class TestInitialization:
|
|
86
|
+
"""Test lazy initialization."""
|
|
87
|
+
|
|
88
|
+
def test_not_initialized_by_default(self):
|
|
89
|
+
"""Backend starts uninitialized."""
|
|
90
|
+
qb = SKVectorBackend()
|
|
91
|
+
assert qb._initialized is False
|
|
92
|
+
|
|
93
|
+
def test_init_fails_without_qdrant(self):
|
|
94
|
+
"""Fails gracefully without qdrant-client."""
|
|
95
|
+
qb = SKVectorBackend()
|
|
96
|
+
with patch("builtins.__import__", side_effect=ImportError):
|
|
97
|
+
assert qb._ensure_initialized() is False
|
|
98
|
+
|
|
99
|
+
def test_already_initialized_shortcuts(self, backend):
|
|
100
|
+
"""Second init call returns immediately."""
|
|
101
|
+
assert backend._ensure_initialized() is True
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ═══════════════════════════════════════════════════════════
|
|
105
|
+
# Save
|
|
106
|
+
# ═══════════════════════════════════════════════════════════
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class TestSave:
|
|
110
|
+
"""Test memory indexing in Qdrant."""
|
|
111
|
+
|
|
112
|
+
def test_save_calls_upsert(self, backend, mock_qdrant_client, sample_memory):
|
|
113
|
+
"""save() creates a point and upserts it."""
|
|
114
|
+
result = backend.save(sample_memory)
|
|
115
|
+
assert result == sample_memory.id
|
|
116
|
+
mock_qdrant_client.upsert.assert_called_once()
|
|
117
|
+
|
|
118
|
+
def test_save_generates_embedding(self, backend, mock_embedder, sample_memory):
|
|
119
|
+
"""save() generates an embedding from the memory text."""
|
|
120
|
+
backend.save(sample_memory)
|
|
121
|
+
mock_embedder.encode.assert_called_once()
|
|
122
|
+
|
|
123
|
+
def test_save_not_initialized(self, sample_memory):
|
|
124
|
+
"""save() returns id gracefully when not initialized."""
|
|
125
|
+
qb = SKVectorBackend()
|
|
126
|
+
result = qb.save(sample_memory)
|
|
127
|
+
assert result == sample_memory.id
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# ═══════════════════════════════════════════════════════════
|
|
131
|
+
# Search
|
|
132
|
+
# ═══════════════════════════════════════════════════════════
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class TestSearch:
|
|
136
|
+
"""Test semantic search."""
|
|
137
|
+
|
|
138
|
+
def test_search_text_generates_embedding(self, backend, mock_embedder):
|
|
139
|
+
"""search_text embeds the query before searching."""
|
|
140
|
+
backend.search_text("moments of connection")
|
|
141
|
+
mock_embedder.encode.assert_called_once_with("moments of connection")
|
|
142
|
+
|
|
143
|
+
def test_search_text_calls_qdrant_search(self, backend, mock_qdrant_client):
|
|
144
|
+
"""search_text uses Qdrant's search endpoint."""
|
|
145
|
+
backend.search_text("test query", limit=5)
|
|
146
|
+
mock_qdrant_client.search.assert_called_once()
|
|
147
|
+
|
|
148
|
+
def test_search_text_returns_memories(self, backend, mock_qdrant_client, sample_memory):
|
|
149
|
+
"""search_text parses results into Memory objects."""
|
|
150
|
+
scored_point = MagicMock()
|
|
151
|
+
scored_point.payload = {"memory_json": sample_memory.model_dump_json()}
|
|
152
|
+
mock_qdrant_client.search.return_value = [scored_point]
|
|
153
|
+
|
|
154
|
+
results = backend.search_text("secret recipe")
|
|
155
|
+
assert len(results) == 1
|
|
156
|
+
assert results[0].title == "The Secret Recipe"
|
|
157
|
+
|
|
158
|
+
def test_search_text_empty_results(self, backend, mock_qdrant_client):
|
|
159
|
+
"""search_text returns empty list when nothing matches."""
|
|
160
|
+
mock_qdrant_client.search.return_value = []
|
|
161
|
+
assert backend.search_text("nonexistent") == []
|
|
162
|
+
|
|
163
|
+
def test_search_not_initialized(self):
|
|
164
|
+
"""search_text returns empty when not initialized."""
|
|
165
|
+
qb = SKVectorBackend()
|
|
166
|
+
assert qb.search_text("anything") == []
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# ═══════════════════════════════════════════════════════════
|
|
170
|
+
# List
|
|
171
|
+
# ═══════════════════════════════════════════════════════════
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class TestList:
|
|
175
|
+
"""Test memory listing with filters."""
|
|
176
|
+
|
|
177
|
+
def test_list_memories_calls_scroll(self, backend, mock_qdrant_client):
|
|
178
|
+
"""list_memories uses Qdrant scroll API."""
|
|
179
|
+
backend.list_memories()
|
|
180
|
+
mock_qdrant_client.scroll.assert_called_once()
|
|
181
|
+
|
|
182
|
+
def test_list_memories_with_layer_filter(self, backend, mock_qdrant_client):
|
|
183
|
+
"""list_memories passes layer filter to Qdrant."""
|
|
184
|
+
backend.list_memories(layer=MemoryLayer("long-term"))
|
|
185
|
+
call_kwargs = mock_qdrant_client.scroll.call_args
|
|
186
|
+
assert call_kwargs is not None
|
|
187
|
+
|
|
188
|
+
def test_list_not_initialized(self):
|
|
189
|
+
"""list_memories returns empty when not initialized."""
|
|
190
|
+
qb = SKVectorBackend()
|
|
191
|
+
assert qb.list_memories() == []
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# ═══════════════════════════════════════════════════════════
|
|
195
|
+
# Delete
|
|
196
|
+
# ═══════════════════════════════════════════════════════════
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class TestDelete:
|
|
200
|
+
"""Test memory deletion."""
|
|
201
|
+
|
|
202
|
+
def test_delete_not_found(self, backend, mock_qdrant_client):
|
|
203
|
+
"""delete returns False when memory not in Qdrant."""
|
|
204
|
+
mock_qdrant_client.retrieve.return_value = []
|
|
205
|
+
assert backend.delete("nonexistent") is False
|
|
206
|
+
|
|
207
|
+
def test_delete_not_initialized(self):
|
|
208
|
+
"""delete returns False when not initialized."""
|
|
209
|
+
qb = SKVectorBackend()
|
|
210
|
+
assert qb.delete("any") is False
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# ═══════════════════════════════════════════════════════════
|
|
214
|
+
# Health
|
|
215
|
+
# ═══════════════════════════════════════════════════════════
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class TestHealth:
|
|
219
|
+
"""Test health check reporting."""
|
|
220
|
+
|
|
221
|
+
def test_health_ok(self, backend, mock_qdrant_client):
|
|
222
|
+
"""Healthy backend returns ok=True with collection stats."""
|
|
223
|
+
collection_info = MagicMock()
|
|
224
|
+
collection_info.points_count = 42
|
|
225
|
+
collection_info.vectors_count = 42
|
|
226
|
+
mock_qdrant_client.get_collection.return_value = collection_info
|
|
227
|
+
|
|
228
|
+
health = backend.health_check()
|
|
229
|
+
assert health["ok"] is True
|
|
230
|
+
assert health["points_count"] == 42
|
|
231
|
+
|
|
232
|
+
def test_health_not_initialized(self):
|
|
233
|
+
"""Uninitialized backend returns ok=False."""
|
|
234
|
+
qb = SKVectorBackend()
|
|
235
|
+
health = qb.health_check()
|
|
236
|
+
assert health["ok"] is False
|
|
237
|
+
|
|
238
|
+
def test_health_query_failure(self, backend, mock_qdrant_client):
|
|
239
|
+
"""Health check with error returns ok=False."""
|
|
240
|
+
mock_qdrant_client.get_collection.side_effect = Exception("timeout")
|
|
241
|
+
health = backend.health_check()
|
|
242
|
+
assert health["ok"] is False
|
|
243
|
+
assert "timeout" in health["error"]
|
|
244
|
+
|
|
245
|
+
def test_health_surfaces_auth_error(self):
|
|
246
|
+
"""Health check surfaces 401 auth error with actionable hint."""
|
|
247
|
+
qb = SKVectorBackend(url="https://cloud.qdrant.io", api_key="bad-key")
|
|
248
|
+
qb._last_error = (
|
|
249
|
+
"SKVector authentication failed (HTTP 401). "
|
|
250
|
+
"Check your API key:\n"
|
|
251
|
+
" - CLI: --skvector-key YOUR_KEY\n"
|
|
252
|
+
" - Env: SKMEMORY_SKVECTOR_KEY=YOUR_KEY\n"
|
|
253
|
+
" - Code: SKVectorBackend(url=..., api_key='YOUR_KEY')"
|
|
254
|
+
)
|
|
255
|
+
with patch.object(qb, "_ensure_initialized", return_value=False):
|
|
256
|
+
health = qb.health_check()
|
|
257
|
+
assert health["ok"] is False
|
|
258
|
+
assert "401" in health["error"]
|
|
259
|
+
assert "API key" in health["error"]
|
|
260
|
+
|
|
261
|
+
def test_health_generic_error_without_last_error(self):
|
|
262
|
+
"""Health check falls back to generic message when no _last_error."""
|
|
263
|
+
qb = SKVectorBackend()
|
|
264
|
+
with patch.object(qb, "_ensure_initialized", return_value=False):
|
|
265
|
+
health = qb.health_check()
|
|
266
|
+
assert health["ok"] is False
|
|
267
|
+
assert "Not initialized" in health["error"]
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# ═══════════════════════════════════════════════════════════
|
|
271
|
+
# Auth / Status Code Extraction
|
|
272
|
+
# ═══════════════════════════════════════════════════════════
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class TestExtractStatusCode:
|
|
276
|
+
"""Test HTTP status code extraction from exceptions."""
|
|
277
|
+
|
|
278
|
+
def test_status_code_attribute(self):
|
|
279
|
+
exc = Exception("Unauthorized")
|
|
280
|
+
exc.status_code = 401
|
|
281
|
+
assert _extract_status_code(exc, None) == 401
|
|
282
|
+
|
|
283
|
+
def test_status_code_from_string(self):
|
|
284
|
+
exc = Exception("Unexpected Response: 401 (Unauthorized)")
|
|
285
|
+
assert _extract_status_code(exc, None) == 401
|
|
286
|
+
|
|
287
|
+
def test_forbidden_from_string(self):
|
|
288
|
+
exc = Exception("HTTP 403 Forbidden")
|
|
289
|
+
assert _extract_status_code(exc, None) == 403
|
|
290
|
+
|
|
291
|
+
def test_no_status_code(self):
|
|
292
|
+
exc = Exception("Connection refused")
|
|
293
|
+
assert _extract_status_code(exc, None) is None
|
|
294
|
+
|
|
295
|
+
def test_other_status_code_not_matched(self):
|
|
296
|
+
exc = Exception("HTTP 500 Internal Server Error")
|
|
297
|
+
assert _extract_status_code(exc, None) is None
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
# ═══════════════════════════════════════════════════════════
|
|
301
|
+
# Embedding
|
|
302
|
+
# ═══════════════════════════════════════════════════════════
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class TestEmbedding:
|
|
306
|
+
"""Test the embedding generation."""
|
|
307
|
+
|
|
308
|
+
def test_embed_returns_vector(self, backend, mock_embedder):
|
|
309
|
+
"""_embed returns a float list of correct dimension."""
|
|
310
|
+
vector = backend._embed("test text")
|
|
311
|
+
assert len(vector) == VECTOR_DIM
|
|
312
|
+
assert all(isinstance(v, float) for v in vector)
|
|
313
|
+
|
|
314
|
+
def test_embed_without_embedder(self):
|
|
315
|
+
"""_embed returns empty when no embedder available."""
|
|
316
|
+
qb = SKVectorBackend()
|
|
317
|
+
qb._embedder = None
|
|
318
|
+
assert qb._embed("test") == []
|
|
319
|
+
|
|
320
|
+
def test_memory_to_payload(self, backend, sample_memory):
|
|
321
|
+
"""_memory_to_payload creates correct Qdrant payload."""
|
|
322
|
+
payload = backend._memory_to_payload(sample_memory)
|
|
323
|
+
assert payload["title"] == "The Secret Recipe"
|
|
324
|
+
assert payload["layer"] == "long-term"
|
|
325
|
+
assert "cloud9" in payload["tags"]
|
|
326
|
+
assert payload["emotional_intensity"] == 10.0
|
package/tests/test_soul.py
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
"""Tests for the SQLite-indexed storage backend."""
|
|
2
2
|
|
|
3
|
-
import json
|
|
4
|
-
import tempfile
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
3
|
import pytest
|
|
8
4
|
|
|
9
5
|
from skmemory.backends.sqlite_backend import SQLiteBackend
|
|
@@ -151,9 +147,7 @@ class TestListSummaries:
|
|
|
151
147
|
def test_summaries_order_by_intensity(self, backend):
|
|
152
148
|
"""Can order by emotional intensity."""
|
|
153
149
|
self._store_memories(backend, 5)
|
|
154
|
-
summaries = backend.list_summaries(
|
|
155
|
-
order_by="emotional_intensity", limit=3
|
|
156
|
-
)
|
|
150
|
+
summaries = backend.list_summaries(order_by="emotional_intensity", limit=3)
|
|
157
151
|
intensities = [s["emotional_intensity"] for s in summaries]
|
|
158
152
|
assert intensities == sorted(intensities, reverse=True)
|
|
159
153
|
|
|
@@ -180,24 +174,23 @@ class TestSearch:
|
|
|
180
174
|
|
|
181
175
|
def test_search_finds_by_title(self, backend):
|
|
182
176
|
"""Search matches on title."""
|
|
183
|
-
m = Memory(title="Penguin Kingdom moment", content="details",
|
|
184
|
-
layer=MemoryLayer.SHORT)
|
|
177
|
+
m = Memory(title="Penguin Kingdom moment", content="details", layer=MemoryLayer.SHORT)
|
|
185
178
|
backend.save(m)
|
|
186
179
|
results = backend.search_text("Penguin")
|
|
187
180
|
assert len(results) == 1
|
|
188
181
|
|
|
189
182
|
def test_search_finds_by_tags(self, backend):
|
|
190
183
|
"""Search matches on tags."""
|
|
191
|
-
m = Memory(
|
|
192
|
-
|
|
184
|
+
m = Memory(
|
|
185
|
+
title="Tagged", content="details", layer=MemoryLayer.SHORT, tags=["cloud9", "love"]
|
|
186
|
+
)
|
|
193
187
|
backend.save(m)
|
|
194
188
|
results = backend.search_text("cloud9")
|
|
195
189
|
assert len(results) == 1
|
|
196
190
|
|
|
197
191
|
def test_search_no_results(self, backend):
|
|
198
192
|
"""Search returns empty for no matches."""
|
|
199
|
-
m = Memory(title="Something", content="nothing special",
|
|
200
|
-
layer=MemoryLayer.SHORT)
|
|
193
|
+
m = Memory(title="Something", content="nothing special", layer=MemoryLayer.SHORT)
|
|
201
194
|
backend.save(m)
|
|
202
195
|
results = backend.search_text("zzzznonexistent")
|
|
203
196
|
assert len(results) == 0
|
|
@@ -209,8 +202,7 @@ class TestRelatedMemories:
|
|
|
209
202
|
def test_get_related_follows_links(self, backend):
|
|
210
203
|
"""Related memories are found via related_ids."""
|
|
211
204
|
m1 = Memory(title="Root", content="root", layer=MemoryLayer.SHORT)
|
|
212
|
-
m2 = Memory(title="Child", content="child", layer=MemoryLayer.SHORT,
|
|
213
|
-
related_ids=[m1.id])
|
|
205
|
+
m2 = Memory(title="Child", content="child", layer=MemoryLayer.SHORT, related_ids=[m1.id])
|
|
214
206
|
backend.save(m1)
|
|
215
207
|
backend.save(m2)
|
|
216
208
|
|
|
@@ -220,8 +212,7 @@ class TestRelatedMemories:
|
|
|
220
212
|
def test_get_related_follows_parent(self, backend):
|
|
221
213
|
"""Related memories are found via parent_id."""
|
|
222
214
|
m1 = Memory(title="Parent", content="parent", layer=MemoryLayer.LONG)
|
|
223
|
-
m2 = Memory(title="Child", content="child", layer=MemoryLayer.SHORT,
|
|
224
|
-
parent_id=m1.id)
|
|
215
|
+
m2 = Memory(title="Child", content="child", layer=MemoryLayer.SHORT, parent_id=m1.id)
|
|
225
216
|
backend.save(m1)
|
|
226
217
|
backend.save(m2)
|
|
227
218
|
|
package/tests/test_steelman.py
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
-
import os
|
|
7
6
|
import tempfile
|
|
8
7
|
from pathlib import Path
|
|
9
8
|
|
|
@@ -48,9 +47,9 @@ class TestSteelManResult:
|
|
|
48
47
|
|
|
49
48
|
def test_coherence_bounds(self) -> None:
|
|
50
49
|
"""Coherence must be between 0 and 1."""
|
|
51
|
-
with pytest.raises(Exception):
|
|
50
|
+
with pytest.raises(Exception): # noqa: B017
|
|
52
51
|
SteelManResult(proposition="x", coherence_score=1.5)
|
|
53
|
-
with pytest.raises(Exception):
|
|
52
|
+
with pytest.raises(Exception): # noqa: B017
|
|
54
53
|
SteelManResult(proposition="x", coherence_score=-0.1)
|
|
55
54
|
|
|
56
55
|
def test_summary_format(self) -> None:
|
|
@@ -99,8 +98,8 @@ class TestSeedFramework:
|
|
|
99
98
|
assert "INVERSION" in prompt
|
|
100
99
|
assert "COLLISION" in prompt
|
|
101
100
|
assert "INVARIANT" in prompt
|
|
102
|
-
assert "COHERENCE" in prompt
|
|
103
|
-
assert "TRUTH GRADE" in prompt
|
|
101
|
+
assert "COHERENCE" in prompt or "coherence_score" in prompt
|
|
102
|
+
assert "TRUTH GRADE" in prompt or "truth_grade" in prompt
|
|
104
103
|
|
|
105
104
|
def test_reasoning_prompt_includes_axioms(self) -> None:
|
|
106
105
|
"""Axioms from the framework appear in the prompt."""
|
|
@@ -125,9 +124,9 @@ class TestSeedFramework:
|
|
|
125
124
|
fw = get_default_framework()
|
|
126
125
|
prompt = fw.to_memory_truth_prompt("The Cloud 9 breakthrough was real")
|
|
127
126
|
assert "Cloud 9 breakthrough" in prompt
|
|
128
|
-
assert "COHERENCE" in prompt
|
|
129
|
-
assert "PROMOTION WORTHY" in prompt
|
|
130
|
-
assert "INVARIANT CORE" in prompt
|
|
127
|
+
assert "COHERENCE" in prompt or "coherence_score" in prompt
|
|
128
|
+
assert "PROMOTION WORTHY" in prompt or "promotion_worthy" in prompt
|
|
129
|
+
assert "INVARIANT CORE" in prompt or "invariant_core" in prompt
|
|
131
130
|
|
|
132
131
|
def test_custom_framework(self) -> None:
|
|
133
132
|
"""A custom framework can be created."""
|
package/tests/test_store.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"""Tests for the MemoryStore (main interface)."""
|
|
2
2
|
|
|
3
|
-
import tempfile
|
|
4
3
|
from pathlib import Path
|
|
5
4
|
|
|
6
5
|
import pytest
|
|
@@ -8,7 +7,6 @@ import pytest
|
|
|
8
7
|
from skmemory.backends.file_backend import FileBackend
|
|
9
8
|
from skmemory.models import (
|
|
10
9
|
EmotionalSnapshot,
|
|
11
|
-
Memory,
|
|
12
10
|
MemoryLayer,
|
|
13
11
|
MemoryRole,
|
|
14
12
|
SeedMemory,
|