memgres 0.2.0__tar.gz → 0.2.1__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.
- {memgres-0.2.0 → memgres-0.2.1}/PKG-INFO +6 -4
- {memgres-0.2.0 → memgres-0.2.1}/README.md +5 -3
- {memgres-0.2.0 → memgres-0.2.1}/memgres/qdrant_backend.py +31 -3
- {memgres-0.2.0 → memgres-0.2.1}/memgres.egg-info/PKG-INFO +6 -4
- {memgres-0.2.0 → memgres-0.2.1}/memgres.egg-info/SOURCES.txt +1 -0
- {memgres-0.2.0 → memgres-0.2.1}/pyproject.toml +1 -1
- memgres-0.2.1/tests/test_qdrant_ca.py +72 -0
- {memgres-0.2.0 → memgres-0.2.1}/tests/test_qdrant_integration.py +10 -0
- {memgres-0.2.0 → memgres-0.2.1}/LICENSE +0 -0
- {memgres-0.2.0 → memgres-0.2.1}/memgres/__init__.py +0 -0
- {memgres-0.2.0 → memgres-0.2.1}/memgres/blame.py +0 -0
- {memgres-0.2.0 → memgres-0.2.1}/memgres/config.py +0 -0
- {memgres-0.2.0 → memgres-0.2.1}/memgres/diffing.py +0 -0
- {memgres-0.2.0 → memgres-0.2.1}/memgres/embeddings.py +0 -0
- {memgres-0.2.0 → memgres-0.2.1}/memgres/identity.py +0 -0
- {memgres-0.2.0 → memgres-0.2.1}/memgres/mcp_server.py +0 -0
- {memgres-0.2.0 → memgres-0.2.1}/memgres/migrations/0001_core.sql +0 -0
- {memgres-0.2.0 → memgres-0.2.1}/memgres/migrations/0002_identity.sql +0 -0
- {memgres-0.2.0 → memgres-0.2.1}/memgres/schema.py +0 -0
- {memgres-0.2.0 → memgres-0.2.1}/memgres/search.py +0 -0
- {memgres-0.2.0 → memgres-0.2.1}/memgres/server.py +0 -0
- {memgres-0.2.0 → memgres-0.2.1}/memgres/store.py +0 -0
- {memgres-0.2.0 → memgres-0.2.1}/memgres.egg-info/dependency_links.txt +0 -0
- {memgres-0.2.0 → memgres-0.2.1}/memgres.egg-info/entry_points.txt +0 -0
- {memgres-0.2.0 → memgres-0.2.1}/memgres.egg-info/requires.txt +0 -0
- {memgres-0.2.0 → memgres-0.2.1}/memgres.egg-info/top_level.txt +0 -0
- {memgres-0.2.0 → memgres-0.2.1}/setup.cfg +0 -0
- {memgres-0.2.0 → memgres-0.2.1}/tests/test_blame_integration.py +0 -0
- {memgres-0.2.0 → memgres-0.2.1}/tests/test_config.py +0 -0
- {memgres-0.2.0 → memgres-0.2.1}/tests/test_diffing.py +0 -0
- {memgres-0.2.0 → memgres-0.2.1}/tests/test_embeddings.py +0 -0
- {memgres-0.2.0 → memgres-0.2.1}/tests/test_identity_integration.py +0 -0
- {memgres-0.2.0 → memgres-0.2.1}/tests/test_search_integration.py +0 -0
- {memgres-0.2.0 → memgres-0.2.1}/tests/test_security_integration.py +0 -0
- {memgres-0.2.0 → memgres-0.2.1}/tests/test_server_integration.py +0 -0
- {memgres-0.2.0 → memgres-0.2.1}/tests/test_store_integration.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memgres
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Drop-in memory for AI agents: one Postgres, lexical + semantic recall, diff-versioned history, GDPR-erasable.
|
|
5
5
|
Author: mozgsml
|
|
6
6
|
License-Expression: MIT
|
|
@@ -282,8 +282,10 @@ Either way the model gets tools `memory_write`, `memory_recall`, `memory_get`,
|
|
|
282
282
|
X"* / *"what do you know about Y?"* and it calls them. (For semantic recall add the
|
|
283
283
|
embedding env vars — see [docs/BACKENDS.md](docs/BACKENDS.md).)
|
|
284
284
|
|
|
285
|
-
|
|
286
|
-
|
|
285
|
+
### Isolation — pin the identity in the client config
|
|
286
|
+
|
|
287
|
+
The agent **never handles the token** (so the model spends nothing echoing a
|
|
288
|
+
secret and can't switch user):
|
|
287
289
|
|
|
288
290
|
- **stdio**: set `MEMGRES_KEY_MODE=open` + `MEMGRES_TOKEN=<mgk_…>` in the client's
|
|
289
291
|
`env` block (above).
|
|
@@ -305,7 +307,7 @@ argument for the model to supply — force it either way with
|
|
|
305
307
|
library after the model produces text (see the examples above). Use this when you
|
|
306
308
|
control the agent loop and decide when to write/recall.
|
|
307
309
|
|
|
308
|
-
##
|
|
310
|
+
## Multi-tenant, tokens & isolation
|
|
309
311
|
|
|
310
312
|
There is **no token for single-user / local use** — leave everything default
|
|
311
313
|
(`MEMGRES_KEY_MODE=single`) and it just works. Two token concepts exist, unrelated:
|
|
@@ -247,8 +247,10 @@ Either way the model gets tools `memory_write`, `memory_recall`, `memory_get`,
|
|
|
247
247
|
X"* / *"what do you know about Y?"* and it calls them. (For semantic recall add the
|
|
248
248
|
embedding env vars — see [docs/BACKENDS.md](docs/BACKENDS.md).)
|
|
249
249
|
|
|
250
|
-
|
|
251
|
-
|
|
250
|
+
### Isolation — pin the identity in the client config
|
|
251
|
+
|
|
252
|
+
The agent **never handles the token** (so the model spends nothing echoing a
|
|
253
|
+
secret and can't switch user):
|
|
252
254
|
|
|
253
255
|
- **stdio**: set `MEMGRES_KEY_MODE=open` + `MEMGRES_TOKEN=<mgk_…>` in the client's
|
|
254
256
|
`env` block (above).
|
|
@@ -270,7 +272,7 @@ argument for the model to supply — force it either way with
|
|
|
270
272
|
library after the model produces text (see the examples above). Use this when you
|
|
271
273
|
control the agent loop and decide when to write/recall.
|
|
272
274
|
|
|
273
|
-
##
|
|
275
|
+
## Multi-tenant, tokens & isolation
|
|
274
276
|
|
|
275
277
|
There is **no token for single-user / local use** — leave everything default
|
|
276
278
|
(`MEMGRES_KEY_MODE=single`) and it just works. Two token concepts exist, unrelated:
|
|
@@ -13,7 +13,13 @@ So tag/tree/TTL changes never need to touch Qdrant — only a body change re-emb
|
|
|
13
13
|
(upsert) and `forget` deletes the point. Config for the connection:
|
|
14
14
|
|
|
15
15
|
QDRANT_URL (default http://localhost:6333) · QDRANT_API_KEY ·
|
|
16
|
-
MEMGRES_QDRANT_COLLECTION (default "memgres")
|
|
16
|
+
MEMGRES_QDRANT_COLLECTION (default "memgres") ·
|
|
17
|
+
MEMGRES_QDRANT_CA (path to a CA/self-signed cert to trust for an https URL)
|
|
18
|
+
|
|
19
|
+
MEMGRES_QDRANT_CA is only needed when Qdrant serves TLS with a certificate the
|
|
20
|
+
system trust store doesn't already know (a self-signed or private-CA deployment):
|
|
21
|
+
point it at the PEM to verify against. Leave it unset for plain http or a
|
|
22
|
+
publicly-trusted cert.
|
|
17
23
|
|
|
18
24
|
Needs the `[qdrant]` extra (qdrant-client).
|
|
19
25
|
"""
|
|
@@ -26,14 +32,19 @@ from typing import List, Optional, Sequence, Tuple
|
|
|
26
32
|
|
|
27
33
|
class QdrantIndex:
|
|
28
34
|
def __init__(self, dim: int, url: Optional[str] = None,
|
|
29
|
-
api_key: Optional[str] = None, collection: Optional[str] = None
|
|
35
|
+
api_key: Optional[str] = None, collection: Optional[str] = None,
|
|
36
|
+
ca_cert: Optional[str] = None):
|
|
30
37
|
from qdrant_client import QdrantClient
|
|
31
38
|
|
|
32
39
|
self.dim = dim
|
|
33
40
|
self.collection = collection or os.environ.get("MEMGRES_QDRANT_COLLECTION", "memgres")
|
|
34
41
|
url = url or os.environ.get("QDRANT_URL", "http://localhost:6333")
|
|
35
42
|
api_key = api_key or os.environ.get("QDRANT_API_KEY") or None
|
|
36
|
-
|
|
43
|
+
ca_cert = ca_cert or os.environ.get("MEMGRES_QDRANT_CA") or None
|
|
44
|
+
# A self-signed / private-CA https endpoint isn't in the system trust store;
|
|
45
|
+
# `verify=<pem>` makes the underlying httpx client trust exactly that cert.
|
|
46
|
+
extra = {"verify": ca_cert} if ca_cert else {}
|
|
47
|
+
self.client = QdrantClient(url=url, api_key=api_key, **extra)
|
|
37
48
|
self._ensure()
|
|
38
49
|
|
|
39
50
|
def _ensure(self) -> None:
|
|
@@ -44,6 +55,7 @@ class QdrantIndex:
|
|
|
44
55
|
self.collection,
|
|
45
56
|
vectors_config=VectorParams(size=self.dim, distance=Distance.COSINE),
|
|
46
57
|
)
|
|
58
|
+
self._ensure_namespace_index()
|
|
47
59
|
return
|
|
48
60
|
info = self.client.get_collection(self.collection)
|
|
49
61
|
size = info.config.params.vectors.size
|
|
@@ -53,6 +65,22 @@ class QdrantIndex:
|
|
|
53
65
|
f"Qdrant collection '{self.collection}' has dim {size}, model emits "
|
|
54
66
|
f"{self.dim} — re-embed into a fresh collection, don't mix models."
|
|
55
67
|
)
|
|
68
|
+
self._ensure_namespace_index()
|
|
69
|
+
|
|
70
|
+
def _ensure_namespace_index(self) -> None:
|
|
71
|
+
"""Keyword payload index on `namespace` so tenant-filtered ANN search
|
|
72
|
+
stays fast as the collection grows (isolation is enforced by the query
|
|
73
|
+
filter regardless; this is purely the speed of that filter). Idempotent:
|
|
74
|
+
check the existing payload schema rather than catch a re-create error."""
|
|
75
|
+
from qdrant_client.models import PayloadSchemaType
|
|
76
|
+
|
|
77
|
+
info = self.client.get_collection(self.collection)
|
|
78
|
+
if "namespace" in (info.payload_schema or {}):
|
|
79
|
+
return
|
|
80
|
+
self.client.create_payload_index(
|
|
81
|
+
self.collection, field_name="namespace",
|
|
82
|
+
field_schema=PayloadSchemaType.KEYWORD,
|
|
83
|
+
)
|
|
56
84
|
|
|
57
85
|
def upsert(self, id: str, vector: Sequence[float], namespace: str) -> None:
|
|
58
86
|
from qdrant_client.models import PointStruct
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memgres
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Drop-in memory for AI agents: one Postgres, lexical + semantic recall, diff-versioned history, GDPR-erasable.
|
|
5
5
|
Author: mozgsml
|
|
6
6
|
License-Expression: MIT
|
|
@@ -282,8 +282,10 @@ Either way the model gets tools `memory_write`, `memory_recall`, `memory_get`,
|
|
|
282
282
|
X"* / *"what do you know about Y?"* and it calls them. (For semantic recall add the
|
|
283
283
|
embedding env vars — see [docs/BACKENDS.md](docs/BACKENDS.md).)
|
|
284
284
|
|
|
285
|
-
|
|
286
|
-
|
|
285
|
+
### Isolation — pin the identity in the client config
|
|
286
|
+
|
|
287
|
+
The agent **never handles the token** (so the model spends nothing echoing a
|
|
288
|
+
secret and can't switch user):
|
|
287
289
|
|
|
288
290
|
- **stdio**: set `MEMGRES_KEY_MODE=open` + `MEMGRES_TOKEN=<mgk_…>` in the client's
|
|
289
291
|
`env` block (above).
|
|
@@ -305,7 +307,7 @@ argument for the model to supply — force it either way with
|
|
|
305
307
|
library after the model produces text (see the examples above). Use this when you
|
|
306
308
|
control the agent loop and decide when to write/recall.
|
|
307
309
|
|
|
308
|
-
##
|
|
310
|
+
## Multi-tenant, tokens & isolation
|
|
309
311
|
|
|
310
312
|
There is **no token for single-user / local use** — leave everything default
|
|
311
313
|
(`MEMGRES_KEY_MODE=single`) and it just works. Two token concepts exist, unrelated:
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Unit test: MEMGRES_QDRANT_CA is forwarded to the Qdrant client as `verify=`.
|
|
2
|
+
|
|
3
|
+
No live Qdrant needed — a fake client records the kwargs it was built with and
|
|
4
|
+
stubs the calls `_ensure()` makes, so this runs anywhere (unlike the integration
|
|
5
|
+
tests that need a real server). Guards the self-signed / private-CA TLS path.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
12
|
+
|
|
13
|
+
import pytest # noqa: E402
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _FakeCollectionInfo:
|
|
17
|
+
def __init__(self, dim):
|
|
18
|
+
class _V: # info.config.params.vectors.size
|
|
19
|
+
size = dim
|
|
20
|
+
class _P:
|
|
21
|
+
vectors = _V()
|
|
22
|
+
class _C:
|
|
23
|
+
params = _P()
|
|
24
|
+
self.config = _C()
|
|
25
|
+
self.payload_schema = {"namespace": object()} # pretend the index exists
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _FakeClient:
|
|
29
|
+
last_kwargs = None
|
|
30
|
+
|
|
31
|
+
def __init__(self, **kwargs):
|
|
32
|
+
_FakeClient.last_kwargs = kwargs
|
|
33
|
+
|
|
34
|
+
def collection_exists(self, _c):
|
|
35
|
+
return True
|
|
36
|
+
|
|
37
|
+
def get_collection(self, _c):
|
|
38
|
+
return _FakeCollectionInfo(dim=8)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.fixture
|
|
42
|
+
def fake_qdrant(monkeypatch):
|
|
43
|
+
import qdrant_client
|
|
44
|
+
monkeypatch.setattr(qdrant_client, "QdrantClient", _FakeClient)
|
|
45
|
+
_FakeClient.last_kwargs = None
|
|
46
|
+
yield
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_ca_cert_forwarded_as_verify(fake_qdrant, monkeypatch):
|
|
50
|
+
monkeypatch.setenv("MEMGRES_QDRANT_CA", "/etc/ssl/my-ca.pem")
|
|
51
|
+
from memgres.qdrant_backend import QdrantIndex
|
|
52
|
+
QdrantIndex(dim=8, url="https://qdrant.example:6333")
|
|
53
|
+
assert _FakeClient.last_kwargs.get("verify") == "/etc/ssl/my-ca.pem"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_no_ca_means_no_verify_kwarg(fake_qdrant, monkeypatch):
|
|
57
|
+
monkeypatch.delenv("MEMGRES_QDRANT_CA", raising=False)
|
|
58
|
+
from memgres.qdrant_backend import QdrantIndex
|
|
59
|
+
QdrantIndex(dim=8, url="http://qdrant.example:6333")
|
|
60
|
+
# default trust store path: don't pin `verify` at all
|
|
61
|
+
assert "verify" not in _FakeClient.last_kwargs
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_explicit_ca_arg_beats_env(fake_qdrant, monkeypatch):
|
|
65
|
+
monkeypatch.setenv("MEMGRES_QDRANT_CA", "/env/ca.pem")
|
|
66
|
+
from memgres.qdrant_backend import QdrantIndex
|
|
67
|
+
QdrantIndex(dim=8, url="https://qdrant.example:6333", ca_cert="/arg/ca.pem")
|
|
68
|
+
assert _FakeClient.last_kwargs.get("verify") == "/arg/ca.pem"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if __name__ == "__main__":
|
|
72
|
+
sys.exit(pytest.main([__file__, "-v"]))
|
|
@@ -96,6 +96,16 @@ def test_no_pgvector_column_when_qdrant(store):
|
|
|
96
96
|
assert "embedding" not in cols
|
|
97
97
|
|
|
98
98
|
|
|
99
|
+
def test_namespace_payload_index_created(store):
|
|
100
|
+
# tenant-scoped ANN search filters on `namespace`; a keyword payload index
|
|
101
|
+
# keeps that filter fast as the collection grows (isolation holds either way)
|
|
102
|
+
from qdrant_client import QdrantClient
|
|
103
|
+
qc = QdrantClient(url=QURL)
|
|
104
|
+
schema = qc.get_collection(COLL).payload_schema or {}
|
|
105
|
+
assert "namespace" in schema
|
|
106
|
+
assert str(schema["namespace"].data_type).lower().endswith("keyword")
|
|
107
|
+
|
|
108
|
+
|
|
99
109
|
def test_semantic_recall_via_qdrant(store):
|
|
100
110
|
store.write(body="I love apple pie\n", path="food.apple", tags=["fruit"])
|
|
101
111
|
store.write(body="banana bread recipe\n", path="food.banana", tags=["fruit"])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|