pycontextdb 0.4.3__tar.gz → 0.4.5__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.3 → pycontextdb-0.4.5}/CHANGELOG.md +21 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/PKG-INFO +2 -2
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/README.md +1 -1
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/__init__.py +1 -1
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/client.py +5 -13
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/dynamics/evolution.py +3 -2
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/store/postgres_store.py +29 -1
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/docs/architecture.md +5 -2
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/pyproject.toml +1 -1
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/evals/test_trust_model.py +36 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/unit/test_evolution.py +26 -1
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/unit/test_pg_sql.py +33 -1
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/.github/CODEOWNERS +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/.github/copilot-instructions.md +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/.github/pull_request_template.md +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/.github/workflows/ci.yml +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/.github/workflows/publish.yml +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/.gitignore +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/.pre-commit-config.yaml +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/COMMERCIAL.md +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/CONTRIBUTING.md +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/LICENSE +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/NOTICE +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/OPEN_CORE.md +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/ai-tools/.cursorrules +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/ai-tools/CLAUDE.md +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/ai-tools/README.md +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/ai-tools/llms-full.txt +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/ai-tools/llms.txt +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/assets/README.md +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/assets/architecture.png +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/assets/benchmarks.png +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/assets/hero.png +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/assets/performance.png +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/assets/problem.png +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/assets/quickstart.png +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/benchmarks/__init__.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/benchmarks/run_benchmarks.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/benchmarks/trust_bakeoff.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/agents/__init__.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/agents/memory_bus.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/agents/rl_manager.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/cli.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/connectors/__init__.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/connectors/base.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/connectors/postgres.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/core/__init__.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/core/clock.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/core/config.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/core/exceptions.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/core/models.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/core/policy.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/core/slots.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/dynamics/__init__.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/dynamics/formation.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/dynamics/retrieval.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/dynamics/salience.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/dynamics/trust.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/graphs/__init__.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/graphs/base.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/graphs/causal.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/graphs/entity.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/graphs/semantic.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/graphs/temporal.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/integrations/__init__.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/integrations/act.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/integrations/autogen.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/integrations/crewai.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/integrations/langchain.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/integrations/livekit.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/integrations/openai_tools.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/integrations/pipecat.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/integrations/prompting.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/mcp.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/memory/__init__.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/memory/experiential.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/memory/factual.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/memory/working.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/pool.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/privacy/__init__.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/privacy/audit.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/privacy/injection.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/privacy/pii_detector.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/privacy/retention.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/py.typed +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/serve.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/store/__init__.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/store/base.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/store/factory.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/store/pg_sql.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/store/sqlite_store.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/store/vector_index.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/utils/__init__.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/utils/embeddings.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/utils/llm.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/contextdb/utils/migrations.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/docs/api.md +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/docs/connectors.md +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/docs/index.md +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/docs/memory_evolution.md +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/docs/migrations.md +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/docs/multi_tenant.md +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/docs/postgres.md +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/docs/prompts.md +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/docs/quickstart.md +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/docs/serve.md +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/docs/trust_policy.md +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/examples/customer_support.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/examples/multi_agent_team.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/examples/phone_agent.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/examples/research_assistant.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/legal/CLA-entity.md +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/legal/CLA-individual.md +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/legal/signed/README.md +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/mkdocs.yml +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/scripts/check_open_core_boundary.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/scripts/check_release.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/__init__.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/conftest.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/evals/__init__.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/integration/__init__.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/integration/test_client.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/integration/test_langchain_integration.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/pg_util.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/test_version.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/unit/__init__.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/unit/test_audit.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/unit/test_config.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/unit/test_connectors.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/unit/test_decision_scope.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/unit/test_embeddings.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/unit/test_formation.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/unit/test_graphs.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/unit/test_host_api.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/unit/test_init.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/unit/test_memory_bus.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/unit/test_memory_evolution.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/unit/test_models.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/unit/test_open_core_boundary.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/unit/test_pii_detector.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/unit/test_postgres_concurrency.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/unit/test_postgres_connector.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/unit/test_postgres_memory_evolution.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/unit/test_retention.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/unit/test_retrieval.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/unit/test_scoped_id_access.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/unit/test_serve.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/unit/test_store.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/unit/test_trust_policy.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/unit/test_vector_index.py +0 -0
- {pycontextdb-0.4.3 → pycontextdb-0.4.5}/tests/unit/test_vector_scope.py +0 -0
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.4.5 — 2026-10-05
|
|
6
|
+
|
|
7
|
+
- `forget_user` no longer deletes other users' memories. It added every graph
|
|
8
|
+
neighbor of the erased memories to the deletion set, and entity edges join
|
|
9
|
+
any memories that mention the same name, so on a shared client with the
|
|
10
|
+
entity graph and an extraction model, erasing one user could hard-delete
|
|
11
|
+
another user's memories. It now deletes only the user's memories and the
|
|
12
|
+
summaries derived from them, and severs their graph edges.
|
|
13
|
+
- Consolidated summaries keep the weakest corroboration count of their inputs
|
|
14
|
+
instead of the sum. Repeated copies of one third-party claim could
|
|
15
|
+
consolidate into a summary that passed the default trust bar and was
|
|
16
|
+
returned by `recall_for_action`. The summary now stays pending confirmation.
|
|
17
|
+
- Unchanged limitation: entity rows are shared across users, and attributes
|
|
18
|
+
merged into an entity from a user's text are not removed by `forget_user`.
|
|
19
|
+
|
|
20
|
+
## 0.4.4 — 2026-09-20
|
|
21
|
+
|
|
22
|
+
- PostgreSQL sibling composition now uses an ordered user-plus-entity cache
|
|
23
|
+
index, so a scoped recall does not scan other users that share the same
|
|
24
|
+
entity key.
|
|
25
|
+
|
|
5
26
|
## 0.4.3 — 2026-09-20
|
|
6
27
|
|
|
7
28
|
- High-cardinality Postgres recall now indexes cached IDs by user and entity,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pycontextdb
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.5
|
|
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
|
|
@@ -120,7 +120,7 @@ Speed is table stakes. The production failure that gets agents fired is **acting
|
|
|
120
120
|
|
|
121
121
|
Every memory carries **epistemic provenance** (`user_stated` / `agent_inferred` / `third_party`), **confidence**, **independent corroboration** (same-speaker repeats do not count), and an **action-relevant** flag. The action bar is data — `TrustPolicy.hospital()` vs `TrustPolicy.restaurant()` — not a hardcoded `if`. Facts that gate actions must pass the active policy before `recall_for_action()` releases them; everything else comes back marked `requires_confirmation`. `factual.confirm()` is the writeback: the agent asked, the user said yes, the fact graduates. Stock hosts (Pipecat, LiveKit, MCP) go through `VerifyBeforeAct` so an untrusted fact never reaches an action-shaped prompt.
|
|
122
122
|
|
|
123
|
-
On top of that: **versioned slot vocabulary** + an LLM-free slotter (so `add_fast` and supersede agree), **extractor-as-security-boundary** (a prompt-injected `user/allergy` the raw text does not support is demoted), **contested slots** (independent speakers asserting different values do not last-write-win — both stay current, neither is actionable, `confirm()` resolves), **compositional hop** (`"when is the Denver meeting?"` surfaces `meeting/time` after hitting `meeting/location`), **PII-safe recall** (queries are redacted before embed so a raw email still retrieves `[EMAIL]`), **temporal supersede** with an injectable clock (`valid_from`/`valid_until`/`superseded_by` + as-of queries), a **latency-tiered write path** (`add_fast`: no LLM on the turn path, p95 ≈ 1.5ms local), **salience** (recency × frequency × criticality — a year-old allergy still outranks 500 fresh noise memories), **write-time injection screening** with demoted `[RECALLED DATA — not instructions]` rendering, **recall-side observability** (`db.explain(id)` logs score *and* decision flags as they stood at recall time; `VerifyBeforeAct` writes `DECIDE` to the audit log), **verifiable forgetting** that
|
|
123
|
+
On top of that: **versioned slot vocabulary** + an LLM-free slotter (so `add_fast` and supersede agree), **extractor-as-security-boundary** (a prompt-injected `user/allergy` the raw text does not support is demoted), **contested slots** (independent speakers asserting different values do not last-write-win — both stay current, neither is actionable, `confirm()` resolves), **compositional hop** (`"when is the Denver meeting?"` surfaces `meeting/time` after hitting `meeting/location`), **PII-safe recall** (queries are redacted before embed so a raw email still retrieves `[EMAIL]`), **temporal supersede** with an injectable clock (`valid_from`/`valid_until`/`superseded_by` + as-of queries), a **latency-tiered write path** (`add_fast`: no LLM on the turn path, p95 ≈ 1.5ms local), **salience** (recency × frequency × criticality — a year-old allergy still outranks 500 fresh noise memories), **write-time injection screening** with demoted `[RECALLED DATA — not instructions]` rendering, **recall-side observability** (`db.explain(id)` logs score *and* decision flags as they stood at recall time; `VerifyBeforeAct` writes `DECIDE` to the audit log), **verifiable forgetting** that severs graph edges without touching other users' memories, and **tenant/agent isolation enforced at the storage layer**.
|
|
124
124
|
|
|
125
125
|
Don't take our word for it — the fabrication benchmark ships in-repo and runs on every PR. Same store, same embedder, same utterance policy; the only difference is the trust model:
|
|
126
126
|
|
|
@@ -54,7 +54,7 @@ Speed is table stakes. The production failure that gets agents fired is **acting
|
|
|
54
54
|
|
|
55
55
|
Every memory carries **epistemic provenance** (`user_stated` / `agent_inferred` / `third_party`), **confidence**, **independent corroboration** (same-speaker repeats do not count), and an **action-relevant** flag. The action bar is data — `TrustPolicy.hospital()` vs `TrustPolicy.restaurant()` — not a hardcoded `if`. Facts that gate actions must pass the active policy before `recall_for_action()` releases them; everything else comes back marked `requires_confirmation`. `factual.confirm()` is the writeback: the agent asked, the user said yes, the fact graduates. Stock hosts (Pipecat, LiveKit, MCP) go through `VerifyBeforeAct` so an untrusted fact never reaches an action-shaped prompt.
|
|
56
56
|
|
|
57
|
-
On top of that: **versioned slot vocabulary** + an LLM-free slotter (so `add_fast` and supersede agree), **extractor-as-security-boundary** (a prompt-injected `user/allergy` the raw text does not support is demoted), **contested slots** (independent speakers asserting different values do not last-write-win — both stay current, neither is actionable, `confirm()` resolves), **compositional hop** (`"when is the Denver meeting?"` surfaces `meeting/time` after hitting `meeting/location`), **PII-safe recall** (queries are redacted before embed so a raw email still retrieves `[EMAIL]`), **temporal supersede** with an injectable clock (`valid_from`/`valid_until`/`superseded_by` + as-of queries), a **latency-tiered write path** (`add_fast`: no LLM on the turn path, p95 ≈ 1.5ms local), **salience** (recency × frequency × criticality — a year-old allergy still outranks 500 fresh noise memories), **write-time injection screening** with demoted `[RECALLED DATA — not instructions]` rendering, **recall-side observability** (`db.explain(id)` logs score *and* decision flags as they stood at recall time; `VerifyBeforeAct` writes `DECIDE` to the audit log), **verifiable forgetting** that
|
|
57
|
+
On top of that: **versioned slot vocabulary** + an LLM-free slotter (so `add_fast` and supersede agree), **extractor-as-security-boundary** (a prompt-injected `user/allergy` the raw text does not support is demoted), **contested slots** (independent speakers asserting different values do not last-write-win — both stay current, neither is actionable, `confirm()` resolves), **compositional hop** (`"when is the Denver meeting?"` surfaces `meeting/time` after hitting `meeting/location`), **PII-safe recall** (queries are redacted before embed so a raw email still retrieves `[EMAIL]`), **temporal supersede** with an injectable clock (`valid_from`/`valid_until`/`superseded_by` + as-of queries), a **latency-tiered write path** (`add_fast`: no LLM on the turn path, p95 ≈ 1.5ms local), **salience** (recency × frequency × criticality — a year-old allergy still outranks 500 fresh noise memories), **write-time injection screening** with demoted `[RECALLED DATA — not instructions]` rendering, **recall-side observability** (`db.explain(id)` logs score *and* decision flags as they stood at recall time; `VerifyBeforeAct` writes `DECIDE` to the audit log), **verifiable forgetting** that severs graph edges without touching other users' memories, and **tenant/agent isolation enforced at the storage layer**.
|
|
58
58
|
|
|
59
59
|
Don't take our word for it — the fabrication benchmark ships in-repo and runs on every PR. Same store, same embedder, same utterance policy; the only difference is the trust model:
|
|
60
60
|
|
|
@@ -1685,21 +1685,13 @@ class ContextDB:
|
|
|
1685
1685
|
if added == 0:
|
|
1686
1686
|
break
|
|
1687
1687
|
|
|
1688
|
-
#
|
|
1689
|
-
#
|
|
1690
|
-
# the
|
|
1688
|
+
# Sever graph edges so a forgotten memory cannot be reached or
|
|
1689
|
+
# rebuilt through the graph. Neighbors are never added to the
|
|
1690
|
+
# deletion set: entity edges join every memory that mentions the
|
|
1691
|
+
# same name, including other users' memories, and this user's own
|
|
1692
|
+
# and derived memories are already in ``targets``.
|
|
1691
1693
|
for graph in self._graphs.values():
|
|
1692
1694
|
for memory_id in list(targets):
|
|
1693
|
-
try:
|
|
1694
|
-
neighbors = await graph.get_neighbors(memory_id, max_results=50)
|
|
1695
|
-
except Exception: # noqa: BLE001
|
|
1696
|
-
continue
|
|
1697
|
-
for nid, _weight in neighbors:
|
|
1698
|
-
if nid in targets:
|
|
1699
|
-
continue
|
|
1700
|
-
neighbor = await store.get_raw(nid)
|
|
1701
|
-
if neighbor is not None:
|
|
1702
|
-
targets[nid] = neighbor
|
|
1703
1695
|
try:
|
|
1704
1696
|
await graph.remove_node(memory_id)
|
|
1705
1697
|
except Exception: # noqa: BLE001
|
|
@@ -98,7 +98,8 @@ class Consolidator:
|
|
|
98
98
|
# Trust-conservative merge: a summary must never be MORE trusted
|
|
99
99
|
# than its least trusted input. Otherwise consolidation launders
|
|
100
100
|
# epistemic status — five third-party hearsay fragments would
|
|
101
|
-
# emerge as a confident first-party fact.
|
|
101
|
+
# emerge as a confident first-party fact. Corroboration is not
|
|
102
|
+
# summed: copies of one claim are not independent support.
|
|
102
103
|
new_item = MemoryItem(
|
|
103
104
|
content=summary,
|
|
104
105
|
embedding=cluster_items[0].embedding,
|
|
@@ -110,7 +111,7 @@ class Consolidator:
|
|
|
110
111
|
key=lambda s: _SOURCE_RANK[s],
|
|
111
112
|
),
|
|
112
113
|
confidence=min(m.confidence for m in cluster_items),
|
|
113
|
-
corroboration_count=
|
|
114
|
+
corroboration_count=min(m.independent_corroboration for m in cluster_items),
|
|
114
115
|
action_relevant=any(m.action_relevant for m in cluster_items),
|
|
115
116
|
injection_suspect=any(m.injection_suspect for m in cluster_items),
|
|
116
117
|
)
|
|
@@ -282,6 +282,10 @@ class PostgresStore(BaseStore):
|
|
|
282
282
|
self._index_items: dict[str, MemoryItem] = {}
|
|
283
283
|
self._index_ids_by_user: dict[str | None, set[str]] = {}
|
|
284
284
|
self._index_ids_by_entity: dict[str, dict[str, None]] = {}
|
|
285
|
+
self._index_ids_by_user_entity: dict[
|
|
286
|
+
tuple[str | None, str],
|
|
287
|
+
dict[str, None],
|
|
288
|
+
] = {}
|
|
285
289
|
self._embedding_dim = embedding_dim
|
|
286
290
|
self._embedding_model_id = embedding_model_id
|
|
287
291
|
self._index_loaded = False
|
|
@@ -389,6 +393,10 @@ class PostgresStore(BaseStore):
|
|
|
389
393
|
self._index_ids_by_user.setdefault(item.user_id, set()).add(item.id)
|
|
390
394
|
if item.entity_key is not None:
|
|
391
395
|
self._index_ids_by_entity.setdefault(item.entity_key, {})[item.id] = None
|
|
396
|
+
self._index_ids_by_user_entity.setdefault(
|
|
397
|
+
(item.user_id, item.entity_key),
|
|
398
|
+
{},
|
|
399
|
+
)[item.id] = None
|
|
392
400
|
|
|
393
401
|
def _discard_index_item(self, memory_id: str) -> None:
|
|
394
402
|
item = self._index_items.pop(memory_id, None)
|
|
@@ -405,11 +413,22 @@ class PostgresStore(BaseStore):
|
|
|
405
413
|
entity_ids.pop(memory_id, None)
|
|
406
414
|
if not entity_ids:
|
|
407
415
|
self._index_ids_by_entity.pop(item.entity_key, None)
|
|
416
|
+
scoped_entity_ids = self._index_ids_by_user_entity.get(
|
|
417
|
+
(item.user_id, item.entity_key)
|
|
418
|
+
)
|
|
419
|
+
if scoped_entity_ids is not None:
|
|
420
|
+
scoped_entity_ids.pop(memory_id, None)
|
|
421
|
+
if not scoped_entity_ids:
|
|
422
|
+
self._index_ids_by_user_entity.pop(
|
|
423
|
+
(item.user_id, item.entity_key),
|
|
424
|
+
None,
|
|
425
|
+
)
|
|
408
426
|
|
|
409
427
|
def _clear_index_items(self) -> None:
|
|
410
428
|
self._index_items.clear()
|
|
411
429
|
self._index_ids_by_user.clear()
|
|
412
430
|
self._index_ids_by_entity.clear()
|
|
431
|
+
self._index_ids_by_user_entity.clear()
|
|
413
432
|
|
|
414
433
|
def _scope_sql(self, user_id: str | None = None) -> tuple[str, list[Any]]:
|
|
415
434
|
clauses: list[str] = []
|
|
@@ -1101,8 +1120,17 @@ class PostgresStore(BaseStore):
|
|
|
1101
1120
|
await self._ensure_index()
|
|
1102
1121
|
excluded = exclude_ids or set()
|
|
1103
1122
|
items: list[MemoryItem] = []
|
|
1123
|
+
scope = self._resolve_scope(user_id)
|
|
1104
1124
|
for entity_key in dict.fromkeys(entity_keys):
|
|
1105
|
-
|
|
1125
|
+
entity_ids = (
|
|
1126
|
+
self._index_ids_by_entity.get(entity_key, {})
|
|
1127
|
+
if scope is None
|
|
1128
|
+
else self._index_ids_by_user_entity.get(
|
|
1129
|
+
(scope, entity_key),
|
|
1130
|
+
{},
|
|
1131
|
+
)
|
|
1132
|
+
)
|
|
1133
|
+
for memory_id in entity_ids:
|
|
1106
1134
|
item = self._index_items.get(memory_id)
|
|
1107
1135
|
if item is None:
|
|
1108
1136
|
continue
|
|
@@ -41,7 +41,8 @@ Three pipelines operate over the storage layer:
|
|
|
41
41
|
NOOP operations with lineage and consistency results. Separately, the
|
|
42
42
|
auto-linker mirrors each new write into graph indices; the consolidator
|
|
43
43
|
merges dense semantic clusters into summaries that inherit *worst-case*
|
|
44
|
-
trust (no laundering); the
|
|
44
|
+
trust, including the weakest corroboration count (no laundering); the
|
|
45
|
+
pruner drops stale / redundant memories by
|
|
45
46
|
policy.
|
|
46
47
|
3. **Retrieval** — a query becomes an answer. The query is PII-redacted
|
|
47
48
|
before embed. Query classifier picks graph weights; each graph produces
|
|
@@ -78,7 +79,9 @@ search, `DECIDE` (act/ask/abstain), and deletion — and because the chain
|
|
|
78
79
|
is append-only, only the PII-processed form of a query is ever logged.
|
|
79
80
|
`pii_action="encrypt"` fails closed: without a key the client refuses to
|
|
80
81
|
initialize rather than store plaintext annotation originals.
|
|
81
|
-
`forget_user`
|
|
82
|
+
`forget_user` deletes the user's memories and the summaries derived from them,
|
|
83
|
+
severs their graph edges, and signs the deletion set. It never deletes another
|
|
84
|
+
user's memory through a shared edge. The retention
|
|
82
85
|
manager applies typed TTLs and honors right-to-erasure requests. Isolation
|
|
83
86
|
is a store predicate (`user_id` / `tenant_id` / `agent_id`), not a
|
|
84
87
|
convention.
|
|
@@ -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.5"
|
|
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"
|
|
@@ -813,6 +813,42 @@ async def test_eval_7_1_forget_user_leaves_zero_residue(tmp_path: Path) -> None:
|
|
|
813
813
|
await db.close()
|
|
814
814
|
|
|
815
815
|
|
|
816
|
+
async def test_eval_7_2_forget_user_spares_other_users_sharing_an_entity(
|
|
817
|
+
tmp_path: Path,
|
|
818
|
+
) -> None:
|
|
819
|
+
"""EVAL-7.2: on a shared client, erasing one user must not delete another
|
|
820
|
+
user's memory because both mention the same entity."""
|
|
821
|
+
db = contextdb.init(
|
|
822
|
+
tenant_id="org-shared",
|
|
823
|
+
config=make_config(tmp_path, enable_entity_graph=True),
|
|
824
|
+
)
|
|
825
|
+
db._llm = MockLLM(
|
|
826
|
+
responses={"Extract": '{"entities": [{"name": "Acme", "type": "ORGANIZATION"}]}'}
|
|
827
|
+
)
|
|
828
|
+
try:
|
|
829
|
+
mine = await db.factual.add(
|
|
830
|
+
"Prefers Friday calls about the Acme account.",
|
|
831
|
+
source="user_stated",
|
|
832
|
+
user_id="alice",
|
|
833
|
+
)
|
|
834
|
+
theirs = await db.factual.add(
|
|
835
|
+
"Escalation contact for the Acme renewal.",
|
|
836
|
+
source="user_stated",
|
|
837
|
+
user_id="bob",
|
|
838
|
+
)
|
|
839
|
+
graph = db._graphs["entity"]
|
|
840
|
+
assert [nid for nid, _ in await graph.get_neighbors(mine.id)] == [theirs.id]
|
|
841
|
+
|
|
842
|
+
deleted = await db.forget_user("alice")
|
|
843
|
+
|
|
844
|
+
assert deleted == 1
|
|
845
|
+
assert await db.verify_forgotten("alice") is True
|
|
846
|
+
assert [m.id for m in await db.factual.list_facts(user_id="bob")] == [theirs.id]
|
|
847
|
+
assert await graph.get_neighbors(theirs.id) == []
|
|
848
|
+
finally:
|
|
849
|
+
await db.close()
|
|
850
|
+
|
|
851
|
+
|
|
816
852
|
# ---------------------------------------------------------------------------
|
|
817
853
|
# Epic 8 — Realtime + agent-host integrations
|
|
818
854
|
# ---------------------------------------------------------------------------
|
|
@@ -22,9 +22,34 @@ async def test_consolidation_merges_with_worst_case_trust(client: ContextDB) ->
|
|
|
22
22
|
assert summary.source == "consolidator"
|
|
23
23
|
assert summary.epistemic_source == "third_party"
|
|
24
24
|
assert summary.confidence == 0.4
|
|
25
|
-
assert summary.corroboration_count ==
|
|
25
|
+
assert summary.corroboration_count == 1
|
|
26
26
|
assert summary.action_relevant is True
|
|
27
27
|
assert summary.metadata["consolidated_from"]
|
|
28
|
+
assert client.trust_policy.is_trusted(summary) is False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def test_consolidated_copies_of_one_rumor_still_require_confirmation(
|
|
32
|
+
client: ContextDB,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Five copies of one third-party rumor are one source, not five. The
|
|
35
|
+
summary must stay pending confirmation instead of passing the trust bar."""
|
|
36
|
+
copies = [
|
|
37
|
+
await client.factual.add(
|
|
38
|
+
"Forum post says the refund for the annual plan was approved.",
|
|
39
|
+
source="third_party",
|
|
40
|
+
action_relevant=True,
|
|
41
|
+
)
|
|
42
|
+
for _ in range(5)
|
|
43
|
+
]
|
|
44
|
+
assert all(copy.requires_confirmation_under(client.trust_policy) for copy in copies)
|
|
45
|
+
|
|
46
|
+
summaries = await client.consolidate(min_cluster_size=5)
|
|
47
|
+
|
|
48
|
+
assert summaries, "identical copies should have merged"
|
|
49
|
+
summary = summaries[0]
|
|
50
|
+
assert summary.independent_corroboration == 1
|
|
51
|
+
assert summary.requires_confirmation_under(client.trust_policy)
|
|
52
|
+
assert await client.factual.recall_for_action("issue the annual plan refund") == []
|
|
28
53
|
|
|
29
54
|
|
|
30
55
|
async def test_consolidation_screens_summary_for_injection(client: ContextDB) -> None:
|
|
@@ -98,6 +98,16 @@ async def test_list_by_entities_filters_and_copies_only_the_requested_limit(
|
|
|
98
98
|
)
|
|
99
99
|
for index in range(20)
|
|
100
100
|
]
|
|
101
|
+
foreign = [
|
|
102
|
+
MemoryItem(
|
|
103
|
+
content=f"foreign-{index}",
|
|
104
|
+
entity_key="entity-a",
|
|
105
|
+
user_id="user-2",
|
|
106
|
+
tenant_id="tenant-1",
|
|
107
|
+
agent_id="agent-1",
|
|
108
|
+
)
|
|
109
|
+
for index in range(1_000)
|
|
110
|
+
]
|
|
101
111
|
future = MemoryItem(
|
|
102
112
|
content="future",
|
|
103
113
|
entity_key="entity-a",
|
|
@@ -106,13 +116,15 @@ async def test_list_by_entities_filters_and_copies_only_the_requested_limit(
|
|
|
106
116
|
agent_id="agent-1",
|
|
107
117
|
valid_from=now + timedelta(days=1),
|
|
108
118
|
)
|
|
109
|
-
indexed = [*entity_b, future, *entity_a]
|
|
119
|
+
indexed = [*foreign, *entity_b, future, *entity_a]
|
|
110
120
|
for item in indexed:
|
|
111
121
|
store._cache_index_item(item)
|
|
112
122
|
store._index_loaded = True
|
|
113
123
|
|
|
114
124
|
original_model_copy = MemoryItem.model_copy
|
|
125
|
+
original_scope_allows = store._item_scope_allows
|
|
115
126
|
copies = 0
|
|
127
|
+
scope_checks = 0
|
|
116
128
|
|
|
117
129
|
def counted_model_copy(
|
|
118
130
|
self: MemoryItem,
|
|
@@ -125,6 +137,13 @@ async def test_list_by_entities_filters_and_copies_only_the_requested_limit(
|
|
|
125
137
|
return original_model_copy(self, update=update, deep=deep)
|
|
126
138
|
|
|
127
139
|
monkeypatch.setattr(MemoryItem, "model_copy", counted_model_copy)
|
|
140
|
+
|
|
141
|
+
def counted_scope_allows(item: MemoryItem, user_id: str | None) -> bool:
|
|
142
|
+
nonlocal scope_checks
|
|
143
|
+
scope_checks += 1
|
|
144
|
+
return original_scope_allows(item, user_id)
|
|
145
|
+
|
|
146
|
+
monkeypatch.setattr(store, "_item_scope_allows", counted_scope_allows)
|
|
128
147
|
results = await store.list_by_entities(
|
|
129
148
|
["entity-a", "entity-b"],
|
|
130
149
|
user_id="user-1",
|
|
@@ -135,6 +154,7 @@ async def test_list_by_entities_filters_and_copies_only_the_requested_limit(
|
|
|
135
154
|
|
|
136
155
|
assert len(results) == 5
|
|
137
156
|
assert copies == 5
|
|
157
|
+
assert scope_checks == 7
|
|
138
158
|
assert all(item.entity_key == "entity-a" for item in results)
|
|
139
159
|
assert entity_a[0].id not in {item.id for item in results}
|
|
140
160
|
results[0].metadata["nested"].append(99)
|
|
@@ -238,6 +258,12 @@ def test_postgres_secondary_indexes_follow_cache_lifecycle() -> None:
|
|
|
238
258
|
}
|
|
239
259
|
assert list(store._index_ids_by_entity["entity-a"]) == [first.id]
|
|
240
260
|
assert list(store._index_ids_by_entity["entity-b"]) == [second.id]
|
|
261
|
+
assert list(store._index_ids_by_user_entity["user-1", "entity-a"]) == [
|
|
262
|
+
first.id
|
|
263
|
+
]
|
|
264
|
+
assert list(store._index_ids_by_user_entity["user-2", "entity-b"]) == [
|
|
265
|
+
second.id
|
|
266
|
+
]
|
|
241
267
|
|
|
242
268
|
replacement = first.model_copy(
|
|
243
269
|
update={"user_id": "user-3", "entity_key": "entity-c"}
|
|
@@ -245,18 +271,24 @@ def test_postgres_secondary_indexes_follow_cache_lifecycle() -> None:
|
|
|
245
271
|
store._cache_index_item(replacement)
|
|
246
272
|
assert "user-1" not in store._index_ids_by_user
|
|
247
273
|
assert "entity-a" not in store._index_ids_by_entity
|
|
274
|
+
assert ("user-1", "entity-a") not in store._index_ids_by_user_entity
|
|
248
275
|
assert store._index_ids_by_user["user-3"] == {first.id}
|
|
249
276
|
assert list(store._index_ids_by_entity["entity-c"]) == [first.id]
|
|
277
|
+
assert list(store._index_ids_by_user_entity["user-3", "entity-c"]) == [
|
|
278
|
+
first.id
|
|
279
|
+
]
|
|
250
280
|
|
|
251
281
|
store._discard_index_item(first.id)
|
|
252
282
|
assert "user-3" not in store._index_ids_by_user
|
|
253
283
|
assert "entity-c" not in store._index_ids_by_entity
|
|
284
|
+
assert ("user-3", "entity-c") not in store._index_ids_by_user_entity
|
|
254
285
|
assert first.id not in store._index_items
|
|
255
286
|
|
|
256
287
|
store._clear_index_items()
|
|
257
288
|
assert store._index_items == {}
|
|
258
289
|
assert store._index_ids_by_user == {}
|
|
259
290
|
assert store._index_ids_by_entity == {}
|
|
291
|
+
assert store._index_ids_by_user_entity == {}
|
|
260
292
|
|
|
261
293
|
|
|
262
294
|
@pytest.mark.asyncio
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|