Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
9 changes: 9 additions & 0 deletions mnemoverse/_async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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
Expand Down Expand Up @@ -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)

Expand Down
5 changes: 4 additions & 1 deletion mnemoverse/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
)
)

Expand Down
29 changes: 29 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading