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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,16 @@ For a runnable version with centralized configuration defaults, start with
`memind-examples/memind-example-java/README.md` and
`memind-examples/memind-example-java/src/main/java/com/openmemind/ai/memory/example/java/support/ExampleSettings.java`.

### Open API ingestion semantics

The default ingestion endpoints (`/open/v1/memory/extract`, `/add-message`, and `/commit`) are
fire-and-forget: a successful HTTP response means Memind accepted and dispatched the work, not that extraction
completed. Retry-aware clients should use `/open/v1/memory/extract/sync` with a caller-owned raw-content payload
and clear their local retry state only when the returned extraction status is `SUCCESS`.

`/open/v1/memory/add-message/sync` and `/open/v1/memory/commit/sync` report immediate server-buffer success or
failure, but they are not durable replay boundaries because the server owns the buffered conversation state.

---

## Examples
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import com.openmemind.ai.client.model.request.CommitMemoryRequest;
import com.openmemind.ai.client.model.request.ExtractMemoryRequest;
import com.openmemind.ai.client.model.request.RetrieveMemoryRequest;
import com.openmemind.ai.client.model.response.AddMessageResponse;
import com.openmemind.ai.client.model.response.ExtractMemoryResponse;
import com.openmemind.ai.client.model.response.HealthResponse;
import com.openmemind.ai.client.model.response.RetrieveMemoryResponse;
import java.time.Duration;
Expand Down Expand Up @@ -50,8 +52,8 @@ public void addMessage(AddMessageRequest request) {
joinAndUnwrap(addMessageAsync(request));
}

public void extract(ExtractMemoryRequest request) {
joinAndUnwrap(extractAsync(request));
public ExtractMemoryResponse extract(ExtractMemoryRequest request) {
return joinAndUnwrap(extractAsync(request));
}

public void commit(CommitMemoryRequest request) {
Expand All @@ -68,26 +70,30 @@ public HealthResponse health() {

public CompletableFuture<Void> addMessageAsync(AddMessageRequest request) {
ensureOpen();
return httpClient.post(
"/open/v1/memory/add-message",
Objects.requireNonNull(request, "request"),
new TypeReference<ApiResult<Void>>() {});
return httpClient
.post(
"/open/v1/memory/add-message/sync",
Objects.requireNonNull(request, "request"),
new TypeReference<ApiResult<AddMessageResponse>>() {})
.thenApply(ignored -> null);
}

public CompletableFuture<Void> extractAsync(ExtractMemoryRequest request) {
public CompletableFuture<ExtractMemoryResponse> extractAsync(ExtractMemoryRequest request) {
ensureOpen();
return httpClient.post(
"/open/v1/memory/extract",
"/open/v1/memory/extract/sync",
Objects.requireNonNull(request, "request"),
new TypeReference<ApiResult<Void>>() {});
new TypeReference<ApiResult<ExtractMemoryResponse>>() {});
}

public CompletableFuture<Void> commitAsync(CommitMemoryRequest request) {
ensureOpen();
return httpClient.post(
"/open/v1/memory/commit",
Objects.requireNonNull(request, "request"),
new TypeReference<ApiResult<Void>>() {});
return httpClient
.post(
"/open/v1/memory/commit/sync",
Objects.requireNonNull(request, "request"),
new TypeReference<ApiResult<ExtractMemoryResponse>>() {})
.thenApply(ignored -> null);
}

public CompletableFuture<RetrieveMemoryResponse> retrieveAsync(RetrieveMemoryRequest request) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.openmemind.ai.client.model.response;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown = true)
public record AddMessageResponse(boolean triggered, ExtractMemoryResponse result) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.openmemind.ai.client.model.response;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import java.util.List;

@JsonIgnoreProperties(ignoreUnknown = true)
public record ExtractMemoryResponse(
String status,
List<String> rawDataIds,
List<Long> itemIds,
List<Long> insightIds,
boolean insightPending,
Long durationMillis,
String errorMessage) {}
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@
import com.openmemind.ai.client.model.common.Message;
import com.openmemind.ai.client.model.common.Strategy;
import com.openmemind.ai.client.model.request.AddMessageRequest;
import com.openmemind.ai.client.model.request.CommitMemoryRequest;
import com.openmemind.ai.client.model.request.ExtractMemoryRequest;
import com.openmemind.ai.client.model.request.RetrieveMemoryRequest;
import com.openmemind.ai.client.model.response.ExtractMemoryResponse;
import com.openmemind.ai.client.model.response.HealthResponse;
import com.openmemind.ai.client.model.response.RetrieveMemoryResponse;
import java.util.List;
Expand Down Expand Up @@ -62,11 +64,11 @@ void health_returnsResponse(WireMockRuntimeInfo wmInfo) {
@Test
void addMessage_sendsCorrectPayload(WireMockRuntimeInfo wmInfo) {
stubFor(
post("/open/v1/memory/add-message")
post("/open/v1/memory/add-message/sync")
.willReturn(
okJson(
"""
{"code":"200"}
{"code":"success","data":{"triggered":false}}
""")));

try (MemindClient client = MemindClient.builder().baseUrl(wmInfo.getHttpBaseUrl()).build()) {
Expand All @@ -79,36 +81,168 @@ void addMessage_sendsCorrectPayload(WireMockRuntimeInfo wmInfo) {
}

verify(
postRequestedFor(urlEqualTo("/open/v1/memory/add-message"))
postRequestedFor(urlEqualTo("/open/v1/memory/add-message/sync"))
.withRequestBody(matchingJsonPath("$.userId", equalTo("user-1")))
.withRequestBody(matchingJsonPath("$.message.role", equalTo("USER"))));
}

@Test
void extract_sendsRawContent(WireMockRuntimeInfo wmInfo) {
stubFor(
post("/open/v1/memory/extract")
post("/open/v1/memory/extract/sync")
.willReturn(
okJson(
"""
{"code":"200"}
{"code":"success","data":{
"status":"SUCCESS",
"rawDataIds":["rd-1"],
"itemIds":[101],
"insightIds":[],
"insightPending":false,
"durationMillis":12
}}
""")));

try (MemindClient client = MemindClient.builder().baseUrl(wmInfo.getHttpBaseUrl()).build()) {
client.extract(
ExtractMemoryRequest.builder()
.userId("user-1")
.agentId("agent-1")
.rawContent(ConversationContent.of(List.of(Message.user("test"))))
.build());
ExtractMemoryResponse response =
client.extract(
ExtractMemoryRequest.builder()
.userId("user-1")
.agentId("agent-1")
.rawContent(ConversationContent.of(List.of(Message.user("test"))))
.build());

assertThat(response.status()).isEqualTo("SUCCESS");
assertThat(response.rawDataIds()).containsExactly("rd-1");
}

verify(
postRequestedFor(urlEqualTo("/open/v1/memory/extract"))
postRequestedFor(urlEqualTo("/open/v1/memory/extract/sync"))
.withRequestBody(
matchingJsonPath("$.rawContent.type", equalTo("conversation"))));
}

@Test
void commit_usesSyncEndpoint(WireMockRuntimeInfo wmInfo) {
stubFor(
post("/open/v1/memory/commit/sync")
.willReturn(
okJson(
"""
{"code":"success","data":{
"status":"SUCCESS",
"rawDataIds":[],
"itemIds":[],
"insightIds":[],
"insightPending":false
}}
""")));

try (MemindClient client = MemindClient.builder().baseUrl(wmInfo.getHttpBaseUrl()).build()) {
client.commit(
CommitMemoryRequest.builder().userId("user-1").agentId("agent-1").build());
}

verify(postRequestedFor(urlEqualTo("/open/v1/memory/commit/sync")));
}

@Test
void extract_partialSuccessIsReturnedToCaller(WireMockRuntimeInfo wmInfo) {
stubFor(
post("/open/v1/memory/extract/sync")
.willReturn(
okJson(
"""
{"code":"success","data":{
"status":"PARTIAL_SUCCESS",
"rawDataIds":["rd-1"],
"itemIds":[],
"insightIds":[],
"insightPending":false,
"errorMessage":"insight failed"
}}
""")));

try (MemindClient client = MemindClient.builder().baseUrl(wmInfo.getHttpBaseUrl()).build()) {
ExtractMemoryResponse response =
client.extract(
ExtractMemoryRequest.builder()
.userId("u")
.agentId("a")
.rawContent(ConversationContent.of(List.of(Message.user("test"))))
.build());

assertThat(response.status()).isEqualTo("PARTIAL_SUCCESS");
assertThat(response.errorMessage()).isEqualTo("insight failed");
}
}

@Test
void extract_failureEnvelopeThrowsApiException(WireMockRuntimeInfo wmInfo) {
stubFor(
post("/open/v1/memory/extract/sync")
.willReturn(
aResponse()
.withStatus(500)
.withHeader("Content-Type", "application/json")
.withBody(
"""
{"code":"extraction_failed","message":"extract failed","traceId":"t1"}
""")));

try (MemindClient client = MemindClient.builder().baseUrl(wmInfo.getHttpBaseUrl()).build()) {
assertThatThrownBy(
() ->
client.extract(
ExtractMemoryRequest.builder()
.userId("u")
.agentId("a")
.rawContent(
ConversationContent.of(
List.of(Message.user("test"))))
.build()))
.isInstanceOf(MemindApiException.class)
.satisfies(
ex -> {
var apiEx = (MemindApiException) ex;
assertThat(apiEx.getHttpStatus()).isEqualTo(500);
assertThat(apiEx.getErrorCode()).isEqualTo("extraction_failed");
});
}
}

@Test
void extractAsync_returnsExtractionResponse(WireMockRuntimeInfo wmInfo) {
stubFor(
post("/open/v1/memory/extract/sync")
.willReturn(
okJson(
"""
{"code":"success","data":{
"status":"SUCCESS",
"rawDataIds":["rd-async"],
"itemIds":[],
"insightIds":[],
"insightPending":false
}}
""")));

try (MemindClient client = MemindClient.builder().baseUrl(wmInfo.getHttpBaseUrl()).build()) {
ExtractMemoryResponse response =
client.extractAsync(
ExtractMemoryRequest.builder()
.userId("user-1")
.agentId("agent-1")
.rawContent(ConversationContent.of(List.of(Message.user("test"))))
.build())
.join();

assertThat(response.rawDataIds()).containsExactly("rd-async");
}

verify(postRequestedFor(urlEqualTo("/open/v1/memory/extract/sync")));
}

@Test
void retrieve_returnsMemories(WireMockRuntimeInfo wmInfo) {
stubFor(
Expand Down
11 changes: 7 additions & 4 deletions memind-clients/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,12 @@ from memind.types import ConversationContent
with MemindClient(base_url="http://localhost:8080") as client:
health = client.health()

client.memory.extract(
response = client.memory.extract(
user_id="user-1",
agent_id="agent-1",
raw_content=ConversationContent(messages=[Message.user("I like coffee")]),
raw_content=ConversationContent(messages=[Message.user("Remember that I prefer concise answers.")]),
)

client.memory.commit(user_id="user-1", agent_id="agent-1")
print(response.status)

result = client.memory.retrieve(
user_id="user-1",
Expand All @@ -34,6 +33,10 @@ with MemindClient(base_url="http://localhost:8080") as client:
)
```

`memory.extract()` uses Memind's synchronous extraction endpoint and returns `ExtractMemoryResponse`. Treat only
`status == "SUCCESS"` as safe to clear caller-owned retry payloads; `PARTIAL_SUCCESS` is surfaced so applications
can keep or re-enqueue the original payload.

## Asynchronous Usage

```python
Expand Down
4 changes: 4 additions & 0 deletions memind-clients/python/src/memind/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@
from memind._version import __version__
from memind.types import (
AddMessageRequest,
AddMessageResponse,
ApiResult,
AudioBlock,
Base64Source,
CommitMemoryRequest,
ContentBlock,
ConversationContent,
ExtractMemoryRequest,
ExtractMemoryResponse,
FinalView,
HealthResponse,
ImageBlock,
Expand All @@ -57,6 +59,7 @@

__all__ = [
"AddMessageRequest",
"AddMessageResponse",
"ApiResult",
"AsyncMemindClient",
"AudioBlock",
Expand All @@ -65,6 +68,7 @@
"ContentBlock",
"ConversationContent",
"ExtractMemoryRequest",
"ExtractMemoryResponse",
"FinalView",
"HealthResponse",
"ImageBlock",
Expand Down
Loading
Loading