pycontextdb 0.4.1__tar.gz → 0.4.2__tar.gz
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.
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/CHANGELOG.md +7 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/PKG-INFO +1 -1
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/__init__.py +1 -1
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/client.py +3 -2
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/dynamics/retrieval.py +28 -16
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/store/base.py +26 -6
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/store/postgres_store.py +28 -8
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/store/sqlite_store.py +2 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/pyproject.toml +1 -1
- pycontextdb-0.4.2/tests/unit/test_pg_sql.py +228 -0
- pycontextdb-0.4.2/tests/unit/test_retrieval.py +84 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/tests/unit/test_store.py +39 -2
- pycontextdb-0.4.1/tests/unit/test_pg_sql.py +0 -94
- pycontextdb-0.4.1/tests/unit/test_retrieval.py +0 -30
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/.github/CODEOWNERS +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/.github/copilot-instructions.md +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/.github/pull_request_template.md +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/.github/workflows/ci.yml +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/.github/workflows/publish.yml +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/.gitignore +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/.pre-commit-config.yaml +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/COMMERCIAL.md +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/CONTRIBUTING.md +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/LICENSE +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/NOTICE +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/OPEN_CORE.md +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/README.md +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/ai-tools/.cursorrules +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/ai-tools/CLAUDE.md +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/ai-tools/README.md +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/ai-tools/llms-full.txt +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/ai-tools/llms.txt +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/assets/README.md +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/assets/architecture.png +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/assets/benchmarks.png +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/assets/hero.png +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/assets/performance.png +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/assets/problem.png +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/assets/quickstart.png +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/benchmarks/__init__.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/benchmarks/run_benchmarks.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/benchmarks/trust_bakeoff.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/agents/__init__.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/agents/memory_bus.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/agents/rl_manager.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/cli.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/connectors/__init__.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/connectors/base.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/connectors/postgres.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/core/__init__.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/core/clock.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/core/config.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/core/exceptions.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/core/models.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/core/policy.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/core/slots.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/dynamics/__init__.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/dynamics/evolution.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/dynamics/formation.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/dynamics/salience.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/dynamics/trust.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/graphs/__init__.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/graphs/base.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/graphs/causal.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/graphs/entity.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/graphs/semantic.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/graphs/temporal.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/integrations/__init__.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/integrations/act.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/integrations/autogen.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/integrations/crewai.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/integrations/langchain.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/integrations/livekit.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/integrations/openai_tools.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/integrations/pipecat.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/integrations/prompting.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/mcp.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/memory/__init__.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/memory/experiential.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/memory/factual.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/memory/working.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/pool.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/privacy/__init__.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/privacy/audit.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/privacy/injection.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/privacy/pii_detector.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/privacy/retention.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/py.typed +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/serve.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/store/__init__.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/store/factory.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/store/pg_sql.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/store/vector_index.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/utils/__init__.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/utils/embeddings.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/utils/llm.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/contextdb/utils/migrations.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/docs/api.md +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/docs/architecture.md +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/docs/connectors.md +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/docs/index.md +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/docs/memory_evolution.md +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/docs/migrations.md +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/docs/multi_tenant.md +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/docs/postgres.md +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/docs/prompts.md +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/docs/quickstart.md +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/docs/serve.md +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/docs/trust_policy.md +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/examples/customer_support.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/examples/multi_agent_team.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/examples/phone_agent.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/examples/research_assistant.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/legal/CLA-entity.md +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/legal/CLA-individual.md +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/legal/signed/README.md +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/mkdocs.yml +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/scripts/check_open_core_boundary.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/scripts/check_release.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/tests/__init__.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/tests/conftest.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/tests/evals/__init__.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/tests/evals/test_trust_model.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/tests/integration/__init__.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/tests/integration/test_client.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/tests/integration/test_langchain_integration.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/tests/pg_util.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/tests/test_version.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/tests/unit/__init__.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/tests/unit/test_audit.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/tests/unit/test_config.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/tests/unit/test_connectors.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/tests/unit/test_decision_scope.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/tests/unit/test_embeddings.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/tests/unit/test_evolution.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/tests/unit/test_formation.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/tests/unit/test_graphs.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/tests/unit/test_host_api.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/tests/unit/test_init.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/tests/unit/test_memory_bus.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/tests/unit/test_memory_evolution.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/tests/unit/test_models.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/tests/unit/test_open_core_boundary.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/tests/unit/test_pii_detector.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/tests/unit/test_postgres_concurrency.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/tests/unit/test_postgres_connector.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/tests/unit/test_postgres_memory_evolution.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/tests/unit/test_retention.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/tests/unit/test_scoped_id_access.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/tests/unit/test_serve.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/tests/unit/test_trust_policy.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/tests/unit/test_vector_index.py +0 -0
- {pycontextdb-0.4.1 → pycontextdb-0.4.2}/tests/unit/test_vector_scope.py +0 -0
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.4.2 — 2026-09-20
|
|
6
|
+
|
|
7
|
+
- Recall now bounds sibling composition before deep-copying Postgres cache
|
|
8
|
+
entries and borrows read-only candidate references while ranking. Final
|
|
9
|
+
results remain detached, and entity priority, temporal validity, user scope,
|
|
10
|
+
and composition limits are unchanged.
|
|
11
|
+
|
|
5
12
|
## 0.4.1 — 2026-08-24
|
|
6
13
|
|
|
7
14
|
- NOOP audit entries now carry the existing memory ID and canonical slot
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pycontextdb
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: The unified context layer for AI agents — memory you can act on without treating wishes as facts
|
|
5
5
|
Project-URL: Homepage, https://github.com/atomsai/contextdb
|
|
6
6
|
Project-URL: Documentation, https://contextdb.dev
|
|
@@ -1928,6 +1928,9 @@ class ContextDB:
|
|
|
1928
1928
|
siblings = await store.list_by_entities(
|
|
1929
1929
|
entity_keys,
|
|
1930
1930
|
user_id=user_id,
|
|
1931
|
+
exclude_ids=seen,
|
|
1932
|
+
valid_at=moment,
|
|
1933
|
+
limit=extra,
|
|
1931
1934
|
)
|
|
1932
1935
|
siblings_by_entity: dict[str, list[MemoryItem]] = {}
|
|
1933
1936
|
for sibling in siblings:
|
|
@@ -1940,8 +1943,6 @@ class ContextDB:
|
|
|
1940
1943
|
if not item.entity_key:
|
|
1941
1944
|
continue
|
|
1942
1945
|
for sibling in siblings_by_entity.get(item.entity_key, []):
|
|
1943
|
-
if sibling.id in seen or not sibling.is_valid_at(moment):
|
|
1944
|
-
continue
|
|
1945
1946
|
seen.add(sibling.id)
|
|
1946
1947
|
extras.append(sibling)
|
|
1947
1948
|
if len(extras) >= extra:
|
|
@@ -15,7 +15,7 @@ heuristics below get ~80% of the signal for zero cost.
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
import re
|
|
18
|
-
from dataclasses import dataclass, field
|
|
18
|
+
from dataclasses import dataclass, field, replace
|
|
19
19
|
from datetime import datetime
|
|
20
20
|
from typing import TYPE_CHECKING
|
|
21
21
|
|
|
@@ -166,7 +166,10 @@ class RetrievalEngine:
|
|
|
166
166
|
"""RRF-fuse per-graph rankings, then multiply by salience."""
|
|
167
167
|
weights = self.classifier.classify(query)
|
|
168
168
|
seed_items = await self.store.search_by_embedding(
|
|
169
|
-
query_embedding,
|
|
169
|
+
query_embedding,
|
|
170
|
+
top_k=top_k * 2,
|
|
171
|
+
user_id=user_id,
|
|
172
|
+
copy_items=False,
|
|
170
173
|
)
|
|
171
174
|
semantic_ranking = [(item.id, 1.0 / (rank + 1)) for rank, item in enumerate(seed_items)]
|
|
172
175
|
rankings: dict[str, list[tuple[str, float]]] = {"semantic": semantic_ranking}
|
|
@@ -201,25 +204,34 @@ class RetrievalEngine:
|
|
|
201
204
|
if not fused:
|
|
202
205
|
# No fusion signal (empty store or zero weights): rank the seeds
|
|
203
206
|
# directly, still applying salience.
|
|
204
|
-
|
|
207
|
+
scored = [
|
|
205
208
|
self._score(
|
|
206
209
|
item, 1.0 / (rank + 1), per_graph_ranks, now, seed_cosine.get(item.id, 0.0)
|
|
207
210
|
)
|
|
208
211
|
for rank, item in enumerate(seed_items[:top_k])
|
|
209
212
|
]
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
213
|
+
else:
|
|
214
|
+
scored = []
|
|
215
|
+
for mid, rrf_score in fused:
|
|
216
|
+
item = seed_by_id.get(mid)
|
|
217
|
+
if item is None:
|
|
218
|
+
item = await self.store.get_raw(mid)
|
|
219
|
+
if item is None:
|
|
220
|
+
continue
|
|
221
|
+
scored.append(
|
|
222
|
+
self._score(
|
|
223
|
+
item,
|
|
224
|
+
rrf_score,
|
|
225
|
+
per_graph_ranks,
|
|
226
|
+
now,
|
|
227
|
+
seed_cosine.get(mid, 0.0),
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
scored.sort(key=lambda s: s.final_score, reverse=True)
|
|
231
|
+
return [
|
|
232
|
+
replace(score, item=score.item.model_copy(deep=True))
|
|
233
|
+
for score in scored[:top_k]
|
|
234
|
+
]
|
|
223
235
|
|
|
224
236
|
def _score(
|
|
225
237
|
self,
|
|
@@ -12,6 +12,7 @@ from contextdb.core.models import MemoryConsistencyToken, MemoryStatus
|
|
|
12
12
|
|
|
13
13
|
if TYPE_CHECKING:
|
|
14
14
|
from contextlib import AbstractAsyncContextManager
|
|
15
|
+
from datetime import datetime
|
|
15
16
|
|
|
16
17
|
from contextdb.core.models import MemoryItem, MemoryType
|
|
17
18
|
|
|
@@ -54,11 +55,15 @@ class BaseStore(ABC):
|
|
|
54
55
|
top_k: int = 10,
|
|
55
56
|
filters: dict[str, object] | None = None,
|
|
56
57
|
user_id: str | None = None,
|
|
58
|
+
*,
|
|
59
|
+
copy_items: bool = True,
|
|
57
60
|
) -> list[MemoryItem]:
|
|
58
61
|
"""Return top-k most similar memories by cosine similarity.
|
|
59
62
|
|
|
60
63
|
``user_id`` filters the result. A store constructed with a fixed
|
|
61
64
|
user scope ignores a conflicting per-call value and stays scoped.
|
|
65
|
+
``copy_items=False`` is reserved for internal read-only ranking;
|
|
66
|
+
public callers receive detached items by default.
|
|
62
67
|
"""
|
|
63
68
|
|
|
64
69
|
@abstractmethod
|
|
@@ -148,16 +153,31 @@ class BaseStore(ABC):
|
|
|
148
153
|
entity_keys: list[str],
|
|
149
154
|
status: MemoryStatus | None = MemoryStatus.ACTIVE,
|
|
150
155
|
user_id: str | None = None,
|
|
156
|
+
*,
|
|
157
|
+
exclude_ids: set[str] | None = None,
|
|
158
|
+
valid_at: datetime | None = None,
|
|
159
|
+
limit: int | None = None,
|
|
151
160
|
) -> list[MemoryItem]:
|
|
161
|
+
"""List scoped entity memories with optional bounded composition filters."""
|
|
162
|
+
|
|
163
|
+
if limit is not None and limit <= 0:
|
|
164
|
+
return []
|
|
165
|
+
excluded = exclude_ids or set()
|
|
152
166
|
items: list[MemoryItem] = []
|
|
153
167
|
for entity_key in dict.fromkeys(entity_keys):
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
user_id=user_id,
|
|
159
|
-
)
|
|
168
|
+
candidates = await self.list_by_entity(
|
|
169
|
+
entity_key,
|
|
170
|
+
status=status,
|
|
171
|
+
user_id=user_id,
|
|
160
172
|
)
|
|
173
|
+
for item in candidates:
|
|
174
|
+
if item.id in excluded:
|
|
175
|
+
continue
|
|
176
|
+
if valid_at is not None and not item.is_valid_at(valid_at):
|
|
177
|
+
continue
|
|
178
|
+
items.append(item)
|
|
179
|
+
if limit is not None and len(items) >= limit:
|
|
180
|
+
return items
|
|
161
181
|
return items
|
|
162
182
|
|
|
163
183
|
async def list_pending_consolidation(self, limit: int = 100) -> list[MemoryItem]:
|
|
@@ -940,6 +940,8 @@ class PostgresStore(BaseStore):
|
|
|
940
940
|
top_k: int = 10,
|
|
941
941
|
filters: dict[str, object] | None = None,
|
|
942
942
|
user_id: str | None = None,
|
|
943
|
+
*,
|
|
944
|
+
copy_items: bool = True,
|
|
943
945
|
) -> list[MemoryItem]:
|
|
944
946
|
index = await self._ensure_index()
|
|
945
947
|
query = np.asarray(embedding, dtype=np.float32)
|
|
@@ -968,7 +970,7 @@ class PostgresStore(BaseStore):
|
|
|
968
970
|
continue
|
|
969
971
|
if filters and not _passes_filters(item, filters):
|
|
970
972
|
continue
|
|
971
|
-
results.append(item.model_copy(deep=True))
|
|
973
|
+
results.append(item.model_copy(deep=True) if copy_items else item)
|
|
972
974
|
if len(results) >= top_k:
|
|
973
975
|
break
|
|
974
976
|
return results
|
|
@@ -1047,22 +1049,40 @@ class PostgresStore(BaseStore):
|
|
|
1047
1049
|
entity_keys: list[str],
|
|
1048
1050
|
status: MemoryStatus | None = MemoryStatus.ACTIVE,
|
|
1049
1051
|
user_id: str | None = None,
|
|
1052
|
+
*,
|
|
1053
|
+
exclude_ids: set[str] | None = None,
|
|
1054
|
+
valid_at: datetime | None = None,
|
|
1055
|
+
limit: int | None = None,
|
|
1050
1056
|
) -> list[MemoryItem]:
|
|
1051
1057
|
if status != MemoryStatus.ACTIVE:
|
|
1052
1058
|
return await super().list_by_entities(
|
|
1053
1059
|
entity_keys,
|
|
1054
1060
|
status=status,
|
|
1055
1061
|
user_id=user_id,
|
|
1062
|
+
exclude_ids=exclude_ids,
|
|
1063
|
+
valid_at=valid_at,
|
|
1064
|
+
limit=limit,
|
|
1056
1065
|
)
|
|
1066
|
+
if limit is not None and limit <= 0:
|
|
1067
|
+
return []
|
|
1057
1068
|
if not self._index_loaded:
|
|
1058
1069
|
await self._ensure_index()
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
for item in self._index_items.values()
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1070
|
+
excluded = exclude_ids or set()
|
|
1071
|
+
items: list[MemoryItem] = []
|
|
1072
|
+
for entity_key in dict.fromkeys(entity_keys):
|
|
1073
|
+
for item in self._index_items.values():
|
|
1074
|
+
if item.entity_key != entity_key:
|
|
1075
|
+
continue
|
|
1076
|
+
if not self._item_scope_allows(item, user_id):
|
|
1077
|
+
continue
|
|
1078
|
+
if item.id in excluded:
|
|
1079
|
+
continue
|
|
1080
|
+
if valid_at is not None and not item.is_valid_at(valid_at):
|
|
1081
|
+
continue
|
|
1082
|
+
items.append(item.model_copy(deep=True))
|
|
1083
|
+
if limit is not None and len(items) >= limit:
|
|
1084
|
+
return items
|
|
1085
|
+
return items
|
|
1066
1086
|
|
|
1067
1087
|
async def list_pending_consolidation(self, limit: int = 100) -> list[MemoryItem]:
|
|
1068
1088
|
sql = (
|
|
@@ -821,6 +821,8 @@ class SQLiteStore(BaseStore):
|
|
|
821
821
|
top_k: int = 10,
|
|
822
822
|
filters: dict[str, object] | None = None,
|
|
823
823
|
user_id: str | None = None,
|
|
824
|
+
*,
|
|
825
|
+
copy_items: bool = True,
|
|
824
826
|
) -> list[MemoryItem]:
|
|
825
827
|
conn = self._require_conn()
|
|
826
828
|
index = await self._ensure_index()
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pycontextdb"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.2"
|
|
8
8
|
description = "The unified context layer for AI agents — memory you can act on without treating wishes as facts"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
from datetime import datetime, timedelta, timezone
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from contextdb.core.exceptions import ConfigError
|
|
7
|
+
from contextdb.core.models import MemoryItem
|
|
8
|
+
from contextdb.store.factory import (
|
|
9
|
+
is_postgres_url,
|
|
10
|
+
normalize_postgres_url,
|
|
11
|
+
open_store,
|
|
12
|
+
)
|
|
13
|
+
from contextdb.store.pg_sql import qmark_to_dollar, split_script, translate_sqlite_sql
|
|
14
|
+
from contextdb.store.postgres_store import PostgresStore, _PgAdapter
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_qmark_to_dollar() -> None:
|
|
18
|
+
assert qmark_to_dollar("SELECT * FROM t WHERE a = ? AND b = ?") == (
|
|
19
|
+
"SELECT * FROM t WHERE a = $1 AND b = $2"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_insert_or_replace_semantic() -> None:
|
|
24
|
+
sql = translate_sqlite_sql(
|
|
25
|
+
"INSERT OR REPLACE INTO semantic_edges "
|
|
26
|
+
"(source_id, target_id, weight, metadata, created_at) VALUES (?,?,?,?,?)"
|
|
27
|
+
)
|
|
28
|
+
assert "ON CONFLICT (source_id, target_id) DO UPDATE" in sql
|
|
29
|
+
assert "$5" in sql
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_insert_or_ignore() -> None:
|
|
33
|
+
sql = translate_sqlite_sql("INSERT OR IGNORE INTO memory_entity_edges VALUES (?,?,?,?)")
|
|
34
|
+
assert "ON CONFLICT DO NOTHING" in sql
|
|
35
|
+
assert "$4" in sql
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_split_script() -> None:
|
|
39
|
+
assert split_script("A;\nB;\n") == ["A", "B"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_postgres_url() -> None:
|
|
43
|
+
assert is_postgres_url("postgresql://localhost/db")
|
|
44
|
+
assert is_postgres_url("postgres://localhost/db")
|
|
45
|
+
assert not is_postgres_url("sqlite:///x.db")
|
|
46
|
+
assert normalize_postgres_url("postgresql+asyncpg://h/db") == "postgresql://h/db"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_external_postgres_pool_rejects_sqlite() -> None:
|
|
50
|
+
with pytest.raises(ConfigError, match="requires a PostgreSQL"):
|
|
51
|
+
open_store("sqlite:///:memory:", postgres_pool=object())
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.mark.asyncio
|
|
55
|
+
async def test_postgres_store_does_not_close_external_pool() -> None:
|
|
56
|
+
class FakePool:
|
|
57
|
+
def __init__(self) -> None:
|
|
58
|
+
self.closed = 0
|
|
59
|
+
|
|
60
|
+
async def close(self) -> None:
|
|
61
|
+
self.closed += 1
|
|
62
|
+
|
|
63
|
+
pool = FakePool()
|
|
64
|
+
store = PostgresStore("postgresql://example/contextdb", pool=pool)
|
|
65
|
+
await store.close()
|
|
66
|
+
assert pool.closed == 0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@pytest.mark.asyncio
|
|
70
|
+
async def test_list_by_entities_filters_and_copies_only_the_requested_limit(
|
|
71
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
72
|
+
) -> None:
|
|
73
|
+
store = PostgresStore(
|
|
74
|
+
"postgresql://example/contextdb",
|
|
75
|
+
tenant_id="tenant-1",
|
|
76
|
+
agent_id="agent-1",
|
|
77
|
+
pool=object(),
|
|
78
|
+
)
|
|
79
|
+
now = datetime.now(tz=timezone.utc)
|
|
80
|
+
entity_b = [
|
|
81
|
+
MemoryItem(
|
|
82
|
+
content=f"b-{index}",
|
|
83
|
+
entity_key="entity-b",
|
|
84
|
+
user_id="user-1",
|
|
85
|
+
tenant_id="tenant-1",
|
|
86
|
+
agent_id="agent-1",
|
|
87
|
+
)
|
|
88
|
+
for index in range(10)
|
|
89
|
+
]
|
|
90
|
+
entity_a = [
|
|
91
|
+
MemoryItem(
|
|
92
|
+
content=f"a-{index}",
|
|
93
|
+
entity_key="entity-a",
|
|
94
|
+
user_id="user-1",
|
|
95
|
+
tenant_id="tenant-1",
|
|
96
|
+
agent_id="agent-1",
|
|
97
|
+
metadata={"nested": [index]},
|
|
98
|
+
)
|
|
99
|
+
for index in range(20)
|
|
100
|
+
]
|
|
101
|
+
future = MemoryItem(
|
|
102
|
+
content="future",
|
|
103
|
+
entity_key="entity-a",
|
|
104
|
+
user_id="user-1",
|
|
105
|
+
tenant_id="tenant-1",
|
|
106
|
+
agent_id="agent-1",
|
|
107
|
+
valid_from=now + timedelta(days=1),
|
|
108
|
+
)
|
|
109
|
+
indexed = [*entity_b, future, *entity_a]
|
|
110
|
+
store._index_items = {item.id: item for item in indexed}
|
|
111
|
+
store._index_loaded = True
|
|
112
|
+
|
|
113
|
+
original_model_copy = MemoryItem.model_copy
|
|
114
|
+
copies = 0
|
|
115
|
+
|
|
116
|
+
def counted_model_copy(
|
|
117
|
+
self: MemoryItem,
|
|
118
|
+
*,
|
|
119
|
+
update: dict[str, Any] | None = None,
|
|
120
|
+
deep: bool = False,
|
|
121
|
+
) -> MemoryItem:
|
|
122
|
+
nonlocal copies
|
|
123
|
+
copies += 1
|
|
124
|
+
return original_model_copy(self, update=update, deep=deep)
|
|
125
|
+
|
|
126
|
+
monkeypatch.setattr(MemoryItem, "model_copy", counted_model_copy)
|
|
127
|
+
results = await store.list_by_entities(
|
|
128
|
+
["entity-a", "entity-b"],
|
|
129
|
+
user_id="user-1",
|
|
130
|
+
exclude_ids={entity_a[0].id},
|
|
131
|
+
valid_at=now,
|
|
132
|
+
limit=5,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
assert len(results) == 5
|
|
136
|
+
assert copies == 5
|
|
137
|
+
assert all(item.entity_key == "entity-a" for item in results)
|
|
138
|
+
assert entity_a[0].id not in {item.id for item in results}
|
|
139
|
+
results[0].metadata["nested"].append(99)
|
|
140
|
+
source = store._index_items[results[0].id]
|
|
141
|
+
assert source.metadata["nested"] != results[0].metadata["nested"]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@pytest.mark.asyncio
|
|
145
|
+
async def test_postgres_embedding_search_can_borrow_read_only_index_items(
|
|
146
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
147
|
+
) -> None:
|
|
148
|
+
store = PostgresStore(
|
|
149
|
+
"postgresql://example/contextdb",
|
|
150
|
+
tenant_id="tenant-1",
|
|
151
|
+
agent_id="agent-1",
|
|
152
|
+
pool=object(),
|
|
153
|
+
)
|
|
154
|
+
source = MemoryItem(
|
|
155
|
+
content="Thursday is confirmed",
|
|
156
|
+
embedding=[1.0, 0.0],
|
|
157
|
+
user_id="user-1",
|
|
158
|
+
tenant_id="tenant-1",
|
|
159
|
+
agent_id="agent-1",
|
|
160
|
+
metadata={"nested": ["source"]},
|
|
161
|
+
)
|
|
162
|
+
store._index_items = {source.id: source}
|
|
163
|
+
|
|
164
|
+
class FakeIndex:
|
|
165
|
+
def search(
|
|
166
|
+
self,
|
|
167
|
+
_query: object,
|
|
168
|
+
*,
|
|
169
|
+
top_k: int,
|
|
170
|
+
include_ids: set[str],
|
|
171
|
+
) -> list[tuple[str, float]]:
|
|
172
|
+
assert top_k == 1
|
|
173
|
+
assert include_ids == {source.id}
|
|
174
|
+
return [(source.id, 1.0)]
|
|
175
|
+
|
|
176
|
+
async def ensure_index() -> Any:
|
|
177
|
+
return FakeIndex()
|
|
178
|
+
|
|
179
|
+
monkeypatch.setattr(store, "_ensure_index", ensure_index)
|
|
180
|
+
copied = await store.search_by_embedding(
|
|
181
|
+
[1.0, 0.0],
|
|
182
|
+
top_k=1,
|
|
183
|
+
user_id="user-1",
|
|
184
|
+
)
|
|
185
|
+
borrowed = await store.search_by_embedding(
|
|
186
|
+
[1.0, 0.0],
|
|
187
|
+
top_k=1,
|
|
188
|
+
user_id="user-1",
|
|
189
|
+
copy_items=False,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
assert copied == [source]
|
|
193
|
+
assert copied[0] is not source
|
|
194
|
+
assert borrowed[0] is source
|
|
195
|
+
copied[0].metadata["nested"].append("caller")
|
|
196
|
+
assert source.metadata == {"nested": ["source"]}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@pytest.mark.asyncio
|
|
200
|
+
async def test_bound_adapter_reuses_advisory_transaction_connection() -> None:
|
|
201
|
+
class NoAcquirePool:
|
|
202
|
+
def acquire(self) -> None:
|
|
203
|
+
raise AssertionError("bound execution must not acquire another connection")
|
|
204
|
+
|
|
205
|
+
class FakeConnection:
|
|
206
|
+
def __init__(self) -> None:
|
|
207
|
+
self.statements: list[str] = []
|
|
208
|
+
|
|
209
|
+
async def fetch(self, sql: str, *params: object) -> list[dict[str, int]]:
|
|
210
|
+
self.statements.append(sql)
|
|
211
|
+
return [{"value": 1}]
|
|
212
|
+
|
|
213
|
+
async def execute(self, sql: str, *params: object) -> str:
|
|
214
|
+
self.statements.append(sql)
|
|
215
|
+
return "UPDATE 1"
|
|
216
|
+
|
|
217
|
+
adapter = _PgAdapter(NoAcquirePool())
|
|
218
|
+
connection = FakeConnection()
|
|
219
|
+
token = adapter.bind(connection)
|
|
220
|
+
try:
|
|
221
|
+
selected = await adapter.execute("SELECT value FROM example")
|
|
222
|
+
updated = await adapter.execute("UPDATE example SET value = ?", (2,))
|
|
223
|
+
finally:
|
|
224
|
+
adapter.reset(token)
|
|
225
|
+
|
|
226
|
+
assert await selected.fetchall() == [{"value": 1}]
|
|
227
|
+
assert updated.rowcount == 1
|
|
228
|
+
assert len(connection.statements) == 2
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Tests for multi-graph retrieval."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from contextdb.core.models import MemoryItem
|
|
8
|
+
from contextdb.dynamics.retrieval import (
|
|
9
|
+
QueryClassifier,
|
|
10
|
+
RetrievalEngine,
|
|
11
|
+
RetrievalFuser,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_query_classifier_weights() -> None:
|
|
16
|
+
qc = QueryClassifier()
|
|
17
|
+
weights_temporal = qc.classify("when did we last talk?")
|
|
18
|
+
assert "temporal" in weights_temporal
|
|
19
|
+
weights_causal = qc.classify("why did the build fail?")
|
|
20
|
+
assert "causal" in weights_causal
|
|
21
|
+
weights_generic = qc.classify("tell me about python")
|
|
22
|
+
assert weights_generic["semantic"] == 1.0
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_rrf_fusion() -> None:
|
|
26
|
+
fuser = RetrievalFuser(k=60)
|
|
27
|
+
rankings = {
|
|
28
|
+
"semantic": [("a", 0.9), ("b", 0.7), ("c", 0.5)],
|
|
29
|
+
"temporal": [("b", 1.0), ("c", 0.8)],
|
|
30
|
+
}
|
|
31
|
+
weights = {"semantic": 0.5, "temporal": 0.5}
|
|
32
|
+
fused = fuser.fuse(rankings, weights)
|
|
33
|
+
ranked_ids = [mid for mid, _ in fused]
|
|
34
|
+
# "b" appears higher than "a" on aggregate (rank 2 semantic + rank 1 temporal
|
|
35
|
+
# vs. rank 1 semantic only).
|
|
36
|
+
assert ranked_ids[0] == "b"
|
|
37
|
+
assert set(ranked_ids) == {"a", "b", "c"}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.mark.asyncio
|
|
41
|
+
async def test_retrieval_ranks_store_references_but_returns_detached_items() -> None:
|
|
42
|
+
source = MemoryItem(
|
|
43
|
+
content="Thursday is confirmed",
|
|
44
|
+
embedding=[1.0, 0.0],
|
|
45
|
+
metadata={"nested": ["source"]},
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
class ReferenceStore:
|
|
49
|
+
async def search_by_embedding(
|
|
50
|
+
self,
|
|
51
|
+
_embedding: list[float],
|
|
52
|
+
top_k: int = 10,
|
|
53
|
+
filters: dict[str, object] | None = None,
|
|
54
|
+
user_id: str | None = None,
|
|
55
|
+
*,
|
|
56
|
+
copy_items: bool = True,
|
|
57
|
+
) -> list[MemoryItem]:
|
|
58
|
+
assert top_k == 2
|
|
59
|
+
assert filters is None
|
|
60
|
+
assert user_id == "user-1"
|
|
61
|
+
assert copy_items is False
|
|
62
|
+
return [source]
|
|
63
|
+
|
|
64
|
+
async def get_raw(self, _memory_id: str) -> MemoryItem | None:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
engine = RetrievalEngine(
|
|
68
|
+
ReferenceStore(), # type: ignore[arg-type]
|
|
69
|
+
{},
|
|
70
|
+
QueryClassifier(),
|
|
71
|
+
RetrievalFuser(),
|
|
72
|
+
)
|
|
73
|
+
results = await engine.search_scored(
|
|
74
|
+
"visit day",
|
|
75
|
+
[1.0, 0.0],
|
|
76
|
+
top_k=1,
|
|
77
|
+
user_id="user-1",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
assert len(results) == 1
|
|
81
|
+
assert results[0].item == source
|
|
82
|
+
assert results[0].item is not source
|
|
83
|
+
results[0].item.metadata["nested"].append("caller")
|
|
84
|
+
assert source.metadata == {"nested": ["source"]}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
from datetime import datetime, timedelta, timezone
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
8
9
|
import pytest
|
|
@@ -174,6 +175,44 @@ async def test_list_and_count(tmp_store: SQLiteStore) -> None:
|
|
|
174
175
|
assert len(items) == 3
|
|
175
176
|
|
|
176
177
|
|
|
178
|
+
@pytest.mark.asyncio
|
|
179
|
+
async def test_list_by_entities_applies_composition_filters(
|
|
180
|
+
tmp_store: SQLiteStore,
|
|
181
|
+
) -> None:
|
|
182
|
+
now = datetime.now(tz=timezone.utc)
|
|
183
|
+
first = await tmp_store.add(
|
|
184
|
+
MemoryItem(
|
|
185
|
+
content="first",
|
|
186
|
+
entity_key="customer",
|
|
187
|
+
attribute_key="first",
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
second = await tmp_store.add(
|
|
191
|
+
MemoryItem(
|
|
192
|
+
content="second",
|
|
193
|
+
entity_key="customer",
|
|
194
|
+
attribute_key="second",
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
await tmp_store.add(
|
|
198
|
+
MemoryItem(
|
|
199
|
+
content="future",
|
|
200
|
+
entity_key="customer",
|
|
201
|
+
attribute_key="future",
|
|
202
|
+
valid_from=now + timedelta(days=1),
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
results = await tmp_store.list_by_entities(
|
|
207
|
+
["customer"],
|
|
208
|
+
exclude_ids={first.id},
|
|
209
|
+
valid_at=now,
|
|
210
|
+
limit=1,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
assert [item.id for item in results] == [second.id]
|
|
214
|
+
|
|
215
|
+
|
|
177
216
|
@pytest.mark.asyncio
|
|
178
217
|
async def test_search_by_embedding(tmp_store: SQLiteStore) -> None:
|
|
179
218
|
a = await tmp_store.add(MemoryItem(content="a", embedding=[1.0] + [0.0] * 31))
|
|
@@ -249,8 +288,6 @@ async def test_iter_memories_paginates(tmp_store: SQLiteStore) -> None:
|
|
|
249
288
|
@pytest.mark.asyncio
|
|
250
289
|
async def test_delete_older_than(tmp_store: SQLiteStore) -> None:
|
|
251
290
|
"""delete_older_than issues a single SQL delete."""
|
|
252
|
-
from datetime import datetime, timezone
|
|
253
|
-
|
|
254
291
|
item = await tmp_store.add(MemoryItem(content="x", embedding=[0.0] * 32))
|
|
255
292
|
future = datetime.now(tz=timezone.utc).replace(year=2099).isoformat()
|
|
256
293
|
removed = await tmp_store.delete_older_than(future, hard=True)
|