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
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public CompletableFuture<Void> addMessageAsync(AddMessageRequest request) {
ensureOpen();
return httpClient
.post(
"/open/v1/memory/add-message/sync",
"/open/v1/memory/sync/add-message",
Objects.requireNonNull(request, "request"),
new TypeReference<ApiResult<AddMessageResponse>>() {})
.thenApply(ignored -> null);
Expand All @@ -81,7 +81,7 @@ public CompletableFuture<Void> addMessageAsync(AddMessageRequest request) {
public CompletableFuture<ExtractMemoryResponse> extractAsync(ExtractMemoryRequest request) {
ensureOpen();
return httpClient.post(
"/open/v1/memory/extract/sync",
"/open/v1/memory/sync/extract",
Objects.requireNonNull(request, "request"),
new TypeReference<ApiResult<ExtractMemoryResponse>>() {});
}
Expand All @@ -90,7 +90,7 @@ public CompletableFuture<Void> commitAsync(CommitMemoryRequest request) {
ensureOpen();
return httpClient
.post(
"/open/v1/memory/commit/sync",
"/open/v1/memory/sync/commit",
Objects.requireNonNull(request, "request"),
new TypeReference<ApiResult<ExtractMemoryResponse>>() {})
.thenApply(ignored -> null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,22 @@
*/
package com.openmemind.ai.client.exception;

import com.fasterxml.jackson.databind.JsonNode;

public class MemindApiException extends MemindClientException {

private final int httpStatus;
private final String errorCode;
private final String errorMessage;
private final String traceId;
private final String requestId;
private final JsonNode details;

public MemindApiException(
int httpStatus, String errorCode, String errorMessage, String traceId) {
int httpStatus,
String errorCode,
String errorMessage,
String requestId,
JsonNode details) {
super("Memind API error ["
+ httpStatus
+ "]: "
Expand All @@ -31,7 +38,8 @@ public MemindApiException(
this.httpStatus = httpStatus;
this.errorCode = errorCode;
this.errorMessage = errorMessage;
this.traceId = traceId;
this.requestId = requestId;
this.details = details;
}

public int getHttpStatus() {
Expand All @@ -46,7 +54,11 @@ public String getErrorMessage() {
return errorMessage;
}

public String getTraceId() {
return traceId;
public String getRequestId() {
return requestId;
}

public JsonNode getDetails() {
return details;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,6 @@
package com.openmemind.ai.client.internal;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import java.time.Instant;

@JsonIgnoreProperties(ignoreUnknown = true)
public record ApiResult<T>(String code, String message, T data, Instant timestamp, String traceId) {

public boolean isSuccess() {
return "200".equals(code) || "success".equals(code);
}
}
public record ApiResult<T>(T data) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* 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.internal;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.JsonNode;

@JsonIgnoreProperties(ignoreUnknown = true)
record ErrorResult(ApiError error) {

@JsonIgnoreProperties(ignoreUnknown = true)
record ApiError(String code, String message, JsonNode details) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public class MemindHttpClient implements AutoCloseable {

private static final Logger log = LoggerFactory.getLogger(MemindHttpClient.class);
private static final String USER_AGENT = "memind-java-client/0.2.0";
private static final String REQUEST_ID_HEADER = "X-Request-Id";

private final HttpClient httpClient;
private final ObjectMapper objectMapper;
Expand Down Expand Up @@ -132,19 +133,38 @@ private <T> T handleResponse(

try {
ApiResult<T> result = objectMapper.readValue(body, responseType);
if (status >= 200 && status < 300 && result.isSuccess()) {
if (status >= 200 && status < 300) {
return result.data();
}

throw new MemindApiException(status, result.code(), result.message(), result.traceId());
throw toApiException(status, body, requestId(response));
} catch (MemindApiException e) {
throw e;
} catch (IOException e) {
throw new MemindApiException(
status, "parse_error", "Failed to parse response: " + new String(body), null);
status,
"parse_error",
"Failed to parse response: " + new String(body),
requestId(response),
null);
}
}

private MemindApiException toApiException(int status, byte[] body, String requestId)
throws IOException {
ErrorResult errorResult = objectMapper.readValue(body, ErrorResult.class);
ErrorResult.ApiError error = errorResult.error();
if (error == null) {
return new MemindApiException(status, "http_error", "HTTP " + status, requestId, null);
}
return new MemindApiException(
status, error.code(), error.message(), requestId, error.details());
}

private static String requestId(HttpResponse<?> response) {
return response.headers().firstValue(REQUEST_ID_HEADER).orElse(null);
}

public ObjectMapper getObjectMapper() {
return objectMapper;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* 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;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

import com.openmemind.ai.client.exception.MemindApiException;
import com.openmemind.ai.client.model.common.ConversationContent;
import com.openmemind.ai.client.model.common.Message;
import com.openmemind.ai.client.model.common.Strategy;
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.time.Duration;
import java.util.List;
import java.util.UUID;
import org.junit.jupiter.api.Test;

class MemindClientIntegrationTest {

@Test
void clientCanCallRealMemindServerHealthAndErrorEnvelope() {
assumeIntegrationEnabled();

String baseUrl = envOrDefault("MEMIND_BASE_URL", "http://localhost:8366");
String apiToken = System.getenv("MEMIND_API_TOKEN");
String suffix = UUID.randomUUID().toString();

try (MemindClient client = client(baseUrl, apiToken)) {
HealthResponse health = client.health();
assertThat(health.status()).isEqualToIgnoringCase("UP");

assertThatThrownBy(
() ->
client.retrieve(
RetrieveMemoryRequest.builder()
.userId("java-it-user-" + suffix)
.agentId("java-it-agent-" + suffix)
.query("")
.strategy(Strategy.SIMPLE)
.build()))
.isInstanceOf(MemindApiException.class)
.satisfies(
ex -> {
MemindApiException apiException = (MemindApiException) ex;
assertThat(apiException.getHttpStatus()).isEqualTo(400);
assertThat(apiException.getErrorCode()).isEqualTo("validation_failed");
assertThat(apiException.getRequestId()).isNotBlank();
});
}
}

@Test
void clientCanRunRealMemindServerMemoryFlow() {
assumeIntegrationEnabled();
assumeTrue(
Boolean.parseBoolean(System.getenv("MEMIND_FULL_MEMORY_FLOW_TEST")),
"Set MEMIND_FULL_MEMORY_FLOW_TEST=true to run extraction/retrieval integration tests");

String baseUrl = envOrDefault("MEMIND_BASE_URL", "http://localhost:8366");
String apiToken = System.getenv("MEMIND_API_TOKEN");
String suffix = UUID.randomUUID().toString();
String userId = "java-it-user-" + suffix;
String agentId = "java-it-agent-" + suffix;
String memoryText = "Java integration memory " + suffix;

try (MemindClient client = client(baseUrl, apiToken)) {
HealthResponse health = client.health();
assertThat(health.status()).isEqualToIgnoringCase("UP");

ExtractMemoryResponse extract =
client.extract(
ExtractMemoryRequest.builder()
.userId(userId)
.agentId(agentId)
.rawContent(
ConversationContent.of(
List.of(Message.user(memoryText))))
.sourceClient("memind-java-integration-test")
.build());
assertThat(extract.status()).isIn("SUCCESS", "PARTIAL_SUCCESS");
assertThat(extract.rawDataIds()).isNotNull();

RetrieveMemoryResponse retrieved =
client.retrieve(
RetrieveMemoryRequest.builder()
.userId(userId)
.agentId(agentId)
.query(memoryText)
.strategy(Strategy.SIMPLE)
.trace(true)
.build());
assertThat(retrieved.items()).isNotNull();
}
}

private void assumeIntegrationEnabled() {
assumeTrue(
Boolean.parseBoolean(System.getenv("MEMIND_INTEGRATION_TEST")),
"Set MEMIND_INTEGRATION_TEST=true to run real-server integration tests");
}

private MemindClient client(String baseUrl, String apiToken) {
return MemindClient.builder()
.baseUrl(baseUrl)
.apiToken(apiToken)
.connectTimeout(Duration.ofSeconds(5))
.readTimeout(Duration.ofSeconds(60))
.build();
}

private String envOrDefault(String name, String defaultValue) {
String value = System.getenv(name);
return value == null || value.isBlank() ? defaultValue : value;
}
}
Loading
Loading