From ebc27bce880e84900d22c9af80d4fe3e0d6c0881 Mon Sep 17 00:00:00 2001 From: Edward Izgorodin Date: Tue, 6 Oct 2026 13:30:02 +0100 Subject: [PATCH] feat(read): diversity on read(), sync and async (core#656) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sent only when above 0, so a caller that does not use it sends the same body as before. Co-Authored-By: Σ --- CHANGELOG.md | 9 ++++++++- mnemoverse/_async_client.py | 9 +++++++++ mnemoverse/client.py | 5 ++++- tests/test_client.py | 29 +++++++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ad1b7b..081cf5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,14 @@ keyword argument is a MINOR, even pre-1.0. ## [Unreleased] -_Nothing yet._ +### Added + +- **`diversity` on `read()`** (0 to 1, default 0), sync and async, passed to + core's `POST /memory/read` (core#656). Above 0, a match that is a near-copy + of one already picked gives its slot to the next distinct memory (maximal + marginal relevance); it changes which memories come back, not their scores, + and applies only when there are more matches than `top_k` and `top_k` is + below 200. At 0 the request body is unchanged. ## [0.4.0] — 2026-10-02 diff --git a/mnemoverse/_async_client.py b/mnemoverse/_async_client.py index e067fe9..a9ff5f9 100644 --- a/mnemoverse/_async_client.py +++ b/mnemoverse/_async_client.py @@ -239,6 +239,7 @@ async def read( exclude_author: str | None = None, include_history: bool = False, fingerprint: str | None = None, + diversity: float = 0.0, ) -> ReadResponse: """Query memory with semantic search + Hebbian expansion. @@ -250,6 +251,12 @@ async def read( ``fingerprint`` asks for an exact episodic match on a fingerprint given at write time (``episodic_hit`` on the response says whether it hit). + ``diversity`` (0 to 1, default 0) keeps near-copies from taking several + slots: above 0, a match too similar to one already picked gives its + slot to the next distinct memory (maximal marginal relevance). It + changes which memories come back, not their scores, and applies only + when there are more matches than ``top_k`` and ``top_k`` is below 200. + ``since`` / ``until`` bound the result by creation time, inclusive at both ends; naive datetimes are read as UTC. ``order_by="recency"`` re-sorts the matched set newest-first without changing which entries @@ -282,6 +289,8 @@ async def read( body["include_history"] = True if fingerprint is not None: body["fingerprint"] = fingerprint + if diversity: + body["diversity"] = diversity data = await self._request("POST", "/api/v1/memory/read", json=body) return ReadResponse.model_validate(data) diff --git a/mnemoverse/client.py b/mnemoverse/client.py index 775d6de..08d3ce1 100644 --- a/mnemoverse/client.py +++ b/mnemoverse/client.py @@ -387,12 +387,14 @@ def read( exclude_author: str | None = None, include_history: bool = False, fingerprint: str | None = None, + diversity: float = 0.0, ) -> ReadResponse: """Query memory with semantic search + Hebbian expansion. ``include_history=True`` returns superseded revisions alongside live ones (each carries ``superseded_by``); ``fingerprint`` asks for an exact - episodic match on a fingerprint given at write time. + episodic match on a fingerprint given at write time. ``diversity`` (0 to 1) + keeps near-copies from taking several slots. ``since`` / ``until`` bound the result by creation time, inclusive at both ends. ``order_by="recency"`` re-sorts the matched set newest-first @@ -412,6 +414,7 @@ def read( since=since, until=until, order_by=order_by, exclude_author=exclude_author, include_history=include_history, fingerprint=fingerprint, + diversity=diversity, ) ) diff --git a/tests/test_client.py b/tests/test_client.py index e7d77b9..4729549 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -944,6 +944,35 @@ async def test_enriched_validation_summary_is_not_duplicated( } +@pytest.mark.parametrize("use_async", [True, False]) +async def test_read_sends_diversity_when_above_zero(use_async: bool, core: MockCore) -> None: + """core#656 accepts ``diversity`` on POST /memory/read; both clients must + put it on the wire when the caller asks for it.""" + core.respond(path="/api/v1/memory/read", json=_READ_EMPTY) + + if use_async: + async_client = AsyncMnemoClient(base_url=core.url, api_key="mk_test", max_retries=0) + try: + await async_client.read("x", diversity=0.3) + finally: + await async_client.close() + else: + with MnemoClient(base_url=core.url, api_key="mk_test", max_retries=0) as sync_client: + sync_client.read("x", diversity=0.3) + + assert core.requests[-1].json()["diversity"] == 0.3 + + +async def test_read_omits_diversity_at_zero(client: AsyncMnemoClient, core: MockCore) -> None: + """0 is the engine's own default, so the body stays byte-identical to what + an older core accepts.""" + core.respond(path="/api/v1/memory/read", json=_READ_EMPTY) + + await client.read("x", diversity=0.0) + + assert "diversity" not in core.requests[-1].json() + + @pytest.mark.parametrize("use_async", [True, False]) async def test_read_sends_include_history_and_fingerprint_when_given( use_async: bool, core: MockCore