Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit a5cf59d

Browse files
Wire EventMemory as backend for LongTermMemory (#1395)
* Claude initial wiring Signed-off-by: Edwin Yu <[email protected]> Example configs Signed-off-by: Edwin Yu <[email protected]> * Add tests Signed-off-by: Edwin Yu <[email protected]> * Add overfetch Signed-off-by: Edwin Yu <[email protected]> * Config updates Signed-off-by: Edwin Yu <[email protected]> * Fixes Signed-off-by: Edwin Yu <[email protected]> * Fix CI Signed-off-by: Edwin Yu <[email protected]> * Make backend configurable from client Signed-off-by: Edwin Yu <[email protected]> * Fix impersonation security problem Signed-off-by: Edwin Yu <[email protected]> * Address AI review, fix latent bugs Signed-off-by: Edwin Yu <[email protected]> * Get episodes in batch Signed-off-by: Edwin Yu <[email protected]> * Address review Signed-off-by: Edwin Yu <[email protected]> --------- Signed-off-by: Edwin Yu <[email protected]> Co-authored-by: Shu Wang <[email protected]>
1 parent 39262c6 commit a5cf59d

46 files changed

Lines changed: 3528 additions & 882 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/installation-test.yml

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,11 @@ jobs:
7373
shell: bash
7474
run: |
7575
set -eo pipefail
76-
whl_name=$(ls *server*.whl)
77-
python -m pip install --find-links . "$whl_name"
76+
# Install every locally built memmachine_* wheel by direct path so
77+
# pip never resolves an internal package to an older stable release
78+
# on PyPI (dev versions are excluded by default).
79+
# Third-party deps still resolve normally via PyPI.
80+
python -m pip install --find-links . memmachine_*.whl
7881
7982
#
8083
# ─────────────────────────────────────
@@ -105,8 +108,8 @@ jobs:
105108
run: |
106109
set -eo pipefail
107110
export PYTHONUTF8=1
108-
whl_name=$(ls *server*.whl)
109-
python -m pip install --find-links . "$whl_name"
111+
# See Linux step for rationale.
112+
python -m pip install --find-links . memmachine_*.whl
110113
111114
#
112115
# ─────────────────────────────────────
@@ -124,8 +127,8 @@ jobs:
124127
shell: bash
125128
run: |
126129
set -eo pipefail
127-
whl_name=$(ls *server*.whl)
128-
python -m pip install --find-links . "$whl_name"
130+
# See Linux step for rationale.
131+
python -m pip install --find-links . memmachine_*.whl
129132
130133
#
131134
# ─────────────────────────────────────
@@ -174,8 +177,15 @@ jobs:
174177
- name: Run memmachine-configure
175178
shell: bash
176179
run: |
180+
# Inputs (in order of wizard prompts):
181+
# y → install Neo4j locally
182+
# (blank) → accept default vector store (sqlite_vector_store, since
183+
# qdrant-client extra is not installed in this CI job)
184+
# Ollama, qwen3:0.6b, (blank api key), base url, embedder model,
185+
# embedder dims, host, port → standard LM / embedder / server config
177186
memmachine-configure << EOF
178187
y
188+
179189
Ollama
180190
qwen3:0.6b
181191

.github/workflows/test-python-client-package.yml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,16 @@ jobs:
5959
shell: bash
6060
run: |
6161
set -eo pipefail
62-
whl_name=$(python -c "import pathlib; d=sorted(pathlib.Path('dist').glob('memmachine_client-*.whl')); print(d[0] if d else '')")
63-
if [ -z "$whl_name" ]; then
64-
echo "No client wheel found" >&2
62+
# Install every locally built memmachine_* wheel by direct path so
63+
# pip never falls back to older stable releases on PyPI for our
64+
# internal packages (dev versions are excluded by default).
65+
shopt -s nullglob
66+
local_whls=(dist/memmachine_*.whl)
67+
if [ ${#local_whls[@]} -eq 0 ]; then
68+
echo "No local memmachine_* wheels found in dist/" >&2
6569
exit 1
6670
fi
67-
python -m pip install --find-links dist/ "$whl_name"
71+
python -m pip install --find-links dist/ "${local_whls[@]}"
6872
6973
- name: Test Python client package imports
7074
shell: bash

.github/workflows/test-server-package.yml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,16 @@ jobs:
5252
shell: bash
5353
run: |
5454
set -eo pipefail
55-
whl_name=$(ls dist/memmachine_server-*.whl | head -1)
56-
pip install --find-links dist/ "$whl_name"
55+
# Install every locally built memmachine_* wheel by direct path so
56+
# pip never falls back to older stable releases on PyPI for our
57+
# internal packages (dev versions are excluded by default).
58+
shopt -s nullglob
59+
local_whls=(dist/memmachine_*.whl)
60+
if [ ${#local_whls[@]} -eq 0 ]; then
61+
echo "No local memmachine_* wheels found in dist/" >&2
62+
exit 1
63+
fi
64+
pip install --find-links dist/ "${local_whls[@]}"
5765
5866
- name: Test server package imports
5967
shell: bash

packages/client/client_tests/test_client.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,43 @@ def test_create_project_failure(self):
206206
project_id="test_project",
207207
)
208208

209+
def test_create_project_event_backend_fields_reach_wire(self):
210+
"""Event-backend fields must reach the wire — without this the SDK
211+
has no supported path to event-backed projects."""
212+
client = MemMachineClient(base_url="http://localhost:8080")
213+
mock_response = Mock()
214+
mock_response.status_code = 200
215+
mock_response.raise_for_status = Mock()
216+
mock_response.json.return_value = {
217+
"org_id": "test_org",
218+
"project_id": "test_project",
219+
"description": "",
220+
"config": {
221+
"backend": "event",
222+
"embedder": "default",
223+
"reranker": "default",
224+
"vector_store": "qdrant_vs",
225+
"segment_store": "sqlite_db",
226+
"properties_schema": {"customer_tier": "str"},
227+
},
228+
}
229+
with patch.object(
230+
client._session, "post", return_value=mock_response
231+
) as mock_post:
232+
client.create_project(
233+
org_id="test_org",
234+
project_id="test_project",
235+
backend="event",
236+
vector_store="qdrant_vs",
237+
segment_store="sqlite_db",
238+
properties_schema={"customer_tier": "str"},
239+
)
240+
cfg = mock_post.call_args[1]["json"]["config"]
241+
assert cfg["backend"] == "event"
242+
assert cfg["vector_store"] == "qdrant_vs"
243+
assert cfg["segment_store"] == "sqlite_db"
244+
assert cfg["properties_schema"] == {"customer_tier": "str"}
245+
209246
@patch("requests.Session.get")
210247
def test_health_check_success(self, mock_get):
211248
"""Test successful health check."""

packages/client/client_tests/test_config.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,30 @@ def test_update_long_term_memory_config_closed_client(self, config, mock_client)
368368
with pytest.raises(RuntimeError, match="client has been closed"):
369369
config.update_long_term_memory_config(embedder="x")
370370

371+
def test_update_long_term_memory_config_event_backend(self, config, mock_client):
372+
"""Event-backend fields (backend / vector_store / segment_store /
373+
properties_schema) must reach the wire — without these the SDK has no
374+
supported path to event-backed projects."""
375+
mock_client.request.return_value = _mock_response(
376+
{"success": True, "message": "Long-term memory configuration updated"}
377+
)
378+
result = config.update_long_term_memory_config(
379+
backend="event",
380+
embedder="new-embedder",
381+
vector_store="qdrant_vs",
382+
segment_store="sqlite_db",
383+
properties_schema={"customer_tier": "str"},
384+
)
385+
assert isinstance(result, UpdateMemoryConfigResponse)
386+
body = mock_client.request.call_args[1]["json"]
387+
assert body["backend"] == "event"
388+
assert body["vector_store"] == "qdrant_vs"
389+
assert body["segment_store"] == "sqlite_db"
390+
assert body["properties_schema"] == {"customer_tier": "str"}
391+
assert body["embedder"] == "new-embedder"
392+
# Untouched declarative-only field must NOT leak into the payload.
393+
assert "vector_graph_store" not in body
394+
371395
def test_update_short_term_memory_config(self, config, mock_client):
372396
mock_client.request.return_value = _mock_response(
373397
{"success": True, "message": "Short-term memory configuration updated"}
@@ -533,9 +557,13 @@ def test_update_episodic_memory_config_with_nested_specs(self, config, mock_clie
533557
{"success": True, "message": "Episodic memory configuration updated"}
534558
)
535559
ltm_spec = UpdateLongTermMemorySpec(
560+
backend=None,
536561
embedder="new-embedder",
537562
reranker="new-reranker",
538563
vector_graph_store="new-store",
564+
vector_store=None,
565+
segment_store=None,
566+
properties_schema=None,
539567
)
540568
stm_spec = UpdateShortTermMemorySpec(
541569
llm_model="new-llm",

packages/client/src/memmachine_client/client.py

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import logging
66
from collections.abc import Mapping, Sequence
77
from types import TracebackType
8-
from typing import TYPE_CHECKING, Any, NoReturn, TypedDict
8+
from typing import TYPE_CHECKING, Any, Literal, NoReturn, TypedDict
99

1010
import requests
1111
from memmachine_common.api.spec import (
@@ -186,6 +186,11 @@ def create_project(
186186
description: str = "",
187187
embedder: str = "",
188188
reranker: str = "",
189+
backend: Literal["declarative", "event"] | None = None,
190+
vector_graph_store: str = "",
191+
vector_store: str = "",
192+
segment_store: str = "",
193+
properties_schema: dict[str, str] | None = None,
189194
timeout: int | None = None,
190195
) -> Project:
191196
"""
@@ -199,6 +204,19 @@ def create_project(
199204
Use "" to let server use its configured defaults, or specify a model name like "default".
200205
reranker: Reranker model name to use (default: "").
201206
Use "" to let server use its configured defaults, or specify a model name like "default".
207+
backend: Long-term-memory backend selector. ``None`` (default)
208+
lets the server pick its configured default. Set to ``"event"``
209+
for the VectorStore + SegmentStore event backend or
210+
``"declarative"`` for the legacy VectorGraphStore backend.
211+
vector_graph_store: VectorGraphStore resource id (declarative
212+
backend only). Use ``""`` to let the server pick its default.
213+
vector_store: VectorStore resource id (event backend only). Use
214+
``""`` to let the server pick its default.
215+
segment_store: SQL engine resource id backing the segment store
216+
(event backend only). Use ``""`` for the server default.
217+
properties_schema: User-defined filterable property names mapped
218+
to type strings ("bool", "int", "float", "str", "datetime").
219+
Event backend only. Defaults to no user-defined properties.
202220
timeout: Request timeout in seconds (uses client default if not provided)
203221
204222
Returns:
@@ -218,7 +236,15 @@ def create_project(
218236
org_id=org_id,
219237
project_id=project_id,
220238
description=description,
221-
config=ProjectConfig(embedder=embedder, reranker=reranker),
239+
config=ProjectConfig(
240+
backend=backend,
241+
embedder=embedder,
242+
reranker=reranker,
243+
vector_graph_store=vector_graph_store,
244+
vector_store=vector_store,
245+
segment_store=segment_store,
246+
properties_schema=properties_schema or {},
247+
),
222248
)
223249
data = spec.model_dump(exclude_none=True)
224250

@@ -337,6 +363,11 @@ def _create_project_with_retry(
337363
description: str,
338364
embedder: str,
339365
reranker: str,
366+
backend: Literal["declarative", "event"] | None,
367+
vector_graph_store: str,
368+
vector_store: str,
369+
segment_store: str,
370+
properties_schema: dict[str, str] | None,
340371
timeout: int | None,
341372
) -> Project:
342373
"""Create project, handling concurrent creation (409) by fetching existing."""
@@ -347,6 +378,11 @@ def _create_project_with_retry(
347378
description=description,
348379
embedder=embedder,
349380
reranker=reranker,
381+
backend=backend,
382+
vector_graph_store=vector_graph_store,
383+
vector_store=vector_store,
384+
segment_store=segment_store,
385+
properties_schema=properties_schema,
350386
timeout=timeout,
351387
)
352388
except requests.HTTPError as create_error:
@@ -371,6 +407,11 @@ def get_or_create_project(
371407
description: str = "",
372408
embedder: str = "",
373409
reranker: str = "",
410+
backend: Literal["declarative", "event"] | None = None,
411+
vector_graph_store: str = "",
412+
vector_store: str = "",
413+
segment_store: str = "",
414+
properties_schema: dict[str, str] | None = None,
374415
timeout: int | None = None,
375416
) -> Project:
376417
"""
@@ -391,6 +432,18 @@ def get_or_create_project(
391432
reranker: Reranker model name to use (default: "").
392433
Only used if project needs to be created.
393434
Use "" to let server use its configured defaults, or specify a model name like "default".
435+
backend: Long-term-memory backend selector for the project being
436+
created. Ignored if the project already exists. See
437+
:py:meth:`create_project` for accepted values.
438+
vector_graph_store: VectorGraphStore resource id (declarative
439+
backend only). Only used if project needs to be created.
440+
vector_store: VectorStore resource id (event backend only).
441+
Only used if project needs to be created.
442+
segment_store: SQL engine resource id for the segment store
443+
(event backend only). Only used if project needs to be created.
444+
properties_schema: User-defined filterable property names mapped
445+
to type strings (event backend only). Only used if project
446+
needs to be created.
394447
timeout: Request timeout in seconds (uses client default if not provided)
395448
396449
Returns:
@@ -416,7 +469,17 @@ def get_or_create_project(
416469
# If project doesn't exist (404), create it
417470
if e.response.status_code == 404:
418471
return self._create_project_with_retry(
419-
org_id, project_id, description, embedder, reranker, timeout
472+
org_id,
473+
project_id,
474+
description,
475+
embedder,
476+
reranker,
477+
backend,
478+
vector_graph_store,
479+
vector_store,
480+
segment_store,
481+
properties_schema,
482+
timeout,
420483
)
421484
# Re-raise other HTTP errors
422485
raise

packages/client/src/memmachine_client/config.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,16 +314,33 @@ def update_long_term_memory_config(
314314
embedder: str | None = None,
315315
reranker: str | None = None,
316316
vector_graph_store: str | None = None,
317+
backend: Literal["declarative", "event"] | None = None,
318+
vector_store: str | None = None,
319+
segment_store: str | None = None,
320+
properties_schema: dict[str, str] | None = None,
317321
enabled: bool | None = None,
318322
timeout: int | None = None,
319323
) -> UpdateMemoryConfigResponse:
320324
"""
321325
Update long-term memory configuration.
322326
327+
All fields are partial-update: pass ``None`` (the default) to leave a
328+
field unchanged. Pass an empty string ``""`` for resource-id fields to
329+
clear them and fall back to server defaults.
330+
323331
Args:
324332
embedder: Name of the embedder to use for long-term memory
325333
reranker: Name of the reranker to use for long-term memory
326334
vector_graph_store: Name of the vector graph store to use
335+
(declarative backend only)
336+
backend: Switch the long-term-memory backend. ``None`` keeps the
337+
existing backend; ``"declarative"`` or ``"event"`` switches.
338+
vector_store: VectorStore resource id (event backend only)
339+
segment_store: SQL engine resource id backing the segment store
340+
(event backend only)
341+
properties_schema: User-defined filterable property names mapped
342+
to type strings ("bool", "int", "float", "str", "datetime").
343+
Event backend only.
327344
enabled: Whether long-term memory is enabled
328345
timeout: Request timeout in seconds (uses client default if not provided)
329346
@@ -337,9 +354,13 @@ def update_long_term_memory_config(
337354
"""
338355
self._check_closed()
339356
spec = UpdateLongTermMemorySpec(
357+
backend=backend,
340358
embedder=embedder,
341359
reranker=reranker,
342360
vector_graph_store=vector_graph_store,
361+
vector_store=vector_store,
362+
segment_store=segment_store,
363+
properties_schema=properties_schema,
343364
)
344365
payload = spec.model_dump(exclude_none=True)
345366
if enabled is not None:

packages/client/src/memmachine_client/project.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,17 @@ def __init__(
9191
self.project_id = project_id
9292
self.description = description
9393
self.config = (
94-
config if config is not None else ProjectConfig(embedder="", reranker="")
94+
config
95+
if config is not None
96+
else ProjectConfig(
97+
backend=None,
98+
embedder="",
99+
reranker="",
100+
vector_graph_store="",
101+
vector_store="",
102+
segment_store="",
103+
properties_schema={},
104+
)
95105
)
96106

97107
def memory(

0 commit comments

Comments
 (0)