pycontextdb 0.4.6__tar.gz → 0.4.7__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.6 → pycontextdb-0.4.7}/CHANGELOG.md +36 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/PKG-INFO +43 -50
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/README.md +42 -49
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/__init__.py +1 -1
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/client.py +49 -4
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/core/config.py +13 -3
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/core/policy.py +6 -4
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/core/slots.py +49 -3
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/dynamics/retrieval.py +50 -1
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/dynamics/trust.py +11 -3
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/graphs/entity.py +118 -12
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/privacy/pii_detector.py +40 -5
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/store/base.py +19 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/store/postgres_store.py +21 -1
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/store/sqlite_store.py +23 -1
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/docs/index.md +8 -4
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/docs/prompts.md +1 -1
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/docs/quickstart.md +3 -1
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/pyproject.toml +1 -1
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/evals/test_trust_model.py +193 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_pii_detector.py +33 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_retrieval.py +12 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/.github/CODEOWNERS +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/.github/copilot-instructions.md +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/.github/pull_request_template.md +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/.github/workflows/ci.yml +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/.github/workflows/publish.yml +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/.gitignore +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/.pre-commit-config.yaml +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/COMMERCIAL.md +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/CONTRIBUTING.md +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/LICENSE +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/NOTICE +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/OPEN_CORE.md +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/ai-tools/.cursorrules +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/ai-tools/CLAUDE.md +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/ai-tools/README.md +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/ai-tools/llms-full.txt +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/ai-tools/llms.txt +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/assets/README.md +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/assets/architecture.png +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/assets/benchmarks.png +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/assets/hero.png +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/assets/performance.png +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/assets/problem.png +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/assets/quickstart.png +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/benchmarks/__init__.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/benchmarks/run_benchmarks.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/benchmarks/trust_bakeoff.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/agents/__init__.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/agents/memory_bus.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/agents/rl_manager.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/cli.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/connectors/__init__.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/connectors/base.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/connectors/postgres.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/core/__init__.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/core/clock.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/core/exceptions.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/core/models.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/dynamics/__init__.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/dynamics/evolution.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/dynamics/formation.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/dynamics/salience.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/graphs/__init__.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/graphs/base.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/graphs/causal.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/graphs/semantic.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/graphs/temporal.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/integrations/__init__.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/integrations/act.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/integrations/autogen.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/integrations/crewai.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/integrations/langchain.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/integrations/livekit.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/integrations/openai_tools.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/integrations/pipecat.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/integrations/prompting.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/mcp.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/memory/__init__.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/memory/experiential.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/memory/factual.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/memory/working.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/pool.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/privacy/__init__.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/privacy/audit.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/privacy/injection.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/privacy/retention.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/py.typed +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/serve.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/store/__init__.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/store/factory.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/store/pg_sql.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/store/vector_index.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/utils/__init__.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/utils/embeddings.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/utils/llm.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/utils/migrations.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/docs/api.md +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/docs/architecture.md +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/docs/connectors.md +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/docs/memory_evolution.md +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/docs/migrations.md +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/docs/multi_tenant.md +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/docs/postgres.md +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/docs/serve.md +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/docs/trust_policy.md +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/examples/customer_support.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/examples/multi_agent_team.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/examples/phone_agent.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/examples/research_assistant.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/legal/CLA-entity.md +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/legal/CLA-individual.md +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/legal/signed/README.md +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/mkdocs.yml +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/scripts/check_open_core_boundary.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/scripts/check_release.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/__init__.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/conftest.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/evals/__init__.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/integration/__init__.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/integration/test_client.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/integration/test_langchain_integration.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/pg_util.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/test_version.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/__init__.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_audit.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_config.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_connectors.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_decision_scope.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_embeddings.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_evolution.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_formation.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_graphs.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_host_api.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_init.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_memory_bus.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_memory_evolution.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_models.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_open_core_boundary.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_pg_sql.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_postgres_concurrency.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_postgres_connector.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_postgres_memory_evolution.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_retention.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_scoped_id_access.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_serve.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_store.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_trust_policy.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_vector_index.py +0 -0
- {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_vector_scope.py +0 -0
|
@@ -2,6 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.4.7 — 2026-10-12
|
|
6
|
+
|
|
7
|
+
- `forget_user` now removes what the user added to shared entities. Entity
|
|
8
|
+
attributes record the memory that supplied them, are rebuilt from the
|
|
9
|
+
remaining sources when a memory is removed, and an entity no memory still
|
|
10
|
+
mentions is deleted. `verify_forgotten` fails if an edge or attribute source
|
|
11
|
+
still points at a deleted memory. Attributes merged before 0.4.7 have no
|
|
12
|
+
recorded source and stay until their entity is unreferenced.
|
|
13
|
+
- In encrypt and redact modes, a slot value that contains a detected PII span
|
|
14
|
+
is stored as a fingerprint, keyed with the PII encryption key when one is
|
|
15
|
+
configured. Slot values were taken from the original
|
|
16
|
+
utterance, so encrypt mode still wrote plaintext emails and phone numbers to
|
|
17
|
+
disk. Comparisons are unchanged. A value written before 0.4.7 compares as
|
|
18
|
+
different from the same value written now, so it is superseded once.
|
|
19
|
+
- `consolidate_pending` no longer drops a new value that reads the same once
|
|
20
|
+
redacted. Two emails saved with `add_fast` both stored as `[EMAIL]`, so the
|
|
21
|
+
second consolidated into a no-op and was lost.
|
|
22
|
+
- Recall adds a lexical ranking for identifiers (tokens of three or more
|
|
23
|
+
characters that contain a digit, such as order numbers and SKUs). An exact
|
|
24
|
+
identifier match passes the relevance floor. Stores gain `search_text`; no
|
|
25
|
+
schema change.
|
|
26
|
+
- `pii_detect_names_and_addresses=True` also redacts street addresses and cued
|
|
27
|
+
names ("my name is", "call me", "this is" followed by two capitalized
|
|
28
|
+
words). Off by default, because a redacted address is hidden from the agent
|
|
29
|
+
too.
|
|
30
|
+
- Appointment kinds get their own slots: "my dentist appointment" and "my
|
|
31
|
+
haircut appointment" no longer replace or merge with each other. Kind
|
|
32
|
+
entities such as `dentist_appointment` use the booking class, and a generic
|
|
33
|
+
`appointment` slot is still accepted where the text names a kind.
|
|
34
|
+
- The relevance floor no longer recommends a fixed 0.15 to 0.25 band. Cosine
|
|
35
|
+
ranges are embedder-specific, and a floor cannot tell whether a memory
|
|
36
|
+
answers a request.
|
|
37
|
+
- README and docs replace v0.1.0 performance numbers and fixed test counts
|
|
38
|
+
with current measurements, and state that no competitor arms ship in the
|
|
39
|
+
bake-off.
|
|
40
|
+
|
|
5
41
|
## 0.4.6 — 2026-10-08
|
|
6
42
|
|
|
7
43
|
- A lower-trust write no longer replaces a first-party fact in its slot.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pycontextdb
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.7
|
|
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
|
|
@@ -74,10 +74,10 @@ Description-Content-Type: text/markdown
|
|
|
74
74
|
<p align="center">
|
|
75
75
|
<a href="https://pypi.org/project/pycontextdb/"><img src="https://img.shields.io/pypi/v/pycontextdb.svg" alt="PyPI version"></a>
|
|
76
76
|
<a href="https://github.com/atomsai/contextdb/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="License: Apache 2.0"></a>
|
|
77
|
-
<a href="https://github.com/atomsai/contextdb/
|
|
77
|
+
<a href="https://github.com/atomsai/contextdb/actions/workflows/ci.yml"><img src="https://github.com/atomsai/contextdb/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI"></a>
|
|
78
78
|
<a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.10%2B-blue.svg" alt="Python 3.10+"></a>
|
|
79
79
|
<a href="https://github.com/atomsai/contextdb/blob/main/pyproject.toml"><img src="https://img.shields.io/badge/mypy-strict-blue.svg" alt="Type Checked"></a>
|
|
80
|
-
<a href="https://github.com/atomsai/contextdb/blob/main/benchmarks/run_benchmarks.py"><img src="https://img.shields.io/badge/search_p95
|
|
80
|
+
<a href="https://github.com/atomsai/contextdb/blob/main/benchmarks/run_benchmarks.py"><img src="https://img.shields.io/badge/search_p95-18ms_%40_5K_(mock_embedder)-blue.svg" alt="Search p95 18 ms at 5K memories with the mock embedder"></a>
|
|
81
81
|
<a href="https://github.com/atomsai/contextdb/stargazers"><img src="https://img.shields.io/github/stars/atomsai/contextdb?style=social" alt="GitHub stars"></a>
|
|
82
82
|
</p>
|
|
83
83
|
|
|
@@ -91,20 +91,18 @@ Description-Content-Type: text/markdown
|
|
|
91
91
|
|
|
92
92
|
## At a glance
|
|
93
93
|
|
|
94
|
-
<p align="center">
|
|
95
|
-
<img src="https://raw.githubusercontent.com/atomsai/contextdb/main/assets/performance.png" alt="Performance at a glance — search p50 3.4ms, 1,930 writes/sec, 112K PII/sec, 5.0ms p95 at 5K memories, 800K vectors/sec index build, 82/82 tests" width="100%">
|
|
96
|
-
</p>
|
|
97
|
-
|
|
98
94
|
| | |
|
|
99
95
|
|---|---|
|
|
100
|
-
| **Write throughput** | 1,
|
|
101
|
-
| **Search latency (1K memories)** | p50 **
|
|
102
|
-
| **Search latency (5K memories)** | p50 **
|
|
103
|
-
| **Vector search (10K × 1,536d)** | p50 **0.
|
|
96
|
+
| **Write throughput** | about 1,600 to 1,800 memories/sec |
|
|
97
|
+
| **Search latency (1K memories)** | p50 **11 ms** · p95 **12 to 14 ms** |
|
|
98
|
+
| **Search latency (5K memories)** | p50 **17 ms** · p95 **18 ms** |
|
|
99
|
+
| **Vector search (10K × 1,536d)** | p50 **0.8 ms** · p95 **1.0 ms** |
|
|
104
100
|
| **PII detection** | 100,000+ texts/sec |
|
|
105
|
-
| **Tests** |
|
|
101
|
+
| **Tests** | full suite and trust-model acceptance evals in CI on every pull request · ruff · mypy `--strict` |
|
|
106
102
|
| **Dependencies** | SQLite + NumPy (FAISS / Postgres optional) |
|
|
107
103
|
|
|
104
|
+
Measured in October 2026 with `python benchmarks/run_benchmarks.py` on an Apple M4 Max, Python 3.12, SQLite, and the mock embedder, so no model call is included. These numbers are the SDK's own overhead; a real embedding model adds its own latency.
|
|
105
|
+
|
|
108
106
|
Those search numbers are **SQLite + the in-repo mock embedder**. A recall
|
|
109
107
|
that embeds the query with OpenAI is typically **100–300ms** before the
|
|
110
108
|
store is touched. Use `embedding_cache_size`, `pip install 'pycontextdb[local]'`,
|
|
@@ -120,16 +118,16 @@ Speed is table stakes. The production failure that gets agents fired is **acting
|
|
|
120
118
|
|
|
121
119
|
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
120
|
|
|
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
|
|
121
|
+
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]`), **exact-identifier recall** (order numbers and SKUs are matched as whole tokens even when vector similarity misses them), **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, write p95 about 1 ms locally in the bake-off), **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 removes what the user added to shared entities, and **tenant/agent isolation enforced at the storage layer**.
|
|
124
122
|
|
|
125
|
-
|
|
123
|
+
The fabrication benchmark ships in the repository, and a test checks it on every pull request. Both arms use the same store, the same mock embedder, and the same scripted utterance policy; the only difference is the trust model:
|
|
126
124
|
|
|
127
125
|
| Arm | Fabrication rate (lower=better) | Recall accuracy | Temporal supersede | Over-refusal | Write p95 |
|
|
128
126
|
| --- | --- | --- | --- | --- | --- |
|
|
129
|
-
| `contextdb-trust` | **0%** | **100%** | yes | no |
|
|
130
|
-
| `raw-store-baseline` (untyped control) | 50% | 88% | NO | no | 0.
|
|
127
|
+
| `contextdb-trust` | **0%** | **100%** | yes | no | 0.99 ms |
|
|
128
|
+
| `raw-store-baseline` (untyped control) | 50% | 88% | NO | no | 0.45 ms |
|
|
131
129
|
|
|
132
|
-
Traps: wish-vs-fact, temporal move, prompt injection, PII redaction, cross-tenant bleed, negation, quantity update, over-refusal. Run it
|
|
130
|
+
Traps: wish-vs-fact, temporal move, prompt injection, PII redaction, cross-tenant bleed, negation, quantity update, over-refusal. Run it with `python benchmarks/trust_bakeoff.py`. These eight synthetic traps are regression evidence inside the harness, not a measure of production behavior, and no other memory product has been run through it. Another system can be compared by implementing the `MemoryArm` protocol. The acceptance evals live in `tests/evals/`.
|
|
133
131
|
|
|
134
132
|
---
|
|
135
133
|
|
|
@@ -507,49 +505,44 @@ Five layers, one dependency. Every component is modular, testable, and replaceab
|
|
|
507
505
|
|
|
508
506
|
## Benchmarks
|
|
509
507
|
|
|
510
|
-
|
|
511
|
-
<img src="https://raw.githubusercontent.com/atomsai/contextdb/main/assets/benchmarks.png" alt="ContextDB v0.1.0 performance dashboard — search p50 3.4ms, 1,930 writes/sec, 112K PII/sec, 82/82 tests" width="100%">
|
|
512
|
-
</p>
|
|
513
|
-
|
|
514
|
-
Six workloads, all hermetic, all reproducible. No API keys, no network, no cached results — just `python benchmarks/run_benchmarks.py`.
|
|
508
|
+
Six workloads, all hermetic and reproducible: no API keys, no network, no cached results. Run `python benchmarks/run_benchmarks.py`. The numbers below are from October 2026 on an Apple M4 Max with Python 3.12, SQLite, the mock embedder, and no FAISS. They measure the SDK's own overhead; a real embedding model adds its own latency.
|
|
515
509
|
|
|
516
510
|
### 1. Write throughput
|
|
517
511
|
|
|
518
|
-
1,000 sequential `add()` calls including Pydantic validation, embedding, SQLite INSERT, and vector-index update.
|
|
512
|
+
1,000 sequential `add()` calls including Pydantic validation, embedding, trust checks, SQLite INSERT, and vector-index update.
|
|
519
513
|
|
|
520
514
|
```
|
|
521
|
-
Throughput: 1,
|
|
522
|
-
Average per write: 0.
|
|
515
|
+
Throughput: about 1,600 to 1,800 writes/sec
|
|
516
|
+
Average per write: 0.6ms
|
|
523
517
|
```
|
|
524
518
|
|
|
525
|
-
### 2. Search latency
|
|
519
|
+
### 2. Search latency: 100 queries against 1,000 memories
|
|
526
520
|
|
|
527
521
|
```
|
|
528
|
-
p50:
|
|
529
|
-
p95:
|
|
530
|
-
p99:
|
|
531
|
-
Mean:
|
|
522
|
+
p50: 11.0ms
|
|
523
|
+
p95: 14.3ms
|
|
524
|
+
p99: 21.1ms
|
|
525
|
+
Mean: 11.7ms
|
|
532
526
|
```
|
|
533
527
|
|
|
534
|
-
|
|
528
|
+
Search includes PII redaction of the query, vector and graph fusion, salience, the trust filter, and the read audit.
|
|
535
529
|
|
|
536
530
|
### 3. Search latency vs scale
|
|
537
531
|
|
|
538
|
-
Latency grows sub-linearly; write cost stays flat until the vector index rebuilds.
|
|
539
|
-
|
|
540
532
|
| Memories | Add (ms/op) | Search p50 | Search p95 |
|
|
541
533
|
|---:|---:|---:|---:|
|
|
542
|
-
| 100 | 0.
|
|
543
|
-
| 500 | 0.
|
|
544
|
-
| 1,000 | 0.
|
|
545
|
-
| 5,000 | 0.
|
|
534
|
+
| 100 | 0.52ms | 9.2ms | 9.8ms |
|
|
535
|
+
| 500 | 0.55ms | 10.0ms | 11.6ms |
|
|
536
|
+
| 1,000 | 0.55ms | 11.1ms | 14.3ms |
|
|
537
|
+
| 5,000 | 0.55ms | 17.2ms | 17.8ms |
|
|
546
538
|
|
|
547
|
-
### 4. PII detection & redaction
|
|
539
|
+
### 4. PII detection & redaction: 1,000 texts
|
|
548
540
|
|
|
549
541
|
```
|
|
550
|
-
Throughput:
|
|
542
|
+
Throughput: 128,194 texts/sec
|
|
551
543
|
Per text: 0.01ms
|
|
552
544
|
Types covered: EMAIL, PHONE, SSN, CREDIT_CARD
|
|
545
|
+
(ADDRESS and cued NAME with pii_detect_names_and_addresses=True)
|
|
553
546
|
```
|
|
554
547
|
|
|
555
548
|
Correctness spot-check:
|
|
@@ -559,29 +552,29 @@ Input: Contact me at test@example.com or 555-123-4567. SSN: 123-45-6789
|
|
|
559
552
|
Redacted: Contact me at [EMAIL] or [PHONE]. SSN: [SSN]
|
|
560
553
|
```
|
|
561
554
|
|
|
562
|
-
PII runs before content reaches storage. It never gets embedded. It never gets logged.
|
|
555
|
+
PII runs before content reaches storage. It never gets embedded. It never gets logged. In encrypt mode, slot values that contain PII are stored as keyed fingerprints, so no plaintext email or phone number is written to disk.
|
|
563
556
|
|
|
564
|
-
### 5. Vector index
|
|
557
|
+
### 5. Vector index: 10K × 1,536-dim vectors
|
|
565
558
|
|
|
566
559
|
Pure-NumPy brute force, no FAISS dependency.
|
|
567
560
|
|
|
568
561
|
```
|
|
569
|
-
Index build: 0.
|
|
570
|
-
Search p50: 0.
|
|
571
|
-
Search p95: 1.
|
|
562
|
+
Index build: 0.011 to 0.017s
|
|
563
|
+
Search p50: 0.8ms
|
|
564
|
+
Search p95: 1.0ms
|
|
572
565
|
Self-retrieval accuracy: PASS
|
|
573
566
|
```
|
|
574
567
|
|
|
575
568
|
FAISS is available as an optional backend for datasets beyond ~100K vectors.
|
|
576
569
|
|
|
577
|
-
### 6. End-to-end
|
|
570
|
+
### 6. End-to-end: customer support agent
|
|
578
571
|
|
|
579
|
-
50 mixed factual
|
|
572
|
+
50 mixed factual and experiential memories, 20 semantic searches, one PII-laden write with redaction verification.
|
|
580
573
|
|
|
581
574
|
```
|
|
582
|
-
Add 50 memories: 0.03s (1,
|
|
583
|
-
Search p50:
|
|
584
|
-
PII add + redact:
|
|
575
|
+
Add 50 memories: 0.03s (about 1,540/sec)
|
|
576
|
+
Search p50: 4.9ms
|
|
577
|
+
PII add + redact: 1.4ms
|
|
585
578
|
PII correctly redacted: PASS
|
|
586
579
|
```
|
|
587
580
|
|
|
@@ -592,7 +585,7 @@ Input: Customer John Smith, email john@acme.com, SSN 123-45-6789 called about
|
|
|
592
585
|
Stored: Customer John Smith, email [EMAIL], SSN [SSN] called about billing
|
|
593
586
|
```
|
|
594
587
|
|
|
595
|
-
|
|
588
|
+
Rerun `python benchmarks/run_benchmarks.py` on your own hardware; the script prints everything shown here.
|
|
596
589
|
|
|
597
590
|
---
|
|
598
591
|
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
<p align="center">
|
|
9
9
|
<a href="https://pypi.org/project/pycontextdb/"><img src="https://img.shields.io/pypi/v/pycontextdb.svg" alt="PyPI version"></a>
|
|
10
10
|
<a href="https://github.com/atomsai/contextdb/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="License: Apache 2.0"></a>
|
|
11
|
-
<a href="https://github.com/atomsai/contextdb/
|
|
11
|
+
<a href="https://github.com/atomsai/contextdb/actions/workflows/ci.yml"><img src="https://github.com/atomsai/contextdb/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI"></a>
|
|
12
12
|
<a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.10%2B-blue.svg" alt="Python 3.10+"></a>
|
|
13
13
|
<a href="https://github.com/atomsai/contextdb/blob/main/pyproject.toml"><img src="https://img.shields.io/badge/mypy-strict-blue.svg" alt="Type Checked"></a>
|
|
14
|
-
<a href="https://github.com/atomsai/contextdb/blob/main/benchmarks/run_benchmarks.py"><img src="https://img.shields.io/badge/search_p95
|
|
14
|
+
<a href="https://github.com/atomsai/contextdb/blob/main/benchmarks/run_benchmarks.py"><img src="https://img.shields.io/badge/search_p95-18ms_%40_5K_(mock_embedder)-blue.svg" alt="Search p95 18 ms at 5K memories with the mock embedder"></a>
|
|
15
15
|
<a href="https://github.com/atomsai/contextdb/stargazers"><img src="https://img.shields.io/github/stars/atomsai/contextdb?style=social" alt="GitHub stars"></a>
|
|
16
16
|
</p>
|
|
17
17
|
|
|
@@ -25,20 +25,18 @@
|
|
|
25
25
|
|
|
26
26
|
## At a glance
|
|
27
27
|
|
|
28
|
-
<p align="center">
|
|
29
|
-
<img src="https://raw.githubusercontent.com/atomsai/contextdb/main/assets/performance.png" alt="Performance at a glance — search p50 3.4ms, 1,930 writes/sec, 112K PII/sec, 5.0ms p95 at 5K memories, 800K vectors/sec index build, 82/82 tests" width="100%">
|
|
30
|
-
</p>
|
|
31
|
-
|
|
32
28
|
| | |
|
|
33
29
|
|---|---|
|
|
34
|
-
| **Write throughput** | 1,
|
|
35
|
-
| **Search latency (1K memories)** | p50 **
|
|
36
|
-
| **Search latency (5K memories)** | p50 **
|
|
37
|
-
| **Vector search (10K × 1,536d)** | p50 **0.
|
|
30
|
+
| **Write throughput** | about 1,600 to 1,800 memories/sec |
|
|
31
|
+
| **Search latency (1K memories)** | p50 **11 ms** · p95 **12 to 14 ms** |
|
|
32
|
+
| **Search latency (5K memories)** | p50 **17 ms** · p95 **18 ms** |
|
|
33
|
+
| **Vector search (10K × 1,536d)** | p50 **0.8 ms** · p95 **1.0 ms** |
|
|
38
34
|
| **PII detection** | 100,000+ texts/sec |
|
|
39
|
-
| **Tests** |
|
|
35
|
+
| **Tests** | full suite and trust-model acceptance evals in CI on every pull request · ruff · mypy `--strict` |
|
|
40
36
|
| **Dependencies** | SQLite + NumPy (FAISS / Postgres optional) |
|
|
41
37
|
|
|
38
|
+
Measured in October 2026 with `python benchmarks/run_benchmarks.py` on an Apple M4 Max, Python 3.12, SQLite, and the mock embedder, so no model call is included. These numbers are the SDK's own overhead; a real embedding model adds its own latency.
|
|
39
|
+
|
|
42
40
|
Those search numbers are **SQLite + the in-repo mock embedder**. A recall
|
|
43
41
|
that embeds the query with OpenAI is typically **100–300ms** before the
|
|
44
42
|
store is touched. Use `embedding_cache_size`, `pip install 'pycontextdb[local]'`,
|
|
@@ -54,16 +52,16 @@ Speed is table stakes. The production failure that gets agents fired is **acting
|
|
|
54
52
|
|
|
55
53
|
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
54
|
|
|
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
|
|
55
|
+
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]`), **exact-identifier recall** (order numbers and SKUs are matched as whole tokens even when vector similarity misses them), **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, write p95 about 1 ms locally in the bake-off), **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 removes what the user added to shared entities, and **tenant/agent isolation enforced at the storage layer**.
|
|
58
56
|
|
|
59
|
-
|
|
57
|
+
The fabrication benchmark ships in the repository, and a test checks it on every pull request. Both arms use the same store, the same mock embedder, and the same scripted utterance policy; the only difference is the trust model:
|
|
60
58
|
|
|
61
59
|
| Arm | Fabrication rate (lower=better) | Recall accuracy | Temporal supersede | Over-refusal | Write p95 |
|
|
62
60
|
| --- | --- | --- | --- | --- | --- |
|
|
63
|
-
| `contextdb-trust` | **0%** | **100%** | yes | no |
|
|
64
|
-
| `raw-store-baseline` (untyped control) | 50% | 88% | NO | no | 0.
|
|
61
|
+
| `contextdb-trust` | **0%** | **100%** | yes | no | 0.99 ms |
|
|
62
|
+
| `raw-store-baseline` (untyped control) | 50% | 88% | NO | no | 0.45 ms |
|
|
65
63
|
|
|
66
|
-
Traps: wish-vs-fact, temporal move, prompt injection, PII redaction, cross-tenant bleed, negation, quantity update, over-refusal. Run it
|
|
64
|
+
Traps: wish-vs-fact, temporal move, prompt injection, PII redaction, cross-tenant bleed, negation, quantity update, over-refusal. Run it with `python benchmarks/trust_bakeoff.py`. These eight synthetic traps are regression evidence inside the harness, not a measure of production behavior, and no other memory product has been run through it. Another system can be compared by implementing the `MemoryArm` protocol. The acceptance evals live in `tests/evals/`.
|
|
67
65
|
|
|
68
66
|
---
|
|
69
67
|
|
|
@@ -441,49 +439,44 @@ Five layers, one dependency. Every component is modular, testable, and replaceab
|
|
|
441
439
|
|
|
442
440
|
## Benchmarks
|
|
443
441
|
|
|
444
|
-
|
|
445
|
-
<img src="https://raw.githubusercontent.com/atomsai/contextdb/main/assets/benchmarks.png" alt="ContextDB v0.1.0 performance dashboard — search p50 3.4ms, 1,930 writes/sec, 112K PII/sec, 82/82 tests" width="100%">
|
|
446
|
-
</p>
|
|
447
|
-
|
|
448
|
-
Six workloads, all hermetic, all reproducible. No API keys, no network, no cached results — just `python benchmarks/run_benchmarks.py`.
|
|
442
|
+
Six workloads, all hermetic and reproducible: no API keys, no network, no cached results. Run `python benchmarks/run_benchmarks.py`. The numbers below are from October 2026 on an Apple M4 Max with Python 3.12, SQLite, the mock embedder, and no FAISS. They measure the SDK's own overhead; a real embedding model adds its own latency.
|
|
449
443
|
|
|
450
444
|
### 1. Write throughput
|
|
451
445
|
|
|
452
|
-
1,000 sequential `add()` calls including Pydantic validation, embedding, SQLite INSERT, and vector-index update.
|
|
446
|
+
1,000 sequential `add()` calls including Pydantic validation, embedding, trust checks, SQLite INSERT, and vector-index update.
|
|
453
447
|
|
|
454
448
|
```
|
|
455
|
-
Throughput: 1,
|
|
456
|
-
Average per write: 0.
|
|
449
|
+
Throughput: about 1,600 to 1,800 writes/sec
|
|
450
|
+
Average per write: 0.6ms
|
|
457
451
|
```
|
|
458
452
|
|
|
459
|
-
### 2. Search latency
|
|
453
|
+
### 2. Search latency: 100 queries against 1,000 memories
|
|
460
454
|
|
|
461
455
|
```
|
|
462
|
-
p50:
|
|
463
|
-
p95:
|
|
464
|
-
p99:
|
|
465
|
-
Mean:
|
|
456
|
+
p50: 11.0ms
|
|
457
|
+
p95: 14.3ms
|
|
458
|
+
p99: 21.1ms
|
|
459
|
+
Mean: 11.7ms
|
|
466
460
|
```
|
|
467
461
|
|
|
468
|
-
|
|
462
|
+
Search includes PII redaction of the query, vector and graph fusion, salience, the trust filter, and the read audit.
|
|
469
463
|
|
|
470
464
|
### 3. Search latency vs scale
|
|
471
465
|
|
|
472
|
-
Latency grows sub-linearly; write cost stays flat until the vector index rebuilds.
|
|
473
|
-
|
|
474
466
|
| Memories | Add (ms/op) | Search p50 | Search p95 |
|
|
475
467
|
|---:|---:|---:|---:|
|
|
476
|
-
| 100 | 0.
|
|
477
|
-
| 500 | 0.
|
|
478
|
-
| 1,000 | 0.
|
|
479
|
-
| 5,000 | 0.
|
|
468
|
+
| 100 | 0.52ms | 9.2ms | 9.8ms |
|
|
469
|
+
| 500 | 0.55ms | 10.0ms | 11.6ms |
|
|
470
|
+
| 1,000 | 0.55ms | 11.1ms | 14.3ms |
|
|
471
|
+
| 5,000 | 0.55ms | 17.2ms | 17.8ms |
|
|
480
472
|
|
|
481
|
-
### 4. PII detection & redaction
|
|
473
|
+
### 4. PII detection & redaction: 1,000 texts
|
|
482
474
|
|
|
483
475
|
```
|
|
484
|
-
Throughput:
|
|
476
|
+
Throughput: 128,194 texts/sec
|
|
485
477
|
Per text: 0.01ms
|
|
486
478
|
Types covered: EMAIL, PHONE, SSN, CREDIT_CARD
|
|
479
|
+
(ADDRESS and cued NAME with pii_detect_names_and_addresses=True)
|
|
487
480
|
```
|
|
488
481
|
|
|
489
482
|
Correctness spot-check:
|
|
@@ -493,29 +486,29 @@ Input: Contact me at test@example.com or 555-123-4567. SSN: 123-45-6789
|
|
|
493
486
|
Redacted: Contact me at [EMAIL] or [PHONE]. SSN: [SSN]
|
|
494
487
|
```
|
|
495
488
|
|
|
496
|
-
PII runs before content reaches storage. It never gets embedded. It never gets logged.
|
|
489
|
+
PII runs before content reaches storage. It never gets embedded. It never gets logged. In encrypt mode, slot values that contain PII are stored as keyed fingerprints, so no plaintext email or phone number is written to disk.
|
|
497
490
|
|
|
498
|
-
### 5. Vector index
|
|
491
|
+
### 5. Vector index: 10K × 1,536-dim vectors
|
|
499
492
|
|
|
500
493
|
Pure-NumPy brute force, no FAISS dependency.
|
|
501
494
|
|
|
502
495
|
```
|
|
503
|
-
Index build: 0.
|
|
504
|
-
Search p50: 0.
|
|
505
|
-
Search p95: 1.
|
|
496
|
+
Index build: 0.011 to 0.017s
|
|
497
|
+
Search p50: 0.8ms
|
|
498
|
+
Search p95: 1.0ms
|
|
506
499
|
Self-retrieval accuracy: PASS
|
|
507
500
|
```
|
|
508
501
|
|
|
509
502
|
FAISS is available as an optional backend for datasets beyond ~100K vectors.
|
|
510
503
|
|
|
511
|
-
### 6. End-to-end
|
|
504
|
+
### 6. End-to-end: customer support agent
|
|
512
505
|
|
|
513
|
-
50 mixed factual
|
|
506
|
+
50 mixed factual and experiential memories, 20 semantic searches, one PII-laden write with redaction verification.
|
|
514
507
|
|
|
515
508
|
```
|
|
516
|
-
Add 50 memories: 0.03s (1,
|
|
517
|
-
Search p50:
|
|
518
|
-
PII add + redact:
|
|
509
|
+
Add 50 memories: 0.03s (about 1,540/sec)
|
|
510
|
+
Search p50: 4.9ms
|
|
511
|
+
PII add + redact: 1.4ms
|
|
519
512
|
PII correctly redacted: PASS
|
|
520
513
|
```
|
|
521
514
|
|
|
@@ -526,7 +519,7 @@ Input: Customer John Smith, email john@acme.com, SSN 123-45-6789 called about
|
|
|
526
519
|
Stored: Customer John Smith, email [EMAIL], SSN [SSN] called about billing
|
|
527
520
|
```
|
|
528
521
|
|
|
529
|
-
|
|
522
|
+
Rerun `python benchmarks/run_benchmarks.py` on your own hardware; the script prints everything shown here.
|
|
530
523
|
|
|
531
524
|
---
|
|
532
525
|
|
|
@@ -17,8 +17,10 @@ from __future__ import annotations
|
|
|
17
17
|
import asyncio
|
|
18
18
|
import contextlib
|
|
19
19
|
import hashlib
|
|
20
|
+
import hmac
|
|
20
21
|
import inspect
|
|
21
22
|
import logging
|
|
23
|
+
import os
|
|
22
24
|
import warnings
|
|
23
25
|
from collections.abc import Awaitable, Callable
|
|
24
26
|
from datetime import datetime, timedelta, timezone
|
|
@@ -62,6 +64,7 @@ from contextdb.store.factory import open_store
|
|
|
62
64
|
from contextdb.utils.embeddings import EmbeddingProvider, get_embedding_provider, wrap_embedder
|
|
63
65
|
from contextdb.utils.llm import LazyLLM, LLMProvider
|
|
64
66
|
|
|
67
|
+
_SLOT_FINGERPRINT_PREFIX = "pii:"
|
|
65
68
|
_logger = logging.getLogger(__name__)
|
|
66
69
|
|
|
67
70
|
if TYPE_CHECKING:
|
|
@@ -195,6 +198,7 @@ class ContextDB:
|
|
|
195
198
|
self._pii = PIIDetector(
|
|
196
199
|
action=self.config.pii_action,
|
|
197
200
|
encryption_key=self.config.pii_encryption_key,
|
|
201
|
+
detect_names_and_addresses=self.config.pii_detect_names_and_addresses,
|
|
198
202
|
)
|
|
199
203
|
|
|
200
204
|
# Graphs (local imports to avoid circular references at module load)
|
|
@@ -426,6 +430,7 @@ class ContextDB:
|
|
|
426
430
|
item.slot_class = chosen.slot_class
|
|
427
431
|
if item.slot_value is None:
|
|
428
432
|
item.slot_value = canonical_slot_value(source_text, chosen)
|
|
433
|
+
item.slot_value = self._protect_slot_value(item.slot_value, source_text)
|
|
429
434
|
item.negated = infer_negation(source_text)
|
|
430
435
|
if item.tenant_id is None:
|
|
431
436
|
item.tenant_id = self.tenant_id
|
|
@@ -435,6 +440,29 @@ class ContextDB:
|
|
|
435
440
|
item.session_id = self.session_id
|
|
436
441
|
return item
|
|
437
442
|
|
|
443
|
+
def _protect_slot_value(self, value: str | None, source_text: str) -> str | None:
|
|
444
|
+
"""Keep PII out of the stored slot value, which exists only for comparison.
|
|
445
|
+
|
|
446
|
+
Content is redacted before storage, but the slot value is derived
|
|
447
|
+
from the original utterance so two different emails stay different.
|
|
448
|
+
When it contains a detected PII span, store a keyed fingerprint.
|
|
449
|
+
"""
|
|
450
|
+
if value is None or value.startswith(_SLOT_FINGERPRINT_PREFIX):
|
|
451
|
+
return value
|
|
452
|
+
if self._pii is None or self._pii.action not in {"redact", "encrypt"}:
|
|
453
|
+
return value
|
|
454
|
+
folded = value.casefold()
|
|
455
|
+
spans = [source_text[a.start : a.end].casefold() for a in self._pii.detect(source_text)]
|
|
456
|
+
if not any(span and span in folded for span in spans):
|
|
457
|
+
return value
|
|
458
|
+
key = (
|
|
459
|
+
self.config.pii_encryption_key
|
|
460
|
+
or os.environ.get("CONTEXTDB_PII_KEY")
|
|
461
|
+
or "contextdb-slot-value"
|
|
462
|
+
).encode("utf-8")
|
|
463
|
+
digest = hmac.new(key, value.encode("utf-8"), hashlib.sha256).hexdigest()
|
|
464
|
+
return f"{_SLOT_FINGERPRINT_PREFIX}{digest}"
|
|
465
|
+
|
|
438
466
|
def _pii_shadow(self, annotations: list[Any], processed: str) -> str | None:
|
|
439
467
|
"""Typed stand-in so a query with a real email can still retrieve [EMAIL]."""
|
|
440
468
|
if not annotations:
|
|
@@ -453,6 +481,7 @@ class ContextDB:
|
|
|
453
481
|
self._pii = PIIDetector(
|
|
454
482
|
action=self.config.pii_action,
|
|
455
483
|
encryption_key=self.config.pii_encryption_key,
|
|
484
|
+
detect_names_and_addresses=self.config.pii_detect_names_and_addresses,
|
|
456
485
|
)
|
|
457
486
|
return self._pii.process(text)[0]
|
|
458
487
|
|
|
@@ -1094,7 +1123,7 @@ class ContextDB:
|
|
|
1094
1123
|
processed_query
|
|
1095
1124
|
)
|
|
1096
1125
|
scored = await self._retrieval.search_scored(
|
|
1097
|
-
|
|
1126
|
+
processed_query, query_embedding, top_k=top_k * 3, user_id=uid
|
|
1098
1127
|
)
|
|
1099
1128
|
except Exception as exc: # noqa: BLE001
|
|
1100
1129
|
if not self.config.lexical_on_embed_failure:
|
|
@@ -1105,7 +1134,8 @@ class ContextDB:
|
|
|
1105
1134
|
floor = self.trust_policy.relevance_floor
|
|
1106
1135
|
# Floor is on cosine (comparable across queries), not RRF×salience
|
|
1107
1136
|
# (which is ~0.01–0.1 and would abstain on everything).
|
|
1108
|
-
|
|
1137
|
+
# An exact identifier match is evidence even when its cosine is low.
|
|
1138
|
+
scored = [s for s in scored if s.cosine >= floor or "lexical" in s.per_graph]
|
|
1109
1139
|
items = [s.item for s in scored if s.item.is_valid_at(moment)]
|
|
1110
1140
|
if memory_type is not None:
|
|
1111
1141
|
items = [m for m in items if m.memory_type == memory_type]
|
|
@@ -1364,12 +1394,22 @@ class ContextDB:
|
|
|
1364
1394
|
# by the raw text, and extracted trust cannot exceed what
|
|
1365
1395
|
# the raw itself would have been assigned.
|
|
1366
1396
|
self._apply_slot(new_item, raw_text=raw.content, extractor_bound=True)
|
|
1397
|
+
if raw.slot_value is not None and (new_item.entity_key, new_item.attribute_key) == (
|
|
1398
|
+
raw.entity_key,
|
|
1399
|
+
raw.attribute_key,
|
|
1400
|
+
):
|
|
1401
|
+
# Stored content is already redacted, so two different
|
|
1402
|
+
# emails both read "[EMAIL]". The pending row kept the
|
|
1403
|
+
# value derived from the original utterance.
|
|
1404
|
+
new_item.slot_value = raw.slot_value
|
|
1367
1405
|
if new_item.epistemic_source == "user_stated" and not infer_slot(
|
|
1368
1406
|
raw.content
|
|
1369
1407
|
):
|
|
1370
1408
|
new_item.epistemic_source = "agent_inferred"
|
|
1371
1409
|
self._screen_item(new_item)
|
|
1372
|
-
saved, outcome = await self._trust.write(
|
|
1410
|
+
saved, outcome = await self._trust.write(
|
|
1411
|
+
new_item, user_id=self.user_id, replacing=raw.id
|
|
1412
|
+
)
|
|
1373
1413
|
if outcome in {"added", "superseded", "contested"}:
|
|
1374
1414
|
await self._link(saved.id)
|
|
1375
1415
|
await store.update(
|
|
@@ -1726,7 +1766,8 @@ class ContextDB:
|
|
|
1726
1766
|
|
|
1727
1767
|
Checks, in order: (1) no rows for the user in any lifecycle state;
|
|
1728
1768
|
(2) every id from the latest FORGET deletion set is gone from the
|
|
1729
|
-
table; (3) no deleted id remains searchable in the vector index
|
|
1769
|
+
table; (3) no deleted id remains searchable in the vector index;
|
|
1770
|
+
(4) no graph still holds an edge or entity attribute source for one.
|
|
1730
1771
|
"""
|
|
1731
1772
|
await self._ensure_init()
|
|
1732
1773
|
store = self._require_store()
|
|
@@ -1748,6 +1789,10 @@ class ContextDB:
|
|
|
1748
1789
|
residue = set(deleted_ids) & await store.index_ids()
|
|
1749
1790
|
if residue:
|
|
1750
1791
|
return False
|
|
1792
|
+
for graph in self._graphs.values():
|
|
1793
|
+
graph_residue = getattr(graph, "residue", None)
|
|
1794
|
+
if graph_residue is not None and await graph_residue(deleted_ids):
|
|
1795
|
+
return False
|
|
1751
1796
|
return True
|
|
1752
1797
|
|
|
1753
1798
|
async def stats(self) -> dict[str, Any]:
|
|
@@ -78,6 +78,15 @@ class ContextDBConfig(BaseSettings):
|
|
|
78
78
|
default="redact",
|
|
79
79
|
description="How detected PII should be handled before storage.",
|
|
80
80
|
)
|
|
81
|
+
pii_detect_names_and_addresses: bool = Field(
|
|
82
|
+
default=False,
|
|
83
|
+
description=(
|
|
84
|
+
"Also redact street addresses and cued names (\"my name is ...\"). "
|
|
85
|
+
"Off by default because a redacted address or name is hidden from "
|
|
86
|
+
"the agent too; turn it on when those must not reach storage or "
|
|
87
|
+
"embeddings."
|
|
88
|
+
),
|
|
89
|
+
)
|
|
81
90
|
pii_encryption_key: str | None = Field(
|
|
82
91
|
default=None,
|
|
83
92
|
description=(
|
|
@@ -95,9 +104,10 @@ class ContextDBConfig(BaseSettings):
|
|
|
95
104
|
default=-1.0,
|
|
96
105
|
description=(
|
|
97
106
|
"Minimum cosine for a recall hit. -1 disables the floor. "
|
|
98
|
-
"0 is NOT off (it drops negative cosines).
|
|
99
|
-
"
|
|
100
|
-
"
|
|
107
|
+
"0 is NOT off (it drops negative cosines). Cosine ranges are "
|
|
108
|
+
"embedder-specific (multilingual-e5 scores almost everything "
|
|
109
|
+
"between 0.7 and 1.0), so calibrate on your own labelled data. "
|
|
110
|
+
"See TrustPolicy.relevance_floor."
|
|
101
111
|
),
|
|
102
112
|
)
|
|
103
113
|
enable_salience: bool = Field(
|
|
@@ -53,10 +53,12 @@ class TrustPolicy(BaseModel):
|
|
|
53
53
|
ge=-1.0,
|
|
54
54
|
le=1.0,
|
|
55
55
|
description=(
|
|
56
|
-
"Minimum cosine for a recall hit. -1 disables the floor
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
56
|
+
"Minimum cosine for a recall hit. -1 disables the floor. Cosine "
|
|
57
|
+
"ranges are embedder-specific: multilingual-e5 models score almost "
|
|
58
|
+
"everything between 0.7 and 1.0, so no fixed band works across "
|
|
59
|
+
"embedders. Calibrate on labelled examples from your own data. A "
|
|
60
|
+
"floor cannot tell whether a memory answers a request; pass the "
|
|
61
|
+
"entity and attribute an action needs to recall_for_action instead."
|
|
60
62
|
),
|
|
61
63
|
)
|
|
62
64
|
# Unknown (non-vocabulary) slots cannot gate actions via the
|