pycontextdb 0.4.4__tar.gz → 0.4.6__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.4 → pycontextdb-0.4.6}/CHANGELOG.md +35 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/PKG-INFO +3 -3
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/README.md +2 -2
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/__init__.py +1 -1
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/client.py +62 -16
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/core/slots.py +20 -1
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/dynamics/evolution.py +3 -2
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/dynamics/trust.py +74 -28
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/memory/factual.py +20 -5
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/docs/architecture.md +5 -2
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/pyproject.toml +1 -1
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/evals/test_trust_model.py +241 -1
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/unit/test_evolution.py +26 -1
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/.github/CODEOWNERS +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/.github/copilot-instructions.md +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/.github/pull_request_template.md +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/.github/workflows/ci.yml +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/.github/workflows/publish.yml +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/.gitignore +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/.pre-commit-config.yaml +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/COMMERCIAL.md +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/CONTRIBUTING.md +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/LICENSE +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/NOTICE +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/OPEN_CORE.md +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/ai-tools/.cursorrules +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/ai-tools/CLAUDE.md +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/ai-tools/README.md +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/ai-tools/llms-full.txt +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/ai-tools/llms.txt +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/assets/README.md +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/assets/architecture.png +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/assets/benchmarks.png +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/assets/hero.png +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/assets/performance.png +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/assets/problem.png +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/assets/quickstart.png +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/benchmarks/__init__.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/benchmarks/run_benchmarks.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/benchmarks/trust_bakeoff.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/agents/__init__.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/agents/memory_bus.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/agents/rl_manager.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/cli.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/connectors/__init__.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/connectors/base.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/connectors/postgres.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/core/__init__.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/core/clock.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/core/config.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/core/exceptions.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/core/models.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/core/policy.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/dynamics/__init__.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/dynamics/formation.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/dynamics/retrieval.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/dynamics/salience.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/graphs/__init__.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/graphs/base.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/graphs/causal.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/graphs/entity.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/graphs/semantic.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/graphs/temporal.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/integrations/__init__.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/integrations/act.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/integrations/autogen.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/integrations/crewai.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/integrations/langchain.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/integrations/livekit.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/integrations/openai_tools.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/integrations/pipecat.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/integrations/prompting.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/mcp.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/memory/__init__.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/memory/experiential.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/memory/working.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/pool.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/privacy/__init__.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/privacy/audit.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/privacy/injection.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/privacy/pii_detector.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/privacy/retention.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/py.typed +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/serve.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/store/__init__.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/store/base.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/store/factory.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/store/pg_sql.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/store/postgres_store.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/store/sqlite_store.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/store/vector_index.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/utils/__init__.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/utils/embeddings.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/utils/llm.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/contextdb/utils/migrations.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/docs/api.md +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/docs/connectors.md +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/docs/index.md +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/docs/memory_evolution.md +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/docs/migrations.md +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/docs/multi_tenant.md +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/docs/postgres.md +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/docs/prompts.md +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/docs/quickstart.md +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/docs/serve.md +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/docs/trust_policy.md +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/examples/customer_support.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/examples/multi_agent_team.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/examples/phone_agent.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/examples/research_assistant.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/legal/CLA-entity.md +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/legal/CLA-individual.md +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/legal/signed/README.md +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/mkdocs.yml +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/scripts/check_open_core_boundary.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/scripts/check_release.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/__init__.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/conftest.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/evals/__init__.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/integration/__init__.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/integration/test_client.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/integration/test_langchain_integration.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/pg_util.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/test_version.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/unit/__init__.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/unit/test_audit.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/unit/test_config.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/unit/test_connectors.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/unit/test_decision_scope.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/unit/test_embeddings.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/unit/test_formation.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/unit/test_graphs.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/unit/test_host_api.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/unit/test_init.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/unit/test_memory_bus.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/unit/test_memory_evolution.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/unit/test_models.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/unit/test_open_core_boundary.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/unit/test_pg_sql.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/unit/test_pii_detector.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/unit/test_postgres_concurrency.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/unit/test_postgres_connector.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/unit/test_postgres_memory_evolution.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/unit/test_retention.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/unit/test_retrieval.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/unit/test_scoped_id_access.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/unit/test_serve.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/unit/test_store.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/unit/test_trust_policy.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/unit/test_vector_index.py +0 -0
- {pycontextdb-0.4.4 → pycontextdb-0.4.6}/tests/unit/test_vector_scope.py +0 -0
|
@@ -2,6 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.4.6 — 2026-10-08
|
|
6
|
+
|
|
7
|
+
- A lower-trust write no longer replaces a first-party fact in its slot.
|
|
8
|
+
Writes made under one user id share a speaker, so a third-party document or
|
|
9
|
+
an agent inference stored for that user superseded what the user had said,
|
|
10
|
+
and the user would then be asked to confirm the document's value. A
|
|
11
|
+
user-stated or confirmed fact now stays current and the slot is contested
|
|
12
|
+
until `confirm()`. Corrections of equal or higher trust still supersede.
|
|
13
|
+
Advisory: GHSA-vpf9-5xgr-3x8m.
|
|
14
|
+
- `factual.recall_for_action` and `factual.pending_confirmations` accept the
|
|
15
|
+
`entity` and `attribute` an action needs. With them, only current facts in
|
|
16
|
+
that slot count, read exactly rather than by similarity, and the read is
|
|
17
|
+
audited like a recall. Without them nothing changes: any trusted memory the
|
|
18
|
+
query retrieves counts, whether or not it answers the request.
|
|
19
|
+
- The slot vocabulary (version 2) adds `user/billing_email`, `user/work_phone`,
|
|
20
|
+
and `user/work_address`. A user's billing email, work phone, or work address
|
|
21
|
+
no longer replaces their personal one. An email filed as a billing email
|
|
22
|
+
stays there instead of being moved to `user/email` and stored as
|
|
23
|
+
`agent_inferred`.
|
|
24
|
+
|
|
25
|
+
## 0.4.5 — 2026-10-05
|
|
26
|
+
|
|
27
|
+
- `forget_user` no longer deletes other users' memories. It added every graph
|
|
28
|
+
neighbor of the erased memories to the deletion set, and entity edges join
|
|
29
|
+
any memories that mention the same name, so on a shared client with the
|
|
30
|
+
entity graph and an extraction model, erasing one user could hard-delete
|
|
31
|
+
another user's memories. It now deletes only the user's memories and the
|
|
32
|
+
summaries derived from them, and severs their graph edges.
|
|
33
|
+
- Consolidated summaries keep the weakest corroboration count of their inputs
|
|
34
|
+
instead of the sum. Repeated copies of one third-party claim could
|
|
35
|
+
consolidate into a summary that passed the default trust bar and was
|
|
36
|
+
returned by `recall_for_action`. The summary now stays pending confirmation.
|
|
37
|
+
- Unchanged limitation: entity rows are shared across users, and attributes
|
|
38
|
+
merged into an entity from a user's text are not removed by `forget_user`.
|
|
39
|
+
|
|
5
40
|
## 0.4.4 — 2026-09-20
|
|
6
41
|
|
|
7
42
|
- PostgreSQL sibling composition now uses an ordered user-plus-entity cache
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pycontextdb
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.6
|
|
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
|
|
@@ -118,9 +118,9 @@ Hermetic, reproducible — run the suite yourself: `python benchmarks/run_benchm
|
|
|
118
118
|
|
|
119
119
|
Speed is table stakes. The production failure that gets agents fired is **acting on a wish as if it were a fact**: a caller says *"I'd like to come in Thursday"* and the agent answers *"we penciled you in."* A wish and a confirmed fact are the same object in every memory store on the market — so ContextDB types them.
|
|
120
120
|
|
|
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.
|
|
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`. Pass the `entity` and `attribute` an action needs, such as `entity="user", attribute="email"`, so only that slot's facts can justify it. `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
|
|
|
@@ -52,9 +52,9 @@ Hermetic, reproducible — run the suite yourself: `python benchmarks/run_benchm
|
|
|
52
52
|
|
|
53
53
|
Speed is table stakes. The production failure that gets agents fired is **acting on a wish as if it were a fact**: a caller says *"I'd like to come in Thursday"* and the agent answers *"we penciled you in."* A wish and a confirmed fact are the same object in every memory store on the market — so ContextDB types them.
|
|
54
54
|
|
|
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.
|
|
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`. Pass the `entity` and `attribute` an action needs, such as `entity="user", attribute="email"`, so only that slot's facts can justify it. `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
|
|
|
@@ -48,7 +48,12 @@ from contextdb.core.models import (
|
|
|
48
48
|
MemoryType,
|
|
49
49
|
)
|
|
50
50
|
from contextdb.core.policy import TrustPolicy
|
|
51
|
-
from contextdb.core.slots import
|
|
51
|
+
from contextdb.core.slots import (
|
|
52
|
+
canonicalize_slot,
|
|
53
|
+
infer_negation,
|
|
54
|
+
infer_slot,
|
|
55
|
+
slot_supported_by,
|
|
56
|
+
)
|
|
52
57
|
from contextdb.dynamics.trust import TrustEngine, infer_action_relevant, speaker_id
|
|
53
58
|
from contextdb.privacy.injection import screen_injection
|
|
54
59
|
from contextdb.privacy.pii_detector import PIIDetector
|
|
@@ -389,7 +394,7 @@ class ContextDB:
|
|
|
389
394
|
inferred = infer_slot(source_text)
|
|
390
395
|
chosen = explicit or inferred
|
|
391
396
|
if explicit is not None and inferred is not None:
|
|
392
|
-
if (explicit
|
|
397
|
+
if not slot_supported_by(explicit, inferred):
|
|
393
398
|
chosen = inferred
|
|
394
399
|
item.epistemic_source = "agent_inferred"
|
|
395
400
|
item.confidence = min(item.confidence, 0.4)
|
|
@@ -1685,21 +1690,13 @@ class ContextDB:
|
|
|
1685
1690
|
if added == 0:
|
|
1686
1691
|
break
|
|
1687
1692
|
|
|
1688
|
-
#
|
|
1689
|
-
#
|
|
1690
|
-
# the
|
|
1693
|
+
# Sever graph edges so a forgotten memory cannot be reached or
|
|
1694
|
+
# rebuilt through the graph. Neighbors are never added to the
|
|
1695
|
+
# deletion set: entity edges join every memory that mentions the
|
|
1696
|
+
# same name, including other users' memories, and this user's own
|
|
1697
|
+
# and derived memories are already in ``targets``.
|
|
1691
1698
|
for graph in self._graphs.values():
|
|
1692
1699
|
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
1700
|
try:
|
|
1704
1701
|
await graph.remove_node(memory_id)
|
|
1705
1702
|
except Exception: # noqa: BLE001
|
|
@@ -1999,8 +1996,18 @@ class ContextDB:
|
|
|
1999
1996
|
self,
|
|
2000
1997
|
user_id: str | None = None,
|
|
2001
1998
|
limit: int = 100,
|
|
1999
|
+
*,
|
|
2000
|
+
entity: str | None = None,
|
|
2001
|
+
attribute: str | None = None,
|
|
2002
2002
|
) -> list[MemoryItem]:
|
|
2003
|
-
"""Facts that are action-relevant and do not yet pass the trust policy.
|
|
2003
|
+
"""Facts that are action-relevant and do not yet pass the trust policy.
|
|
2004
|
+
|
|
2005
|
+
With ``entity`` and ``attribute``, only the current facts in that slot.
|
|
2006
|
+
"""
|
|
2007
|
+
if entity is not None or attribute is not None:
|
|
2008
|
+
rows = await self.slot_facts(entity, attribute, user_id=user_id)
|
|
2009
|
+
pending = [item for item in rows if self.trust_policy.requires_confirmation(item)]
|
|
2010
|
+
return pending[:limit]
|
|
2004
2011
|
uid = self._resolve_user(user_id)
|
|
2005
2012
|
await self._ensure_init()
|
|
2006
2013
|
store = self._require_store()
|
|
@@ -2009,6 +2016,45 @@ class ContextDB:
|
|
|
2009
2016
|
)
|
|
2010
2017
|
return [item for item in rows if self.trust_policy.requires_confirmation(item)]
|
|
2011
2018
|
|
|
2019
|
+
async def slot_facts(
|
|
2020
|
+
self,
|
|
2021
|
+
entity: str | None,
|
|
2022
|
+
attribute: str | None,
|
|
2023
|
+
*,
|
|
2024
|
+
user_id: str | None = None,
|
|
2025
|
+
as_of: datetime | None = None,
|
|
2026
|
+
) -> list[MemoryItem]:
|
|
2027
|
+
"""Current facts in one slot, read exactly rather than by similarity.
|
|
2028
|
+
|
|
2029
|
+
A similarity search returns the nearest memories whether or not they
|
|
2030
|
+
answer the request. When the caller knows which slot an action needs,
|
|
2031
|
+
this is the evidence that action may rely on.
|
|
2032
|
+
"""
|
|
2033
|
+
slot = canonicalize_slot(entity, attribute)
|
|
2034
|
+
if slot is None:
|
|
2035
|
+
raise ValueError("entity and attribute are both required to name a slot")
|
|
2036
|
+
uid = self._resolve_user(user_id)
|
|
2037
|
+
await self._ensure_init()
|
|
2038
|
+
store = self._require_store()
|
|
2039
|
+
moment = as_of or self.clock()
|
|
2040
|
+
rows = await store.list_by_slot(slot.entity, slot.attribute, user_id=uid)
|
|
2041
|
+
items = sorted(
|
|
2042
|
+
(m for m in rows if m.memory_type == MemoryType.FACTUAL and m.is_valid_at(moment)),
|
|
2043
|
+
key=lambda m: m.write_generation,
|
|
2044
|
+
reverse=True,
|
|
2045
|
+
)
|
|
2046
|
+
audit_details = {
|
|
2047
|
+
"slot": [slot.entity, slot.attribute],
|
|
2048
|
+
"hits": len(items),
|
|
2049
|
+
"returned_ids": [m.id for m in items],
|
|
2050
|
+
"as_of": moment.isoformat(),
|
|
2051
|
+
"abstained": not items,
|
|
2052
|
+
}
|
|
2053
|
+
if self._audit is not None and self.config.enable_read_audit:
|
|
2054
|
+
await self._audit.log(operation="SLOT_READ", user_id=uid, details=audit_details)
|
|
2055
|
+
await self._emit("read_audit", user_id=uid, audit_details=audit_details)
|
|
2056
|
+
return items
|
|
2057
|
+
|
|
2012
2058
|
# ------------------------------------------------------------------ #
|
|
2013
2059
|
# Typed memory surfaces
|
|
2014
2060
|
# ------------------------------------------------------------------ #
|
|
@@ -14,7 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
import re
|
|
15
15
|
from dataclasses import dataclass
|
|
16
16
|
|
|
17
|
-
SLOT_VOCAB_VERSION = "
|
|
17
|
+
SLOT_VOCAB_VERSION = "2"
|
|
18
18
|
|
|
19
19
|
# Canonical (entity, attribute) → class. The class drives the per-class
|
|
20
20
|
# corroboration bar in :class:`TrustPolicy`.
|
|
@@ -31,8 +31,11 @@ SLOT_VOCAB: dict[tuple[str, str], str] = {
|
|
|
31
31
|
("reservation", "time"): "booking",
|
|
32
32
|
("reservation", "day"): "booking",
|
|
33
33
|
("user", "email"): "contact",
|
|
34
|
+
("user", "billing_email"): "contact",
|
|
34
35
|
("user", "phone"): "contact",
|
|
36
|
+
("user", "work_phone"): "contact",
|
|
35
37
|
("user", "address"): "contact",
|
|
38
|
+
("user", "work_address"): "contact",
|
|
36
39
|
("user", "name"): "identity",
|
|
37
40
|
("account", "number"): "identity",
|
|
38
41
|
("account", "pin"): "identity",
|
|
@@ -77,6 +80,15 @@ _ATTRIBUTE_ALIASES: dict[str, str] = {
|
|
|
77
80
|
"mobile": "phone",
|
|
78
81
|
"allergies": "allergy",
|
|
79
82
|
"allergic": "allergy",
|
|
83
|
+
"invoice_email": "billing_email",
|
|
84
|
+
"office_phone": "work_phone",
|
|
85
|
+
"office_address": "work_address",
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# Slots the text slotter cannot tell from a broader one: any email address
|
|
89
|
+
# reads as user/email, whether it is personal or where invoices go.
|
|
90
|
+
_REFINES: dict[tuple[str, str], tuple[str, str]] = {
|
|
91
|
+
("user", "billing_email"): ("user", "email"),
|
|
80
92
|
}
|
|
81
93
|
|
|
82
94
|
|
|
@@ -111,6 +123,13 @@ def canonicalize_slot(entity: str | None, attribute: str | None) -> Slot | None:
|
|
|
111
123
|
return Slot(entity=ent, attribute=attr, slot_class=slot_class)
|
|
112
124
|
|
|
113
125
|
|
|
126
|
+
def slot_supported_by(explicit: Slot, inferred: Slot) -> bool:
|
|
127
|
+
"""Whether the slot the text implies is consistent with the one chosen."""
|
|
128
|
+
chosen = (explicit.entity, explicit.attribute)
|
|
129
|
+
implied = (inferred.entity, inferred.attribute)
|
|
130
|
+
return chosen == implied or _REFINES.get(chosen) == implied
|
|
131
|
+
|
|
132
|
+
|
|
114
133
|
_WEEKDAYS = (
|
|
115
134
|
"monday",
|
|
116
135
|
"tuesday",
|
|
@@ -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
|
)
|
|
@@ -137,6 +137,21 @@ def speaker_id(
|
|
|
137
137
|
return "anonymous"
|
|
138
138
|
|
|
139
139
|
|
|
140
|
+
def _first_party(item: MemoryItem) -> bool:
|
|
141
|
+
return item.confirmed or item.epistemic_source == "user_stated"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def outranks(current: MemoryItem, incoming: MemoryItem) -> bool:
|
|
145
|
+
"""Whether ``incoming`` may not replace ``current`` in a slot.
|
|
146
|
+
|
|
147
|
+
Speaker identity alone cannot tell a user's correction from a document
|
|
148
|
+
the agent stored on the user's behalf: both arrive under the same user.
|
|
149
|
+
A first-party or confirmed fact is therefore never superseded by a
|
|
150
|
+
lower-trust write; the slot is contested instead.
|
|
151
|
+
"""
|
|
152
|
+
return _first_party(current) and not _first_party(incoming)
|
|
153
|
+
|
|
154
|
+
|
|
140
155
|
class TrustEngine:
|
|
141
156
|
"""Slot-aware write path used by ``factual.add`` and consolidation."""
|
|
142
157
|
|
|
@@ -252,20 +267,27 @@ class TrustEngine:
|
|
|
252
267
|
)
|
|
253
268
|
return updated, "corroborated"
|
|
254
269
|
|
|
270
|
+
protected = [c for c in current if outranks(c, item)]
|
|
255
271
|
same_speaker = [
|
|
256
|
-
c
|
|
272
|
+
c
|
|
273
|
+
for c in current
|
|
274
|
+
if c not in protected and incoming_speaker in (c.corroborated_by or [])
|
|
257
275
|
]
|
|
258
276
|
other_speaker = [
|
|
259
|
-
c
|
|
277
|
+
c
|
|
278
|
+
for c in current
|
|
279
|
+
if c not in protected and incoming_speaker not in (c.corroborated_by or [])
|
|
260
280
|
]
|
|
261
281
|
|
|
262
282
|
item.write_generation = max_gen + 1
|
|
263
283
|
# Independent speakers asserting different values: contest, do not
|
|
264
|
-
# last-write-win. Same-speaker corrections still supersede
|
|
265
|
-
|
|
284
|
+
# last-write-win. Same-speaker corrections still supersede, except a
|
|
285
|
+
# lower-trust write never replaces a first-party fact.
|
|
286
|
+
contest = protected + ([] if same_speaker else other_speaker)
|
|
287
|
+
if contest and not item.pending_consolidation:
|
|
266
288
|
item.contested = True
|
|
267
289
|
stored = await self.store.add(item)
|
|
268
|
-
for candidate in
|
|
290
|
+
for candidate in contest:
|
|
269
291
|
await self.store.update(candidate.id, contested=True)
|
|
270
292
|
await self._log(
|
|
271
293
|
"CONFLICT",
|
|
@@ -278,8 +300,13 @@ class TrustEngine:
|
|
|
278
300
|
"old_value": candidate.content,
|
|
279
301
|
"new_value": stored.content,
|
|
280
302
|
"speaker": incoming_speaker,
|
|
303
|
+
"reason": (
|
|
304
|
+
"outranked_source" if candidate in protected else "other_speaker"
|
|
305
|
+
),
|
|
281
306
|
},
|
|
282
307
|
)
|
|
308
|
+
for candidate in [c for c in current if c not in contest]:
|
|
309
|
+
await self._supersede(candidate, stored, user_id, item, now)
|
|
283
310
|
await self._log(
|
|
284
311
|
"CREATE",
|
|
285
312
|
stored.id,
|
|
@@ -297,25 +324,9 @@ class TrustEngine:
|
|
|
297
324
|
for candidate in current:
|
|
298
325
|
if item.pending_consolidation and not candidate.pending_consolidation:
|
|
299
326
|
continue
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
superseded_by=stored.id,
|
|
304
|
-
contested=False,
|
|
305
|
-
)
|
|
306
|
-
await self._log(
|
|
307
|
-
"SUPERSEDE",
|
|
308
|
-
candidate.id,
|
|
309
|
-
user_id,
|
|
310
|
-
{
|
|
311
|
-
"superseded_by": stored.id,
|
|
312
|
-
"entity": item.entity_key,
|
|
313
|
-
"attribute": item.attribute_key,
|
|
314
|
-
"old_value": candidate.content,
|
|
315
|
-
"new_value": stored.content,
|
|
316
|
-
"generation": item.write_generation,
|
|
317
|
-
},
|
|
318
|
-
)
|
|
327
|
+
if candidate in protected:
|
|
328
|
+
continue
|
|
329
|
+
await self._supersede(candidate, stored, user_id, item, now)
|
|
319
330
|
outcome = "superseded"
|
|
320
331
|
await self._log(
|
|
321
332
|
"CREATE",
|
|
@@ -325,6 +336,34 @@ class TrustEngine:
|
|
|
325
336
|
)
|
|
326
337
|
return stored, outcome
|
|
327
338
|
|
|
339
|
+
async def _supersede(
|
|
340
|
+
self,
|
|
341
|
+
candidate: MemoryItem,
|
|
342
|
+
stored: MemoryItem,
|
|
343
|
+
user_id: str | None,
|
|
344
|
+
item: MemoryItem,
|
|
345
|
+
now: datetime,
|
|
346
|
+
) -> None:
|
|
347
|
+
await self.store.update(
|
|
348
|
+
candidate.id,
|
|
349
|
+
valid_until=now,
|
|
350
|
+
superseded_by=stored.id,
|
|
351
|
+
contested=False,
|
|
352
|
+
)
|
|
353
|
+
await self._log(
|
|
354
|
+
"SUPERSEDE",
|
|
355
|
+
candidate.id,
|
|
356
|
+
user_id,
|
|
357
|
+
{
|
|
358
|
+
"superseded_by": stored.id,
|
|
359
|
+
"entity": item.entity_key,
|
|
360
|
+
"attribute": item.attribute_key,
|
|
361
|
+
"old_value": candidate.content,
|
|
362
|
+
"new_value": stored.content,
|
|
363
|
+
"generation": item.write_generation,
|
|
364
|
+
},
|
|
365
|
+
)
|
|
366
|
+
|
|
328
367
|
async def evolve_write(
|
|
329
368
|
self,
|
|
330
369
|
item: MemoryItem,
|
|
@@ -591,22 +630,24 @@ class TrustEngine:
|
|
|
591
630
|
previous_memory_ids=previous_ids,
|
|
592
631
|
)
|
|
593
632
|
|
|
633
|
+
protected = [candidate for candidate in current if outranks(candidate, item)]
|
|
594
634
|
same_speaker = [
|
|
595
635
|
candidate
|
|
596
636
|
for candidate in current
|
|
597
|
-
if incoming_speaker in candidate.corroborated_by
|
|
637
|
+
if candidate not in protected and incoming_speaker in candidate.corroborated_by
|
|
598
638
|
]
|
|
599
639
|
other_speaker = [
|
|
600
640
|
candidate
|
|
601
641
|
for candidate in current
|
|
602
|
-
if incoming_speaker not in candidate.corroborated_by
|
|
642
|
+
if candidate not in protected and incoming_speaker not in candidate.corroborated_by
|
|
603
643
|
]
|
|
604
644
|
item.write_generation = max_generation + 1
|
|
605
645
|
|
|
606
|
-
|
|
646
|
+
contest = protected + ([] if same_speaker else other_speaker)
|
|
647
|
+
if contest:
|
|
607
648
|
item.contested = True
|
|
608
649
|
stored = await self.store.add(item)
|
|
609
|
-
for candidate in
|
|
650
|
+
for candidate in contest:
|
|
610
651
|
await self.store.update(candidate.id, contested=True)
|
|
611
652
|
await self._log(
|
|
612
653
|
"CONFLICT",
|
|
@@ -619,8 +660,13 @@ class TrustEngine:
|
|
|
619
660
|
"old_value": candidate.content,
|
|
620
661
|
"new_value": stored.content,
|
|
621
662
|
"speaker": incoming_speaker,
|
|
663
|
+
"reason": (
|
|
664
|
+
"outranked_source" if candidate in protected else "other_speaker"
|
|
665
|
+
),
|
|
622
666
|
},
|
|
623
667
|
)
|
|
668
|
+
for candidate in [c for c in current if c not in contest]:
|
|
669
|
+
await self._supersede(candidate, stored, user_id, item, now)
|
|
624
670
|
await self._log(
|
|
625
671
|
"CREATE",
|
|
626
672
|
stored.id,
|
|
@@ -166,12 +166,24 @@ class FactualMemory:
|
|
|
166
166
|
as_of: datetime | None = None,
|
|
167
167
|
*,
|
|
168
168
|
user_id: str | None = None,
|
|
169
|
+
entity: str | None = None,
|
|
170
|
+
attribute: str | None = None,
|
|
169
171
|
) -> list[MemoryItem]:
|
|
170
|
-
"""Recall only facts an agent may act on without confirming first.
|
|
172
|
+
"""Recall only facts an agent may act on without confirming first.
|
|
173
|
+
|
|
174
|
+
Without a slot, any trusted memory the query retrieves counts, related
|
|
175
|
+
or not. Pass the ``entity`` and ``attribute`` the action needs to rely
|
|
176
|
+
only on trusted facts in that slot.
|
|
177
|
+
"""
|
|
171
178
|
policy = self.client.trust_policy
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
179
|
+
if entity is not None or attribute is not None:
|
|
180
|
+
candidates = await self.client.slot_facts(
|
|
181
|
+
entity, attribute, user_id=self._user(user_id), as_of=as_of
|
|
182
|
+
)
|
|
183
|
+
else:
|
|
184
|
+
candidates = await self.recall(
|
|
185
|
+
query, top_k=top_k * 4, as_of=as_of, user_id=self._user(user_id)
|
|
186
|
+
)
|
|
175
187
|
return [m for m in candidates if policy.is_trusted(m)][:top_k]
|
|
176
188
|
|
|
177
189
|
async def confirm(self, memory_id: str, user_id: str | None = None) -> MemoryItem:
|
|
@@ -182,10 +194,13 @@ class FactualMemory:
|
|
|
182
194
|
self,
|
|
183
195
|
user_id: str | None = None,
|
|
184
196
|
limit: int = 100,
|
|
197
|
+
*,
|
|
198
|
+
entity: str | None = None,
|
|
199
|
+
attribute: str | None = None,
|
|
185
200
|
) -> list[MemoryItem]:
|
|
186
201
|
"""Action-relevant facts that still need a yes from the user."""
|
|
187
202
|
return await self.client.pending_confirmations(
|
|
188
|
-
user_id=self._user(user_id), limit=limit
|
|
203
|
+
user_id=self._user(user_id), limit=limit, entity=entity, attribute=attribute
|
|
189
204
|
)
|
|
190
205
|
|
|
191
206
|
async def update_fact(
|
|
@@ -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.6"
|
|
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"
|