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.
Files changed (151) hide show
  1. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/CHANGELOG.md +36 -0
  2. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/PKG-INFO +43 -50
  3. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/README.md +42 -49
  4. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/__init__.py +1 -1
  5. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/client.py +49 -4
  6. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/core/config.py +13 -3
  7. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/core/policy.py +6 -4
  8. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/core/slots.py +49 -3
  9. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/dynamics/retrieval.py +50 -1
  10. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/dynamics/trust.py +11 -3
  11. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/graphs/entity.py +118 -12
  12. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/privacy/pii_detector.py +40 -5
  13. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/store/base.py +19 -0
  14. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/store/postgres_store.py +21 -1
  15. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/store/sqlite_store.py +23 -1
  16. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/docs/index.md +8 -4
  17. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/docs/prompts.md +1 -1
  18. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/docs/quickstart.md +3 -1
  19. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/pyproject.toml +1 -1
  20. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/evals/test_trust_model.py +193 -0
  21. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_pii_detector.py +33 -0
  22. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_retrieval.py +12 -0
  23. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/.github/CODEOWNERS +0 -0
  24. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/.github/copilot-instructions.md +0 -0
  25. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/.github/pull_request_template.md +0 -0
  26. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/.github/workflows/ci.yml +0 -0
  27. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/.github/workflows/publish.yml +0 -0
  28. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/.gitignore +0 -0
  29. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/.pre-commit-config.yaml +0 -0
  30. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/COMMERCIAL.md +0 -0
  31. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/CONTRIBUTING.md +0 -0
  32. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/LICENSE +0 -0
  33. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/NOTICE +0 -0
  34. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/OPEN_CORE.md +0 -0
  35. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/ai-tools/.cursorrules +0 -0
  36. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/ai-tools/CLAUDE.md +0 -0
  37. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/ai-tools/README.md +0 -0
  38. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/ai-tools/llms-full.txt +0 -0
  39. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/ai-tools/llms.txt +0 -0
  40. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/assets/README.md +0 -0
  41. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/assets/architecture.png +0 -0
  42. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/assets/benchmarks.png +0 -0
  43. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/assets/hero.png +0 -0
  44. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/assets/performance.png +0 -0
  45. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/assets/problem.png +0 -0
  46. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/assets/quickstart.png +0 -0
  47. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/benchmarks/__init__.py +0 -0
  48. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/benchmarks/run_benchmarks.py +0 -0
  49. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/benchmarks/trust_bakeoff.py +0 -0
  50. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/agents/__init__.py +0 -0
  51. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/agents/memory_bus.py +0 -0
  52. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/agents/rl_manager.py +0 -0
  53. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/cli.py +0 -0
  54. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/connectors/__init__.py +0 -0
  55. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/connectors/base.py +0 -0
  56. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/connectors/postgres.py +0 -0
  57. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/core/__init__.py +0 -0
  58. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/core/clock.py +0 -0
  59. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/core/exceptions.py +0 -0
  60. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/core/models.py +0 -0
  61. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/dynamics/__init__.py +0 -0
  62. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/dynamics/evolution.py +0 -0
  63. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/dynamics/formation.py +0 -0
  64. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/dynamics/salience.py +0 -0
  65. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/graphs/__init__.py +0 -0
  66. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/graphs/base.py +0 -0
  67. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/graphs/causal.py +0 -0
  68. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/graphs/semantic.py +0 -0
  69. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/graphs/temporal.py +0 -0
  70. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/integrations/__init__.py +0 -0
  71. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/integrations/act.py +0 -0
  72. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/integrations/autogen.py +0 -0
  73. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/integrations/crewai.py +0 -0
  74. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/integrations/langchain.py +0 -0
  75. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/integrations/livekit.py +0 -0
  76. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/integrations/openai_tools.py +0 -0
  77. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/integrations/pipecat.py +0 -0
  78. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/integrations/prompting.py +0 -0
  79. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/mcp.py +0 -0
  80. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/memory/__init__.py +0 -0
  81. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/memory/experiential.py +0 -0
  82. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/memory/factual.py +0 -0
  83. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/memory/working.py +0 -0
  84. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/pool.py +0 -0
  85. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/privacy/__init__.py +0 -0
  86. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/privacy/audit.py +0 -0
  87. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/privacy/injection.py +0 -0
  88. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/privacy/retention.py +0 -0
  89. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/py.typed +0 -0
  90. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/serve.py +0 -0
  91. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/store/__init__.py +0 -0
  92. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/store/factory.py +0 -0
  93. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/store/pg_sql.py +0 -0
  94. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/store/vector_index.py +0 -0
  95. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/utils/__init__.py +0 -0
  96. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/utils/embeddings.py +0 -0
  97. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/utils/llm.py +0 -0
  98. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/contextdb/utils/migrations.py +0 -0
  99. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/docs/api.md +0 -0
  100. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/docs/architecture.md +0 -0
  101. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/docs/connectors.md +0 -0
  102. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/docs/memory_evolution.md +0 -0
  103. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/docs/migrations.md +0 -0
  104. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/docs/multi_tenant.md +0 -0
  105. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/docs/postgres.md +0 -0
  106. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/docs/serve.md +0 -0
  107. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/docs/trust_policy.md +0 -0
  108. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/examples/customer_support.py +0 -0
  109. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/examples/multi_agent_team.py +0 -0
  110. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/examples/phone_agent.py +0 -0
  111. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/examples/research_assistant.py +0 -0
  112. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/legal/CLA-entity.md +0 -0
  113. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/legal/CLA-individual.md +0 -0
  114. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/legal/signed/README.md +0 -0
  115. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/mkdocs.yml +0 -0
  116. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/scripts/check_open_core_boundary.py +0 -0
  117. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/scripts/check_release.py +0 -0
  118. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/__init__.py +0 -0
  119. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/conftest.py +0 -0
  120. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/evals/__init__.py +0 -0
  121. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/integration/__init__.py +0 -0
  122. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/integration/test_client.py +0 -0
  123. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/integration/test_langchain_integration.py +0 -0
  124. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/pg_util.py +0 -0
  125. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/test_version.py +0 -0
  126. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/__init__.py +0 -0
  127. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_audit.py +0 -0
  128. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_config.py +0 -0
  129. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_connectors.py +0 -0
  130. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_decision_scope.py +0 -0
  131. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_embeddings.py +0 -0
  132. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_evolution.py +0 -0
  133. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_formation.py +0 -0
  134. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_graphs.py +0 -0
  135. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_host_api.py +0 -0
  136. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_init.py +0 -0
  137. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_memory_bus.py +0 -0
  138. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_memory_evolution.py +0 -0
  139. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_models.py +0 -0
  140. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_open_core_boundary.py +0 -0
  141. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_pg_sql.py +0 -0
  142. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_postgres_concurrency.py +0 -0
  143. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_postgres_connector.py +0 -0
  144. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_postgres_memory_evolution.py +0 -0
  145. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_retention.py +0 -0
  146. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_scoped_id_access.py +0 -0
  147. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_serve.py +0 -0
  148. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_store.py +0 -0
  149. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_trust_policy.py +0 -0
  150. {pycontextdb-0.4.6 → pycontextdb-0.4.7}/tests/unit/test_vector_index.py +0 -0
  151. {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.6
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/blob/main/tests/"><img src="https://img.shields.io/badge/tests-195%20passing-brightgreen.svg" alt="Tests"></a>
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-%3C5ms_%40_5K-brightgreen.svg" alt="Search p95"></a>
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,900+ memories/sec |
101
- | **Search latency (1K memories)** | p50 **3.4ms** · p95 **4.5ms** |
102
- | **Search latency (5K memories)** | p50 **3.9ms** · p95 **5.0ms** |
103
- | **Vector search (10K × 1,536d)** | p50 **0.8ms** · p95 **1.0ms** |
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** | 195 passing (incl. 34 trust-model acceptance evals) · ruff clean · mypy `--strict` clean |
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 ≈ 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**.
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
- 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:
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 | 1.5ms |
130
- | `raw-store-baseline` (untyped control) | 50% | 88% | NO | no | 0.9ms |
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: `python benchmarks/trust_bakeoff.py` — acceptance evals live in `tests/evals/`. Mem0/Zep/Letta/supermemory arms implement the `MemoryArm` protocol and plug into the same harness.
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
- <p align="center">
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,930 writes/sec
522
- Average per write: 0.52ms
515
+ Throughput: about 1,600 to 1,800 writes/sec
516
+ Average per write: 0.6ms
523
517
  ```
524
518
 
525
- ### 2. Search latency — 100 queries against 1,000 memories
519
+ ### 2. Search latency: 100 queries against 1,000 memories
526
520
 
527
521
  ```
528
- p50: 3.40ms
529
- p95: 4.54ms
530
- p99: 4.75ms
531
- Mean: 3.59ms
522
+ p50: 11.0ms
523
+ p95: 14.3ms
524
+ p99: 21.1ms
525
+ Mean: 11.7ms
532
526
  ```
533
527
 
534
- Target was <100ms p95. We hit it by an order of magnitude with room to spare.
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.47ms | 3.26ms | 4.25ms |
543
- | 500 | 0.45ms | 3.59ms | 4.16ms |
544
- | 1,000 | 0.43ms | 3.50ms | 4.24ms |
545
- | 5,000 | 0.53ms | 3.89ms | 4.96ms |
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 — 1,000 texts
539
+ ### 4. PII detection & redaction: 1,000 texts
548
540
 
549
541
  ```
550
- Throughput: 111,745 texts/sec
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 — 10K × 1,536-dim vectors
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.013s (~800K vectors/sec)
570
- Search p50: 0.80ms
571
- Search p95: 1.04ms
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 — customer support agent
570
+ ### 6. End-to-end: customer support agent
578
571
 
579
- 50 mixed factual/experiential memories, 20 semantic searches, one PII-laden write with redaction verification.
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,849/sec)
583
- Search p50: 1.70ms
584
- PII add + redact: 0.76ms
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
- Numbers above are from a MacBook-class laptop with no FAISS. Rerun `python benchmarks/run_benchmarks.py` on your own hardware — the script prints everything you see here.
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/blob/main/tests/"><img src="https://img.shields.io/badge/tests-195%20passing-brightgreen.svg" alt="Tests"></a>
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-%3C5ms_%40_5K-brightgreen.svg" alt="Search p95"></a>
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,900+ memories/sec |
35
- | **Search latency (1K memories)** | p50 **3.4ms** · p95 **4.5ms** |
36
- | **Search latency (5K memories)** | p50 **3.9ms** · p95 **5.0ms** |
37
- | **Vector search (10K × 1,536d)** | p50 **0.8ms** · p95 **1.0ms** |
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** | 195 passing (incl. 34 trust-model acceptance evals) · ruff clean · mypy `--strict` clean |
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 ≈ 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**.
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
- 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:
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 | 1.5ms |
64
- | `raw-store-baseline` (untyped control) | 50% | 88% | NO | no | 0.9ms |
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: `python benchmarks/trust_bakeoff.py` — acceptance evals live in `tests/evals/`. Mem0/Zep/Letta/supermemory arms implement the `MemoryArm` protocol and plug into the same harness.
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
- <p align="center">
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,930 writes/sec
456
- Average per write: 0.52ms
449
+ Throughput: about 1,600 to 1,800 writes/sec
450
+ Average per write: 0.6ms
457
451
  ```
458
452
 
459
- ### 2. Search latency — 100 queries against 1,000 memories
453
+ ### 2. Search latency: 100 queries against 1,000 memories
460
454
 
461
455
  ```
462
- p50: 3.40ms
463
- p95: 4.54ms
464
- p99: 4.75ms
465
- Mean: 3.59ms
456
+ p50: 11.0ms
457
+ p95: 14.3ms
458
+ p99: 21.1ms
459
+ Mean: 11.7ms
466
460
  ```
467
461
 
468
- Target was <100ms p95. We hit it by an order of magnitude with room to spare.
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.47ms | 3.26ms | 4.25ms |
477
- | 500 | 0.45ms | 3.59ms | 4.16ms |
478
- | 1,000 | 0.43ms | 3.50ms | 4.24ms |
479
- | 5,000 | 0.53ms | 3.89ms | 4.96ms |
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 — 1,000 texts
473
+ ### 4. PII detection & redaction: 1,000 texts
482
474
 
483
475
  ```
484
- Throughput: 111,745 texts/sec
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 — 10K × 1,536-dim vectors
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.013s (~800K vectors/sec)
504
- Search p50: 0.80ms
505
- Search p95: 1.04ms
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 — customer support agent
504
+ ### 6. End-to-end: customer support agent
512
505
 
513
- 50 mixed factual/experiential memories, 20 semantic searches, one PII-laden write with redaction verification.
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,849/sec)
517
- Search p50: 1.70ms
518
- PII add + redact: 0.76ms
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
- Numbers above are from a MacBook-class laptop with no FAISS. Rerun `python benchmarks/run_benchmarks.py` on your own hardware — the script prints everything you see here.
522
+ Rerun `python benchmarks/run_benchmarks.py` on your own hardware; the script prints everything shown here.
530
523
 
531
524
  ---
532
525
 
@@ -41,7 +41,7 @@ from contextdb.core.policy import TrustPolicy
41
41
  from contextdb.pool import ContextDBPool
42
42
  from contextdb.utils.embeddings import EmbeddingProvider
43
43
 
44
- __version__ = "0.4.6"
44
+ __version__ = "0.4.7"
45
45
 
46
46
  __all__ = [
47
47
  "Clock",
@@ -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
- query, query_embedding, top_k=top_k * 3, user_id=uid
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
- scored = [s for s in scored if s.cosine >= floor]
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(new_item, user_id=self.user_id)
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). Production "
99
- "embedders should use 0.15–0.25 — below that, top-1 "
100
- "similarity is a guess, not a memory."
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 (safe "
57
- "default for small stores / mock embedders). With a production "
58
- "embedder, 0.15–0.25 is the right band — below that, top-1 "
59
- "similarity is a guess, not a memory."
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