diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f7c3be7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,34 @@ +name: CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + cache: maven + + - name: Check Formatting (Spotless) + run: mvn spotless:check + + - name: Build and Verify (Tests + Coverage) + run: mvn clean verify + + - name: Upload Coverage Reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: jacoco-report + path: target/site/jacoco/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4347e3c --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +target/ +*.class +.idea/ +*.iml +.vscode/ +*.log +.DS_Store diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..aeb13c2 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,47 @@ +# Contributing to MemU Java SDK + +Thank you for your interest in contributing to the MemU Java SDK! We welcome contributions from the community. + +## Development Setup + +1. **Java Version**: Ensure you have Java 21 installed. +2. **Maven**: This project uses Maven for dependency management and building. + +## Coding Style + +We use **Google Java Format** to maintain code consistency. This is enforced via the [Spotless Maven Plugin](https://github.com/diffplug/spotless). + +Before committing your changes, please run the following command to automatically format your code: + +```bash +mvn spotless:apply +``` + +If you do not run this, the CI build will fail during the formatting check. + +## Running Tests + +To run the unit and integration tests, use the standard Maven test command: + +```bash +mvn test +``` + +To run the full verification suite (tests + code coverage + formatting check), run: + +```bash +mvn clean verify +``` + +## Pull Requests + +1. Fork the repository. +2. Create a feature branch (`git checkout -b feature/amazing-feature`). +3. Commit your changes. +4. Run `mvn spotless:apply` and `mvn verify` to ensure everything is correct. +5. Push to the branch (`git push origin feature/amazing-feature`). +6. Open a Pull Request. + +## Code of Conduct + +Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms. diff --git a/README.md b/README.md index f712014..cc2f391 100644 --- a/README.md +++ b/README.md @@ -1 +1,96 @@ -# memU-sdk-java +# MemU Java SDK + +![Build Status](https://github.com/nevamind-ai/memu-sdk-java/actions/workflows/ci.yml/badge.svg) +![Version](https://img.shields.io/badge/version-0.1.0--SNAPSHOT-blue) +![License](https://img.shields.io/badge/license-MIT-green) + +The official Java SDK for MemU, an advanced memory storage and retrieval API for AI applications. + +## Installation + +Add the following dependency to your `pom.xml`: + +```xml + + com.nevamind.ai + memu-sdk-java + 0.1.0-SNAPSHOT + +``` + +## Quick Start + +Here is a complete example of how to initialize the client, create a memory, and search for it. + +```java +import com.nevamind.memu.MemUClient; +import com.nevamind.memu.model.*; +import java.util.List; + +public class MemUExample { + public static void main(String[] args) { + // 1. Initialize the client + String baseUrl = "https://api.memu.ai"; + String apiKey = System.getenv("MEMU_API_KEY"); + + MemUClient client = new MemUClient(baseUrl, apiKey); + + // 2. Create a Memory + MemoryItem memory = MemoryItem.builder() + .summary("The user prefers dark mode in their IDE.") + .memoryType(MemoryType.preference) + .build(); + + try { + boolean success = client.createMemory(memory); + System.out.println("Memory created: " + success); + } catch (Exception e) { + System.err.println("Failed to create memory: " + e.getMessage()); + } + + // 3. Search for Memories + SearchRequest request = SearchRequest.builder() + .queries(List.of("What IDE theme does the user like?")) + .limit(3) + .build(); + + try { + SearchResponse response = client.search(request); + response.getResults().forEach(result -> { + System.out.println("Found: " + result.getContent() + " (Score: " + result.getScore() + ")"); + }); + } catch (Exception e) { + System.err.println("Search failed: " + e.getMessage()); + } + } +} +``` + +## Configuration + +The `MemUClient` constructor requires two parameters: + +| Parameter | Description | Example | +|-----------|-------------|---------| +| `baseUrl` | The URL of your MemU API instance. | `https://api.memu.ai` | +| `apiKey` | Your authentication token. | `mem_12345abcde` | + +We recommend storing your API key in an environment variable (e.g., `MEMU_API_KEY`) rather than hardcoding it. + +## Building from Source + +To build the SDK and run tests locally, ensure you have **Java 21** and **Maven** installed. + +```bash +# Clone the repository +git clone https://github.com/nevamind-ai/memu-sdk-java.git +cd memu-sdk-java + +# Build and run integrity checks (Tests + Formatting + Coverage) +mvn clean verify +``` + +## Requirements + +* Java 21 or higher +* Maven 3.6+ diff --git a/lombok.config b/lombok.config new file mode 100644 index 0000000..df71bb6 --- /dev/null +++ b/lombok.config @@ -0,0 +1,2 @@ +config.stopBubbling = true +lombok.addLombokGeneratedAnnotation = true diff --git a/null b/null new file mode 100644 index 0000000..e69de29 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..f33ea07 --- /dev/null +++ b/pom.xml @@ -0,0 +1,136 @@ + + + 4.0.0 + + com.nevamind.ai + memu-sdk-java + 0.1.0-SNAPSHOT + + + 21 + 21 + 21 + UTF-8 + + + + + + com.squareup.okhttp3 + okhttp + 4.12.0 + + + + + com.fasterxml.jackson.core + jackson-databind + 2.16.1 + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.16.1 + + + + + org.projectlombok + lombok + 1.18.30 + provided + + + + + org.junit.jupiter + junit-jupiter + 5.10.1 + test + + + org.mockito + mockito-core + 5.8.0 + test + + + com.squareup.okhttp3 + mockwebserver + 4.12.0 + test + + + org.assertj + assertj-core + 3.24.2 + test + + + + + + + com.diffplug.spotless + spotless-maven-plugin + 2.41.1 + + + + + + + + + + org.jacoco + jacoco-maven-plugin + 0.8.11 + + + + prepare-agent + + + + report + verify + + report + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + ${java.version} + + + org.projectlombok + lombok + 1.18.30 + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.3 + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.6.3 + + false + + + + + diff --git a/src/main/java/com/nevamind/memu/MemUClient.java b/src/main/java/com/nevamind/memu/MemUClient.java new file mode 100644 index 0000000..5f76827 --- /dev/null +++ b/src/main/java/com/nevamind/memu/MemUClient.java @@ -0,0 +1,124 @@ +package com.nevamind.memu; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.nevamind.memu.exception.MemUException; +import com.nevamind.memu.model.MemoryItem; +import com.nevamind.memu.model.SearchRequest; +import com.nevamind.memu.model.SearchResponse; +import java.io.IOException; +import okhttp3.*; + +/** + * Client for interacting with the MemU API. + * + *

Provides methods to create memories and search/retrieve information. + * + *

Usage Example:

+ * + *
{@code
+ * MemUClient client = new MemUClient("https://api.memu.ai", "your-api-key");
+ *
+ * // Create a memory
+ * MemoryItem item = MemoryItem.builder()
+ *     .summary("User passed the Java certification")
+ *     .memoryType(MemoryType.SKILL)
+ *     .build();
+ * client.createMemory(item);
+ *
+ * // Search for memories
+ * SearchRequest request = SearchRequest.builder()
+ *     .queries(List.of("java certification"))
+ *     .limit(5)
+ *     .build();
+ * SearchResponse response = client.search(request);
+ * }
+ */ +public class MemUClient { + + private static final String ENDPOINT_MEMORY = "/v1/memory"; + private static final String ENDPOINT_RETRIEVE = "/v1/retrieve"; + private static final MediaType JSON = MediaType.get("application/json; charset=utf-8"); + + private final String baseUrl; + private final String apiKey; + private final OkHttpClient client; + private final ObjectMapper jsonMapper; + + /** + * Constructs a new MemUClient. + * + * @param baseUrl The base URL of the MemU API (e.g., "https://api.memu.ai"). + * @param apiKey The API key for authentication. + */ + public MemUClient(String baseUrl, String apiKey) { + this.baseUrl = baseUrl; + this.apiKey = apiKey; + this.client = new OkHttpClient(); + this.jsonMapper = new ObjectMapper(); + this.jsonMapper.registerModule(new JavaTimeModule()); + } + + /** + * Creates a new memory item in the MemU system. + * + * @param item The {@link MemoryItem} to create. + * @return {@code true} if the operation was successful. + * @throws MemUException If a network error occurs or the API returns a non-success status code. + */ + public boolean createMemory(MemoryItem item) { + try { + String json = jsonMapper.writeValueAsString(item); + RequestBody body = RequestBody.create(json, JSON); + Request request = + new Request.Builder() + .url(baseUrl + ENDPOINT_MEMORY) + .addHeader("Authorization", "Bearer " + apiKey) + .post(body) + .build(); + + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + throw new MemUException( + "Create memory failed: " + response.code() + " " + response.message()); + } + return true; + } + } catch (IOException e) { + throw new MemUException("Network error creating memory", e); + } + } + + /** + * Searches for memories based on the provided criteria. + * + * @param searchRequest The {@link SearchRequest} containing queries and filters. + * @return A {@link SearchResponse} containing the search results. + * @throws MemUException If a network error occurs, the API returns a non-success status, or the + * response body is empty. + */ + public SearchResponse search(SearchRequest searchRequest) { + try { + String json = jsonMapper.writeValueAsString(searchRequest); + RequestBody body = RequestBody.create(json, JSON); + Request request = + new Request.Builder() + .url(baseUrl + ENDPOINT_RETRIEVE) + .addHeader("Authorization", "Bearer " + apiKey) + .post(body) + .build(); + + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + throw new MemUException("Search failed: " + response.code() + " " + response.message()); + } + if (response.body() == null) { + throw new MemUException("Search returned empty body"); + } + return jsonMapper.readValue(response.body().string(), SearchResponse.class); + } + } catch (IOException e) { + throw new MemUException("Network error searching", e); + } + } +} diff --git a/src/main/java/com/nevamind/memu/exception/MemUException.java b/src/main/java/com/nevamind/memu/exception/MemUException.java new file mode 100644 index 0000000..8e46894 --- /dev/null +++ b/src/main/java/com/nevamind/memu/exception/MemUException.java @@ -0,0 +1,12 @@ +package com.nevamind.memu.exception; + +public class MemUException extends RuntimeException { + + public MemUException(String message) { + super(message); + } + + public MemUException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/nevamind/memu/model/BaseRecord.java b/src/main/java/com/nevamind/memu/model/BaseRecord.java new file mode 100644 index 0000000..5c26a3a --- /dev/null +++ b/src/main/java/com/nevamind/memu/model/BaseRecord.java @@ -0,0 +1,23 @@ +package com.nevamind.memu.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.time.ZonedDateTime; +import java.util.UUID; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class BaseRecord { + + @lombok.Builder.Default private String id = UUID.randomUUID().toString(); + + private ZonedDateTime createdAt; + + private ZonedDateTime updatedAt; +} diff --git a/src/main/java/com/nevamind/memu/model/CategoryItem.java b/src/main/java/com/nevamind/memu/model/CategoryItem.java new file mode 100644 index 0000000..1af33fe --- /dev/null +++ b/src/main/java/com/nevamind/memu/model/CategoryItem.java @@ -0,0 +1,18 @@ +package com.nevamind.memu.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@lombok.EqualsAndHashCode(callSuper = true) +public class CategoryItem extends BaseRecord { + + private String itemId; + + private String categoryId; +} diff --git a/src/main/java/com/nevamind/memu/model/MemoryCategory.java b/src/main/java/com/nevamind/memu/model/MemoryCategory.java new file mode 100644 index 0000000..e783af6 --- /dev/null +++ b/src/main/java/com/nevamind/memu/model/MemoryCategory.java @@ -0,0 +1,23 @@ +package com.nevamind.memu.model; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@lombok.EqualsAndHashCode(callSuper = true) +public class MemoryCategory extends BaseRecord { + + private String name; + + private String description; + + private String summary; + + private List embedding; +} diff --git a/src/main/java/com/nevamind/memu/model/MemoryItem.java b/src/main/java/com/nevamind/memu/model/MemoryItem.java new file mode 100644 index 0000000..570003d --- /dev/null +++ b/src/main/java/com/nevamind/memu/model/MemoryItem.java @@ -0,0 +1,23 @@ +package com.nevamind.memu.model; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@lombok.EqualsAndHashCode(callSuper = true) +public class MemoryItem extends BaseRecord { + + private String resourceId; + + private MemoryType memoryType; + + private String summary; + + private List embedding; +} diff --git a/src/main/java/com/nevamind/memu/model/MemoryType.java b/src/main/java/com/nevamind/memu/model/MemoryType.java new file mode 100644 index 0000000..f70fa25 --- /dev/null +++ b/src/main/java/com/nevamind/memu/model/MemoryType.java @@ -0,0 +1,23 @@ +package com.nevamind.memu.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public enum MemoryType { + @JsonProperty("profile") + PROFILE, + + @JsonProperty("event") + EVENT, + + @JsonProperty("knowledge") + KNOWLEDGE, + + @JsonProperty("behavior") + BEHAVIOR, + + @JsonProperty("skill") + SKILL, + + @JsonProperty("episodic") + EPISODIC; +} diff --git a/src/main/java/com/nevamind/memu/model/Resource.java b/src/main/java/com/nevamind/memu/model/Resource.java new file mode 100644 index 0000000..9aeb81a --- /dev/null +++ b/src/main/java/com/nevamind/memu/model/Resource.java @@ -0,0 +1,27 @@ +package com.nevamind.memu.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@lombok.EqualsAndHashCode(callSuper = true) +@JsonIgnoreProperties(ignoreUnknown = true) +public class Resource extends BaseRecord { + + private String url; + + private String modality; + + private String localPath; + + private String caption; + + private List embedding; +} diff --git a/src/main/java/com/nevamind/memu/model/SearchRequest.java b/src/main/java/com/nevamind/memu/model/SearchRequest.java new file mode 100644 index 0000000..f784675 --- /dev/null +++ b/src/main/java/com/nevamind/memu/model/SearchRequest.java @@ -0,0 +1,20 @@ +package com.nevamind.memu.model; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SearchRequest { + + private List queries; + + @Builder.Default private Integer limit = 10; + + private Double minScore; +} diff --git a/src/main/java/com/nevamind/memu/model/SearchResponse.java b/src/main/java/com/nevamind/memu/model/SearchResponse.java new file mode 100644 index 0000000..bd52f3f --- /dev/null +++ b/src/main/java/com/nevamind/memu/model/SearchResponse.java @@ -0,0 +1,14 @@ +package com.nevamind.memu.model; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SearchResponse { + + private List results; +} diff --git a/src/main/java/com/nevamind/memu/model/SearchResultItem.java b/src/main/java/com/nevamind/memu/model/SearchResultItem.java new file mode 100644 index 0000000..1b05e01 --- /dev/null +++ b/src/main/java/com/nevamind/memu/model/SearchResultItem.java @@ -0,0 +1,20 @@ +package com.nevamind.memu.model; + +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SearchResultItem { + + private String id; + + private String content; + + private Double score; + + private Map metadata; +} diff --git a/src/test/java/com/nevamind/memu/MemUClientTest.java b/src/test/java/com/nevamind/memu/MemUClientTest.java new file mode 100644 index 0000000..4736c85 --- /dev/null +++ b/src/test/java/com/nevamind/memu/MemUClientTest.java @@ -0,0 +1,135 @@ +package com.nevamind.memu; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.nevamind.memu.exception.MemUException; +import com.nevamind.memu.model.*; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class MemUClientTest { + + private MockWebServer mockWebServer; + private MemUClient client; + private ObjectMapper mapper; + + @BeforeEach + void setUp() throws IOException { + mockWebServer = new MockWebServer(); + mockWebServer.start(); + + // Inject mock URL into client + String baseUrl = mockWebServer.url("/").toString(); + if (baseUrl.endsWith("/")) { + baseUrl = baseUrl.substring(0, baseUrl.length() - 1); + } + client = new MemUClient(baseUrl, "test-api-key"); + + mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + } + + @AfterEach + void tearDown() throws IOException { + mockWebServer.shutdown(); + } + + @Test + void testCreateMemory_Success() throws Exception { + // Arrange + MemoryItem item = + MemoryItem.builder() + .summary("Test memory") + .memoryType(MemoryType.EVENT) + .resourceId("res-123") + .build(); + + // Enqueue 200 OK (Assuming API returns empty JSON or similar on success) + mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody("{}")); + + // Act + boolean success = client.createMemory(item); + + // Assert + assertThat(success).isTrue(); + + RecordedRequest request = mockWebServer.takeRequest(); + assertThat(request.getMethod()).isEqualTo("POST"); + assertThat(request.getPath()).isEqualTo("/v1/memory"); + assertThat(request.getHeader("Authorization")).isEqualTo("Bearer test-api-key"); + assertThat(request.getHeader("Content-Type")).contains("application/json"); + + // Verify Body + MemoryItem sentItem = mapper.readValue(request.getBody().readUtf8(), MemoryItem.class); + assertThat(sentItem.getSummary()).isEqualTo("Test memory"); + assertThat(sentItem.getMemoryType()).isEqualTo(MemoryType.EVENT); + assertThat(sentItem.getResourceId()).isEqualTo("res-123"); + } + + @Test + void testSearch_Success() throws Exception { + // Arrange + SearchRequest searchRequest = + SearchRequest.builder().queries(Collections.singletonList("test query")).limit(5).build(); + + SearchResultItem resultItem = + new SearchResultItem("mem-1", "Found content", 0.95, Map.of("source", "chat")); + SearchResponse responseBody = new SearchResponse(List.of(resultItem)); + + mockWebServer.enqueue( + new MockResponse().setResponseCode(200).setBody(mapper.writeValueAsString(responseBody))); + + // Act + SearchResponse response = client.search(searchRequest); + + // Assert + assertThat(response).isNotNull(); + assertThat(response.getResults()).hasSize(1); + assertThat(response.getResults().get(0).getId()).isEqualTo("mem-1"); + assertThat(response.getResults().get(0).getContent()).isEqualTo("Found content"); + + RecordedRequest request = mockWebServer.takeRequest(); + assertThat(request.getPath()).isEqualTo("/v1/retrieve"); + + // Verify Request Body + SearchRequest sentRequest = mapper.readValue(request.getBody().readUtf8(), SearchRequest.class); + assertThat(sentRequest.getQueries()).containsExactly("test query"); + assertThat(sentRequest.getLimit()).isEqualTo(5); + } + + @Test + void testUnauthorized_ThrowsException() { + // Arrange + MemoryItem item = MemoryItem.builder().summary("Secret").build(); + + mockWebServer.enqueue( + new MockResponse().setResponseCode(401).setBody("{\"error\":\"Unauthorized\"}")); + + // Act & Assert + MemUException exception = assertThrows(MemUException.class, () -> client.createMemory(item)); + assertThat(exception.getMessage()).contains("401"); + } + + @Test + void testServerError_ThrowsException() { + // Arrange + SearchRequest request = SearchRequest.builder().queries(List.of("query")).build(); + + mockWebServer.enqueue(new MockResponse().setResponseCode(500).setBody("Internal Server Error")); + + // Act & Assert + MemUException exception = assertThrows(MemUException.class, () -> client.search(request)); + assertThat(exception.getMessage()).contains("500"); + } +} diff --git a/src/test/java/com/nevamind/memu/model/ModelTest.java b/src/test/java/com/nevamind/memu/model/ModelTest.java new file mode 100644 index 0000000..b3c9ae5 --- /dev/null +++ b/src/test/java/com/nevamind/memu/model/ModelTest.java @@ -0,0 +1,214 @@ +package com.nevamind.memu.model; + +import static org.junit.jupiter.api.Assertions.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import java.time.ZonedDateTime; +import java.util.Collections; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ModelTest { + + private ObjectMapper mapper; + + @BeforeEach + void setUp() { + mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + } + + @Test + void testMemoryItemCoverage() throws Exception { + MemoryItem item1 = + MemoryItem.builder() + .id("test-id") + .createdAt(ZonedDateTime.now()) + .updatedAt(ZonedDateTime.now()) + .resourceId("res-1") + .memoryType(MemoryType.EVENT) + .summary("Summary") + .embedding(Collections.singletonList(0.1f)) + .build(); + + MemoryItem item2 = + MemoryItem.builder() + .id("test-id") + .createdAt(item1.getCreatedAt()) + .updatedAt(item1.getUpdatedAt()) + .resourceId("res-1") + .memoryType(MemoryType.EVENT) + .summary("Summary") + .embedding(Collections.singletonList(0.1f)) + .build(); + + assertEquals(item1, item2); + assertEquals(item1.hashCode(), item2.hashCode()); + assertNotNull(item1.toString()); + + String json = mapper.writeValueAsString(item1); + MemoryItem deserialized = mapper.readValue(json, MemoryItem.class); + + assertEquals(item1.getId(), deserialized.getId()); + assertEquals(item1.getResourceId(), deserialized.getResourceId()); + assertEquals(item1.getMemoryType(), deserialized.getMemoryType()); + assertEquals(item1.getSummary(), deserialized.getSummary()); + } + + @Test + void testResourceCoverage() throws Exception { + Resource resource1 = + Resource.builder() + .id("res-id") + .url("http://example.com") + .modality("text") + .localPath("/tmp/file") + .caption("Caption") + .embedding(Collections.singletonList(0.5f)) + .build(); + + Resource resource2 = + Resource.builder() + .id("res-id") + .url("http://example.com") + .modality("text") + .localPath("/tmp/file") + .caption("Caption") + .embedding(Collections.singletonList(0.5f)) + .build(); + + assertEquals(resource1, resource2); + assertEquals(resource1.hashCode(), resource2.hashCode()); + assertNotNull(resource1.toString()); + + String json = mapper.writeValueAsString(resource1); + Resource deserialized = mapper.readValue(json, Resource.class); + assertEquals(resource1.getId(), deserialized.getId()); + assertEquals(resource1.getUrl(), deserialized.getUrl()); + } + + @Test + void testMemoryCategoryCoverage() throws Exception { + MemoryCategory cat1 = + MemoryCategory.builder() + .id("cat-id") + .name("Category") + .description("Desc") + .summary("Sum") + .embedding(Collections.singletonList(0.9f)) + .build(); + + MemoryCategory cat2 = + MemoryCategory.builder() + .id("cat-id") + .name("Category") + .description("Desc") + .summary("Sum") + .embedding(Collections.singletonList(0.9f)) + .build(); + + assertEquals(cat1, cat2); + assertEquals(cat1.hashCode(), cat2.hashCode()); + assertNotNull(cat1.toString()); + + String json = mapper.writeValueAsString(cat1); + MemoryCategory deserialized = mapper.readValue(json, MemoryCategory.class); + assertEquals(cat1.getName(), deserialized.getName()); + } + + @Test + void testCategoryItemCoverage() throws Exception { + CategoryItem item1 = + CategoryItem.builder().id("join-id").itemId("item-1").categoryId("cat-1").build(); + + CategoryItem item2 = + CategoryItem.builder().id("join-id").itemId("item-1").categoryId("cat-1").build(); + + assertEquals(item1, item2); + assertEquals(item1.hashCode(), item2.hashCode()); + assertNotNull(item1.toString()); + + String json = mapper.writeValueAsString(item1); + CategoryItem deserialized = mapper.readValue(json, CategoryItem.class); + assertEquals(item1.getItemId(), deserialized.getItemId()); + } + + @Test + void testMemoryTypeCoverage() throws Exception { + // Test Enum serialization + String json = mapper.writeValueAsString(MemoryType.EVENT); + assertEquals("\"event\"", json); + + MemoryType deserialized = mapper.readValue("\"event\"", MemoryType.class); + assertEquals(MemoryType.EVENT, deserialized); + + // Verify all values present + assertNotNull(MemoryType.valueOf("EVENT")); + assertNotNull(MemoryType.valueOf("PROFILE")); + assertNotNull(MemoryType.valueOf("KNOWLEDGE")); + assertNotNull(MemoryType.valueOf("BEHAVIOR")); + assertNotNull(MemoryType.valueOf("SKILL")); + assertNotNull(MemoryType.valueOf("EPISODIC")); + } + + @Test + void testSearchRequestCoverage() throws Exception { + SearchRequest req1 = + SearchRequest.builder() + .queries(Collections.singletonList("query")) + .limit(10) + .minScore(0.5) + .build(); + + SearchRequest req2 = new SearchRequest(); + req2.setQueries(Collections.singletonList("query")); + req2.setLimit(10); + req2.setMinScore(0.5); + + // Test AllArgsConstructor via Builder vs NoArgsConstructor + Setters + // (indirectly) + assertEquals(req1, req2); + assertEquals(req1.hashCode(), req2.hashCode()); + assertNotNull(req1.toString()); + + String json = mapper.writeValueAsString(req1); + SearchRequest deserialized = mapper.readValue(json, SearchRequest.class); + assertEquals(req1.getLimit(), deserialized.getLimit()); + } + + @Test + void testSearchResponseCoverage() throws Exception { + SearchResultItem item = new SearchResultItem("id", "content", 1.0, null); + SearchResponse res1 = new SearchResponse(Collections.singletonList(item)); + SearchResponse res2 = new SearchResponse(); + res2.setResults(Collections.singletonList(item)); + + assertEquals(res1, res2); + assertEquals(res1.hashCode(), res2.hashCode()); + assertNotNull(res1.toString()); + + String json = mapper.writeValueAsString(res1); + SearchResponse deserialized = mapper.readValue(json, SearchResponse.class); + assertEquals(1, deserialized.getResults().size()); + } + + @Test + void testSearchResultItemCoverage() throws Exception { + SearchResultItem item1 = new SearchResultItem("id", "content", 0.9, Map.of("key", "val")); + SearchResultItem item2 = new SearchResultItem(); + item2.setId("id"); + item2.setContent("content"); + item2.setScore(0.9); + item2.setMetadata(Map.of("key", "val")); + + assertEquals(item1, item2); + assertEquals(item1.hashCode(), item2.hashCode()); + assertNotNull(item1.toString()); + + String json = mapper.writeValueAsString(item1); + SearchResultItem deserialized = mapper.readValue(json, SearchResultItem.class); + assertEquals(item1.getContent(), deserialized.getContent()); + } +}