mem0-postgres-store 0.1.0__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.
- mem0_postgres_store-0.1.0/.github/dependabot.yml +6 -0
- mem0_postgres_store-0.1.0/.github/workflows/checks.yml +60 -0
- mem0_postgres_store-0.1.0/.github/workflows/publish.yml +49 -0
- mem0_postgres_store-0.1.0/.gitignore +19 -0
- mem0_postgres_store-0.1.0/.python-version +1 -0
- mem0_postgres_store-0.1.0/LICENSE +21 -0
- mem0_postgres_store-0.1.0/PKG-INFO +438 -0
- mem0_postgres_store-0.1.0/README.md +423 -0
- mem0_postgres_store-0.1.0/alembic.ini +3 -0
- mem0_postgres_store-0.1.0/compose.yaml +19 -0
- mem0_postgres_store-0.1.0/docs/design.md +33 -0
- mem0_postgres_store-0.1.0/examples/fastapi_integration.py +28 -0
- mem0_postgres_store-0.1.0/examples/minimal.py +15 -0
- mem0_postgres_store-0.1.0/examples/postgres/init.sql +10 -0
- mem0_postgres_store-0.1.0/pyproject.toml +58 -0
- mem0_postgres_store-0.1.0/src/mem0_postgres_store/__init__.py +17 -0
- mem0_postgres_store-0.1.0/src/mem0_postgres_store/context.py +68 -0
- mem0_postgres_store-0.1.0/src/mem0_postgres_store/database.py +153 -0
- mem0_postgres_store-0.1.0/src/mem0_postgres_store/history.py +173 -0
- mem0_postgres_store-0.1.0/src/mem0_postgres_store/integration.py +125 -0
- mem0_postgres_store-0.1.0/src/mem0_postgres_store/migrate.py +35 -0
- mem0_postgres_store-0.1.0/src/mem0_postgres_store/migrations/env.py +27 -0
- mem0_postgres_store-0.1.0/src/mem0_postgres_store/migrations/versions/0001_storage.py +88 -0
- mem0_postgres_store-0.1.0/src/mem0_postgres_store/migrations/versions/0002_append_only_history.py +29 -0
- mem0_postgres_store-0.1.0/src/mem0_postgres_store/py.typed +0 -0
- mem0_postgres_store-0.1.0/src/mem0_postgres_store/schema.py +51 -0
- mem0_postgres_store-0.1.0/src/mem0_postgres_store/vector.py +87 -0
- mem0_postgres_store-0.1.0/tests/conftest.py +81 -0
- mem0_postgres_store-0.1.0/tests/test_contract.py +121 -0
- mem0_postgres_store-0.1.0/tests/test_history.py +200 -0
- mem0_postgres_store-0.1.0/tests/test_sdk.py +265 -0
- mem0_postgres_store-0.1.0/tests/test_security.py +212 -0
- mem0_postgres_store-0.1.0/uv.lock +953 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
name: Checks
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
push:
|
|
6
|
+
branches: [main, master]
|
|
7
|
+
workflow_call:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
lint:
|
|
14
|
+
name: Ruff and mypy
|
|
15
|
+
runs-on: ubuntu-24.04
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
18
|
+
- uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
|
|
19
|
+
with:
|
|
20
|
+
version: "0.7.13"
|
|
21
|
+
enable-cache: true
|
|
22
|
+
- run: uv sync --frozen
|
|
23
|
+
- run: uv run ruff check .
|
|
24
|
+
- run: uv run ruff format --check .
|
|
25
|
+
- run: uv run mypy
|
|
26
|
+
|
|
27
|
+
test:
|
|
28
|
+
name: PostgreSQL integration tests
|
|
29
|
+
runs-on: ubuntu-24.04
|
|
30
|
+
timeout-minutes: 15
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
33
|
+
- uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
|
|
34
|
+
with:
|
|
35
|
+
version: "0.7.13"
|
|
36
|
+
enable-cache: true
|
|
37
|
+
- run: uv sync --frozen
|
|
38
|
+
- run: uv run pytest --junitxml=test-results.xml
|
|
39
|
+
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
40
|
+
if: always()
|
|
41
|
+
with:
|
|
42
|
+
name: test-results
|
|
43
|
+
path: test-results.xml
|
|
44
|
+
|
|
45
|
+
build:
|
|
46
|
+
name: Build distribution
|
|
47
|
+
runs-on: ubuntu-24.04
|
|
48
|
+
steps:
|
|
49
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
50
|
+
- uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
|
|
51
|
+
with:
|
|
52
|
+
version: "0.7.13"
|
|
53
|
+
- run: uv build --no-sources
|
|
54
|
+
- name: Check wheel import and packaged migrations
|
|
55
|
+
run: |
|
|
56
|
+
uv run --no-project --python 3.12 --with dist/*.whl python -c "from importlib.resources import files; import mem0_postgres_store; assert files('mem0_postgres_store').joinpath('migrations/versions/0001_storage.py').is_file()"
|
|
57
|
+
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
58
|
+
with:
|
|
59
|
+
name: distributions
|
|
60
|
+
path: dist/*
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
concurrency:
|
|
11
|
+
group: publish-${{ github.ref }}
|
|
12
|
+
cancel-in-progress: false
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
checks:
|
|
16
|
+
uses: ./.github/workflows/checks.yml
|
|
17
|
+
|
|
18
|
+
publish:
|
|
19
|
+
name: Publish verified distributions
|
|
20
|
+
needs: checks
|
|
21
|
+
runs-on: ubuntu-24.04
|
|
22
|
+
environment:
|
|
23
|
+
name: pypi
|
|
24
|
+
url: https://pypi.org/p/mem0-postgres-store
|
|
25
|
+
permissions:
|
|
26
|
+
id-token: write
|
|
27
|
+
contents: read
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
30
|
+
- uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
|
|
31
|
+
with:
|
|
32
|
+
version: "0.7.13"
|
|
33
|
+
- name: Require tag to match package version
|
|
34
|
+
env:
|
|
35
|
+
RELEASE_TAG: ${{ github.ref_name }}
|
|
36
|
+
run: |
|
|
37
|
+
uv run --no-project --python 3.12 python - <<'PY'
|
|
38
|
+
import os
|
|
39
|
+
import tomllib
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
version = tomllib.loads(Path("pyproject.toml").read_text())["project"]["version"]
|
|
42
|
+
assert os.environ["RELEASE_TAG"] == f"v{version}", "Tag must match pyproject.toml version"
|
|
43
|
+
PY
|
|
44
|
+
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
45
|
+
with:
|
|
46
|
+
name: distributions
|
|
47
|
+
path: dist
|
|
48
|
+
- name: Publish with PyPI Trusted Publishing
|
|
49
|
+
run: uv publish --trusted-publishing always
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Python-generated files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[oc]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
wheels/
|
|
7
|
+
*.egg-info
|
|
8
|
+
|
|
9
|
+
# Virtual environments
|
|
10
|
+
.venv
|
|
11
|
+
|
|
12
|
+
# Tooling and local secrets
|
|
13
|
+
.pytest_cache/
|
|
14
|
+
.ruff_cache/
|
|
15
|
+
.mypy_cache/
|
|
16
|
+
.env
|
|
17
|
+
.env.*
|
|
18
|
+
!.env.example
|
|
19
|
+
test-results.xml
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mg30
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: mem0-postgres-store
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Tenant-isolated PostgreSQL persistence for Mem0 OSS 2.2.1
|
|
5
|
+
Project-URL: Repository, https://github.com/Mg30/mem0-postgres-store
|
|
6
|
+
Project-URL: Issues, https://github.com/Mg30/mem0-postgres-store/issues
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Python: <3.13,>=3.12
|
|
10
|
+
Requires-Dist: alembic<2,>=1.15
|
|
11
|
+
Requires-Dist: mem0ai==2.2.1
|
|
12
|
+
Requires-Dist: psycopg[binary,pool]<4,>=3.2
|
|
13
|
+
Requires-Dist: sqlalchemy<2.1,>=2.0.40
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# mem0-postgres-store
|
|
17
|
+
|
|
18
|
+
PostgreSQL history and recent-message persistence for **Mem0 OSS 2.2.1**, with
|
|
19
|
+
transaction-scoped tenant isolation across history, messages, vector memories,
|
|
20
|
+
and entity vectors. Python 3.12; managed, tested, built, and published with **uv**.
|
|
21
|
+
|
|
22
|
+
## The problem this solves
|
|
23
|
+
|
|
24
|
+
Mem0's Python SDK writes history and extraction context to SQLite even when its
|
|
25
|
+
main memories use pgvector. That leaves local files and an additional persistence
|
|
26
|
+
system to manage across application replicas. Its pgvector connection pool also
|
|
27
|
+
does not carry an application's authenticated tenant context into PostgreSQL RLS.
|
|
28
|
+
|
|
29
|
+
This library keeps those records in PostgreSQL and enforces tenant context on
|
|
30
|
+
both connection pools. It does not fork Mem0, overwrite installed source files,
|
|
31
|
+
shadow the `mem0` package, or open SQLite during initialization. It is a library,
|
|
32
|
+
not an HTTP server. Existing SQLite data is **not automatically imported**.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
Until a release is published, install from the repository:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
uv add 'mem0-postgres-store @ git+https://github.com/Mg30/mem0-postgres-store.git'
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
After publication to PyPI:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uv add mem0-postgres-store
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The dependency pins `mem0ai==2.2.1`. Other SDK versions are rejected. For development:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
git clone https://github.com/Mg30/mem0-postgres-store.git
|
|
52
|
+
cd mem0-postgres-store
|
|
53
|
+
uv sync --frozen
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Prepare PostgreSQL
|
|
57
|
+
|
|
58
|
+
Local development uses the supplied Compose example:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
docker compose up -d --wait
|
|
62
|
+
export MEM0_MIGRATION_URL='postgresql+psycopg://mem0_migrator:local-migration-only@localhost:55432/mem0'
|
|
63
|
+
export MEM0_DATABASE_URL='postgresql+psycopg://mem0_runtime:local-runtime-only@localhost:55432/mem0'
|
|
64
|
+
uv run python -m mem0_postgres_store.migrate --dimensions 1536
|
|
65
|
+
uv run python examples/minimal.py
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The example credentials are for local development only. The initialization script
|
|
69
|
+
runs only on an empty Compose volume. In production, the DBA installs the
|
|
70
|
+
`vector` extension in `public`, provisions separate migration and runtime roles,
|
|
71
|
+
and supplies credentials through the application's configuration boundary.
|
|
72
|
+
Use TLS options in the URL where required by your deployment.
|
|
73
|
+
|
|
74
|
+
[Role provisioning](examples/postgres/init.sql) gives the migrator permission to
|
|
75
|
+
create the schema and Alembic version table. The runtime role is a non-superuser,
|
|
76
|
+
non-owner with `NOBYPASSRLS`, no privileged role memberships, and no schema CREATE,
|
|
77
|
+
TRUNCATE, DROP, or migration privileges. Do not grant it membership in the owner
|
|
78
|
+
role. Never supply the migration URL to an application.
|
|
79
|
+
|
|
80
|
+
The packaged Alembic revisions create:
|
|
81
|
+
|
|
82
|
+
| Table in `mem0_store` | Purpose |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| `history` | Append-only SDK audit events, scoped by tenant and memory ID |
|
|
85
|
+
| `messages` | Most recent 10 messages per tenant/session scope |
|
|
86
|
+
| `mem0_vectors` | Mem0's pgvector memory payloads and embeddings |
|
|
87
|
+
| `mem0_vectors_entities` | Mem0's lazy entity-vector collection |
|
|
88
|
+
|
|
89
|
+
All tables have indexed, explicit `tenant_id` columns, enabled **and forced** RLS,
|
|
90
|
+
and policies with both `USING` and `WITH CHECK` for all DML. A restrictive tenant
|
|
91
|
+
guard prevents additional permissive policies from broadening access. The runtime
|
|
92
|
+
role has only `SELECT`, `INSERT`, and `DELETE` (for tenant reset) on `history` and
|
|
93
|
+
`messages`, so audit rows cannot be rewritten; vector tables also allow `UPDATE`.
|
|
94
|
+
Runtime startup verifies role attributes, owner membership, RLS flags, destructive
|
|
95
|
+
and `UPDATE`-on-history privileges (so an un-upgraded database fails clearly), and
|
|
96
|
+
embedding dimensions. DBAs remain responsible for policy integrity.
|
|
97
|
+
|
|
98
|
+
Migrations are packaged in the wheel. The migration command above is a small
|
|
99
|
+
configuration wrapper around Alembic `upgrade head`. For direct Alembic usage:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
import os
|
|
103
|
+
from alembic import command
|
|
104
|
+
from mem0_postgres_store.migrate import migration_config
|
|
105
|
+
|
|
106
|
+
config = migration_config(
|
|
107
|
+
os.environ["MEM0_MIGRATION_URL"], dimensions=1536, runtime_role="mem0_runtime"
|
|
108
|
+
)
|
|
109
|
+
command.upgrade(config, "head")
|
|
110
|
+
command.current(config)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Alternatively, configure `sqlalchemy.url` in a **local, uncommitted** Alembic
|
|
114
|
+
configuration and run `uv run alembic -x dimensions=1536 -x runtime_role=mem0_runtime
|
|
115
|
+
upgrade head`. Never commit database passwords. The first migration fixes the
|
|
116
|
+
embedding dimension; changing the argument later does not resize existing columns.
|
|
117
|
+
Use an explicit new migration and re-embedding plan. Downgrade deletes data and is
|
|
118
|
+
an administrative operation, not an application reset.
|
|
119
|
+
|
|
120
|
+
## Synchronous Mem0
|
|
121
|
+
|
|
122
|
+
Set telemetry configuration **before importing Mem0**:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
export MEM0_TELEMETRY=false
|
|
126
|
+
# Set credentials/model names for your chosen providers in application config.
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
import os
|
|
131
|
+
from mem0 import Memory
|
|
132
|
+
from mem0_postgres_store import Database, database_vector_config, install, tenant_scope
|
|
133
|
+
|
|
134
|
+
# One Database per worker process; construct after migrations, before serving requests.
|
|
135
|
+
database = Database(os.environ["MEM0_DATABASE_URL"], embedding_dimensions=1536)
|
|
136
|
+
install(database) # Explicit, process-global, idempotent for this Database instance.
|
|
137
|
+
|
|
138
|
+
memory = Memory.from_config(
|
|
139
|
+
{
|
|
140
|
+
"history_db_path": "postgresql://mem0-postgres-store",
|
|
141
|
+
"vector_store": database_vector_config(database),
|
|
142
|
+
"llm": {"provider": "openai", "config": {"model": os.environ["MEM0_LLM_MODEL"]}},
|
|
143
|
+
"embedder": {
|
|
144
|
+
"provider": "openai",
|
|
145
|
+
"config": {
|
|
146
|
+
"model": os.environ["MEM0_EMBEDDING_MODEL"],
|
|
147
|
+
"embedding_dims": 1536,
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Values must come from verified authentication/authorization, not request parameters.
|
|
154
|
+
with tenant_scope(authenticated_principal.tenant_id):
|
|
155
|
+
added = memory.add("I enjoy hiking", user_id=authenticated_principal.user_id)
|
|
156
|
+
results = memory.search("hobbies", filters={"user_id": authenticated_principal.user_id})
|
|
157
|
+
history = memory.history(added["results"][0]["id"])
|
|
158
|
+
|
|
159
|
+
memory.close()
|
|
160
|
+
database.close() # At application shutdown, after all requests have finished.
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Provider configuration and credentials belong to your application. The example
|
|
164
|
+
uses the SDK's OpenAI providers; other SDK model providers can be configured.
|
|
165
|
+
`infer=False` avoids LLM extraction but still requires an embedder. CI uses
|
|
166
|
+
injected deterministic model implementations and makes no paid API calls.
|
|
167
|
+
|
|
168
|
+
`history_db_path` is an explicit binding marker, not a filesystem path or DSN.
|
|
169
|
+
`database_vector_config()` binds the SDK to the pools owned by `Database`.
|
|
170
|
+
Passing another history path, connection pool, collection, or vector DSN fails.
|
|
171
|
+
|
|
172
|
+
For direct storage use, see [the minimal runnable example](examples/minimal.py).
|
|
173
|
+
`PostgresHistoryStore(database)` exposes `add_history`, `batch_add_history`,
|
|
174
|
+
`get_history`, `save_messages`, `get_last_messages`, `reset`, and `close`.
|
|
175
|
+
|
|
176
|
+
## Asynchronous Mem0
|
|
177
|
+
|
|
178
|
+
Use the same bootstrap and config with `AsyncMemory`:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
from mem0 import AsyncMemory
|
|
182
|
+
from mem0_postgres_store import tenant_scope
|
|
183
|
+
|
|
184
|
+
memory = AsyncMemory.from_config(config)
|
|
185
|
+
with tenant_scope(authenticated_principal.tenant_id):
|
|
186
|
+
added = await memory.add("I enjoy hiking", user_id=authenticated_principal.user_id)
|
|
187
|
+
results = await memory.search("hobbies", filters={"user_id": authenticated_principal.user_id})
|
|
188
|
+
history = await memory.history(added["results"][0]["id"])
|
|
189
|
+
memory.close() # Mem0 2.2.1 close() is synchronous, including on AsyncMemory.
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Mem0 offloads synchronous storage calls using `asyncio.to_thread`. ContextVars
|
|
193
|
+
propagate through those calls. Its synchronous entity search uses a thread pool;
|
|
194
|
+
the bootstrap installs a context-copying executor **only in Mem0's main module**.
|
|
195
|
+
The global Python executor implementation is unchanged. Application-created worker
|
|
196
|
+
threads must copy context themselves or establish a new authenticated scope.
|
|
197
|
+
|
|
198
|
+
## FastAPI integration
|
|
199
|
+
|
|
200
|
+
Construct pools and install the integration in each worker's lifespan, never
|
|
201
|
+
before forking. Keep the tenant scope inside the endpoint/service function so
|
|
202
|
+
scope-exit errors occur before a response is returned. The authentication
|
|
203
|
+
dependency below is owned by your application and must verify identity and tenant
|
|
204
|
+
membership.
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
import os
|
|
208
|
+
from contextlib import asynccontextmanager
|
|
209
|
+
from fastapi import Depends, FastAPI, Request
|
|
210
|
+
from mem0 import AsyncMemory
|
|
211
|
+
from mem0_postgres_store import Database, database_vector_config, install, tenant_scope
|
|
212
|
+
from your_app.auth import authenticate, AuthenticatedPrincipal
|
|
213
|
+
from your_app.config import model_config
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@asynccontextmanager
|
|
217
|
+
async def lifespan(app: FastAPI):
|
|
218
|
+
database = Database(os.environ["MEM0_DATABASE_URL"], embedding_dimensions=1536)
|
|
219
|
+
install(database)
|
|
220
|
+
memory = AsyncMemory.from_config(
|
|
221
|
+
{
|
|
222
|
+
**model_config,
|
|
223
|
+
"history_db_path": "postgresql://mem0-postgres-store",
|
|
224
|
+
"vector_store": database_vector_config(database),
|
|
225
|
+
}
|
|
226
|
+
)
|
|
227
|
+
app.state.memory = memory
|
|
228
|
+
try:
|
|
229
|
+
yield
|
|
230
|
+
finally:
|
|
231
|
+
memory.close()
|
|
232
|
+
database.close()
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
app = FastAPI(lifespan=lifespan)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@app.get("/memories")
|
|
239
|
+
async def memories(request: Request, principal: AuthenticatedPrincipal = Depends(authenticate)):
|
|
240
|
+
with tenant_scope(principal.tenant_id):
|
|
241
|
+
result = await request.app.state.memory.get_all(filters={"user_id": principal.user_id})
|
|
242
|
+
return result
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
FastAPI is intentionally not a dependency of this library. See also the
|
|
246
|
+
[service-function example](examples/fastapi_integration.py). Do not reuse a tenant
|
|
247
|
+
scope across requests or start detached work inside one. A copied context becomes
|
|
248
|
+
invalid once its owning scope exits. Each background job needs its own verified
|
|
249
|
+
identity and scope.
|
|
250
|
+
|
|
251
|
+
## Tenant security and reset
|
|
252
|
+
|
|
253
|
+
Each history session and vector cursor transaction executes parameterized
|
|
254
|
+
`set_config('app.tenant_id', tenant_id, true)` on the **same connection** as the
|
|
255
|
+
queries. Commit or rollback clears the setting before pooled reuse. Missing
|
|
256
|
+
application context raises `TenantContextError`; direct SQL with no setting sees
|
|
257
|
+
no rows and cannot insert tenant data. A JSON payload's `tenant_id`, `user_id`
|
|
258
|
+
filters, or a known memory UUID cannot override the relational tenant column/RLS.
|
|
259
|
+
|
|
260
|
+
RLS protects tenants from other application requests, including accidentally
|
|
261
|
+
missing SQL filters. It does **not** protect against a compromised application,
|
|
262
|
+
arbitrary SQL execution with the runtime credentials, privileged administrators,
|
|
263
|
+
or untrusted code calling `tenant_scope` with another tenant's identity. PostgreSQL
|
|
264
|
+
custom settings can be set by the runtime role. Authentication is the trust boundary.
|
|
265
|
+
Within a tenant, Mem0 user/agent/run filters remain application-level scoping;
|
|
266
|
+
this package does not provide separate user-level authorization inside that tenant.
|
|
267
|
+
|
|
268
|
+
Reset is explicitly authorized for one tenant:
|
|
269
|
+
|
|
270
|
+
```python
|
|
271
|
+
with tenant_scope(authenticated_tenant_id, allow_reset=True):
|
|
272
|
+
memory.reset() # await memory.reset() for AsyncMemory
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
It deletes that tenant's history, messages, memories, and entity vectors, retains
|
|
276
|
+
the shared schema, and supports SDK reinitialization. There is no runtime global
|
|
277
|
+
reset or DDL. Quiesce that tenant's requests during reset: the SDK performs it as
|
|
278
|
+
multiple transactions, so reset is not atomic across all stores. Other tenants
|
|
279
|
+
remain protected by RLS.
|
|
280
|
+
|
|
281
|
+
## Architecture and compatibility
|
|
282
|
+
|
|
283
|
+
The [design note](docs/design.md) records the source inspection and rejected
|
|
284
|
+
alternatives. A post-construction replacement opens SQLite first; copying the
|
|
285
|
+
Memory constructors into subclasses duplicates substantial upstream logic. The
|
|
286
|
+
explicit bootstrap instead changes three process-local integration points:
|
|
287
|
+
|
|
288
|
+
1. `mem0.memory.main.SQLiteManager` becomes a factory for `PostgresHistoryStore`.
|
|
289
|
+
2. The pgvector provider registry points at `TenantPGVector`, which inherits
|
|
290
|
+
upstream queries and overrides transaction, collection, and reset boundaries.
|
|
291
|
+
3. Mem0's module-local executor binding copies ContextVars for entity search.
|
|
292
|
+
|
|
293
|
+
Package import alone changes none of them. Install before creating any SDK
|
|
294
|
+
instances: existing objects are not retrofitted, and their existence cannot be
|
|
295
|
+
reliably detected. One open Database binding is supported per process. Repeated
|
|
296
|
+
install with the same instance is safe; conflicting patches are rejected. Installing
|
|
297
|
+
a different Database is rejected while the current one is open, and allowed once it
|
|
298
|
+
is closed, so repeated application lifespans in one process (for example several
|
|
299
|
+
FastAPI `TestClient` blocks in a test suite) work. SDK instances created against the
|
|
300
|
+
closed Database keep failing closed; create new `Memory`/`AsyncMemory` instances
|
|
301
|
+
after rebinding. There is no uninstall.
|
|
302
|
+
All ordinary Mem0 pgvector instances in that process use this registration.
|
|
303
|
+
Other vector providers are outside the security guarantee and must not be used.
|
|
304
|
+
|
|
305
|
+
| Component | Supported / verified |
|
|
306
|
+
| --- | --- |
|
|
307
|
+
| Python | CPython 3.12 |
|
|
308
|
+
| Mem0 SDK | Exactly `mem0ai==2.2.1`, audited source fingerprints |
|
|
309
|
+
| PostgreSQL / pgvector | PostgreSQL 17, `pgvector/pgvector:pg17`, real Testcontainers tests |
|
|
310
|
+
| SQLAlchemy / driver | SQLAlchemy 2.0, psycopg 3; SQLAlchemy history pool + psycopg vector pool |
|
|
311
|
+
| SDK operations | Sync/async add, extraction, get, search, get_all, update, delete, history, reset |
|
|
312
|
+
| Isolation | History, recent messages, main vectors, lazy entity vectors |
|
|
313
|
+
| Other Mem0 versions/backends | Unsupported; require separate audit and tests |
|
|
314
|
+
|
|
315
|
+
## Limitations and operational details
|
|
316
|
+
|
|
317
|
+
- **No cross-store atomic commit.** Mem0 commits vectors and history separately.
|
|
318
|
+
A history failure can leave a vector without its audit row. All our database
|
|
319
|
+
operations propagate errors; `tenant_scope` also records errors that Mem0 catches
|
|
320
|
+
and raises `StorageOperationError` on otherwise successful exit. A caught error
|
|
321
|
+
still marks the scope failed, even if an SDK fallback later succeeds. Inspect
|
|
322
|
+
and reconcile partial writes; blind retries can duplicate memories. The tests
|
|
323
|
+
demonstrate this limitation with an actual PostgreSQL constraint failure.
|
|
324
|
+
- **Process-global coupling.** The integration checks the installed version and
|
|
325
|
+
fingerprints of audited source modules before patching. Modified wheels and
|
|
326
|
+
upgrades fail clearly. It does not detect arbitrary in-memory method replacement
|
|
327
|
+
by other code; do not combine it with unrelated Mem0 patches. Existing SDK
|
|
328
|
+
instances, alternative providers, telemetry, and custom external pools are not
|
|
329
|
+
covered. `MEM0_TELEMETRY=false` is mandatory before Mem0 import. Mem0 can still
|
|
330
|
+
create its ordinary local configuration directory; this is not SQLite persistence.
|
|
331
|
+
- **One fixed collection pair/schema.** Multiple applications may share the
|
|
332
|
+
database when they deliberately share the same authenticated tenant namespace,
|
|
333
|
+
embedding model and dimension. Separate logical datasets need separate databases
|
|
334
|
+
or a future explicit namespace migration. There is no arbitrary collection DDL.
|
|
335
|
+
- **Compatibility choices.** Timestamps round-trip as supplied strings/None;
|
|
336
|
+
history orders by created-at text, UTC updated-at to whole seconds, then insertion
|
|
337
|
+
sequence. SDK ISO timestamps are supported; exotic SQLite date expressions
|
|
338
|
+
(for example Julian day numbers or `now`) are not emulated. Equal timestamps
|
|
339
|
+
are deterministic. Deletion markers return bool. Actor/role and message names
|
|
340
|
+
are retained; arbitrary vector metadata remains JSONB. The upstream history
|
|
341
|
+
interface has no arbitrary metadata field. Unknown dictionary fields are ignored
|
|
342
|
+
as in SQLiteManager. Empty calls still require tenant context. PostgreSQL rejects
|
|
343
|
+
NUL text. Reset intentionally deletes tenant rows instead of dropping tables.
|
|
344
|
+
- **Concurrency/lifecycle.** An advisory transaction lock serializes message
|
|
345
|
+
insertion and pruning per tenant/session across processes. At most 10 committed
|
|
346
|
+
messages remain; higher read limits cannot recover evicted context. History
|
|
347
|
+
batches are atomic. `store.close()` is idempotent and invalidates that manager;
|
|
348
|
+
only `Database.close()` owns pool shutdown. Finish all work before closing.
|
|
349
|
+
Reusing closed objects is unsupported. `timeout` bounds connection/pool waits,
|
|
350
|
+
not query execution; configure statement/lock timeouts with your DBA as needed.
|
|
351
|
+
- **Performance.** Vector queries reuse Mem0's pgvector implementation and currently
|
|
352
|
+
use exact similarity scans. No HNSW/DiskANN index is created; the helper disables
|
|
353
|
+
HNSW. Benchmark production workloads and add appropriate indexes through explicit
|
|
354
|
+
migrations. Pool capacity is up to twice `pool_size` per process (two pools).
|
|
355
|
+
- **Migration scope.** Fresh installations are supported. Existing SQLite or
|
|
356
|
+
pgvector collections need an audited export/import assigning verified tenant
|
|
357
|
+
ownership. No automatic backfill, historical-data reconstruction, memory decay,
|
|
358
|
+
temporal reasoning, or consolidation is included.
|
|
359
|
+
|
|
360
|
+
For an SDK upgrade, inspect all constructor/reset/storage/vector/worker call sites,
|
|
361
|
+
update the exact dependency and audited fingerprints only after review, regenerate
|
|
362
|
+
`uv.lock`, run the complete PostgreSQL suite, and publish a new library release.
|
|
363
|
+
Minimal upstream improvements would be an injected history factory used on init
|
|
364
|
+
and reset, a vector transaction context hook, worker context propagation, and
|
|
365
|
+
persistence errors propagated to callers. Atomic vector/history writes require a
|
|
366
|
+
shared unit-of-work contract beyond a history factory.
|
|
367
|
+
|
|
368
|
+
## Tests, lint, types, build
|
|
369
|
+
|
|
370
|
+
Docker must be running. Automated database tests always use Testcontainers and
|
|
371
|
+
never substitute SQLite or mocks for PostgreSQL. Missing Docker is a failure,
|
|
372
|
+
not a silent skip. No OpenAI key or model download is required.
|
|
373
|
+
|
|
374
|
+
```bash
|
|
375
|
+
uv sync --frozen
|
|
376
|
+
uv run pytest
|
|
377
|
+
uv run ruff check .
|
|
378
|
+
uv run ruff format --check .
|
|
379
|
+
uv run mypy
|
|
380
|
+
uv build --no-sources
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
Tests cover contracts, real SDK flows, no SQLite connection, timestamp/metadata
|
|
384
|
+
round trips, retention, reset, concurrent sync/async requests, role rejection,
|
|
385
|
+
all four RLS commands, forged filters/IDs, pooled context cleanup, rollback,
|
|
386
|
+
connection refusal, live database restart/recovery, and surfaced persistence failures. Mypy checks the library
|
|
387
|
+
and migrations; upstream Mem0 is treated as an untyped dependency.
|
|
388
|
+
|
|
389
|
+
## CI and PyPI releases
|
|
390
|
+
|
|
391
|
+
`checks.yml` runs Ruff, formatting, mypy, Testcontainers tests, and wheel/sdist
|
|
392
|
+
build/import checks on pull requests and pushes to `main`/`master`.
|
|
393
|
+
`publish.yml` runs on pushed `v*` tags, calls those same checks, verifies the tag
|
|
394
|
+
matches `pyproject.toml`, downloads the checked distributions, and publishes with
|
|
395
|
+
`uv publish --trusted-publishing always`. The publish job alone receives OIDC
|
|
396
|
+
permission. No long-lived PyPI API token is required.
|
|
397
|
+
|
|
398
|
+
Configure or verify the **PyPI Trusted Publisher** with these exact values:
|
|
399
|
+
|
|
400
|
+
| Setting | Value |
|
|
401
|
+
| --- | --- |
|
|
402
|
+
| PyPI project | `mem0-postgres-store` |
|
|
403
|
+
| GitHub owner | `Mg30` |
|
|
404
|
+
| GitHub repository | `mem0-postgres-store` |
|
|
405
|
+
| Workflow filename | `publish.yml` |
|
|
406
|
+
| GitHub environment | `pypi` |
|
|
407
|
+
|
|
408
|
+
If the PyPI project does not exist yet, use a pending publisher. A GitHub account
|
|
409
|
+
connection alone is not sufficient: this repository/workflow/environment must be
|
|
410
|
+
authorized in PyPI. That account-side configuration cannot be verified through
|
|
411
|
+
GitHub CLI. See [PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/using-a-publisher/)
|
|
412
|
+
and [uv publishing](https://docs.astral.sh/uv/guides/package/).
|
|
413
|
+
|
|
414
|
+
To release after configuring the publisher and reviewing the code:
|
|
415
|
+
|
|
416
|
+
```bash
|
|
417
|
+
# Update project.version, then regenerate and commit uv.lock with the change.
|
|
418
|
+
uv lock
|
|
419
|
+
uv sync --frozen
|
|
420
|
+
uv run pytest
|
|
421
|
+
# After committing and pushing the release version:
|
|
422
|
+
git tag v0.1.0
|
|
423
|
+
git push origin v0.1.0
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
A tag publishes a real release to PyPI; choose a new version for each release.
|
|
427
|
+
The initial repository push does not create a tag or publish a release.
|
|
428
|
+
|
|
429
|
+
## Sources and licensing
|
|
430
|
+
|
|
431
|
+
This package is MIT licensed. It depends on Mem0 (Apache-2.0) and subclasses its
|
|
432
|
+
installed pgvector backend; it does not vendor Mem0 source. Inspected references:
|
|
433
|
+
[Mem0 v2.2.1](https://github.com/mem0ai/mem0/tree/v2.2.1),
|
|
434
|
+
[history storage](https://github.com/mem0ai/mem0/blob/v2.2.1/mem0/memory/storage.py),
|
|
435
|
+
[pgvector](https://github.com/mem0ai/mem0/blob/v2.2.1/mem0/vector_stores/pgvector.py),
|
|
436
|
+
and the [community history patch](https://github.com/nocyphr/mem0-history-db-patch).
|
|
437
|
+
The community patch declares MIT in its README and package metadata. It was used
|
|
438
|
+
only for architectural comparison; no community patch code was copied or installed.
|