diff --git a/gradient-labs-client/pom.xml b/gradient-labs-client/pom.xml index f325c56..1e48cd9 100644 --- a/gradient-labs-client/pom.xml +++ b/gradient-labs-client/pom.xml @@ -7,7 +7,7 @@ ai.gradientlabs gradient-labs-parent - 1.0.1-SNAPSHOT + 1.1.0-SNAPSHOT gradient-labs-client diff --git a/gradient-labs-client/src/main/java/ai/gradientlabs/client/GradientLabsClient.java b/gradient-labs-client/src/main/java/ai/gradientlabs/client/GradientLabsClient.java index e22ce87..cf1b097 100644 --- a/gradient-labs-client/src/main/java/ai/gradientlabs/client/GradientLabsClient.java +++ b/gradient-labs-client/src/main/java/ai/gradientlabs/client/GradientLabsClient.java @@ -243,6 +243,24 @@ public void deleteConversation(String conversationId) { httpClient.delete(path, Void.class); } + /** + * Bulk uploads a batch of memories scoped to a conversation. + *

+ * Memories are stored verbatim as raw JSON payloads for the AI agent to search + * over on demand during the conversation. The request's idempotency key + * de-duplicates retries: re-uploading with the same key returns the original + * upload instead of inserting again. + * + * @param conversationId the conversation ID + * @param request the memories to upload + * @return the upload result (upload ID and number of memories inserted) + * @throws GradientLabsException if the request fails + */ + public ai.gradientlabs.client.response.ConversationMemoriesBulkUploadResponse bulkUploadConversationMemories(String conversationId, ConversationMemoriesBulkUploadRequest request) { + String path = String.format("/conversations/%s/memories", conversationId); + return httpClient.post(path, request, ai.gradientlabs.client.response.ConversationMemoriesBulkUploadResponse.class); + } + // ==================== Tool Operations ==================== /** diff --git a/gradient-labs-client/src/main/java/ai/gradientlabs/client/request/ConversationMemoriesBulkUploadRequest.java b/gradient-labs-client/src/main/java/ai/gradientlabs/client/request/ConversationMemoriesBulkUploadRequest.java new file mode 100644 index 0000000..2e607d4 --- /dev/null +++ b/gradient-labs-client/src/main/java/ai/gradientlabs/client/request/ConversationMemoriesBulkUploadRequest.java @@ -0,0 +1,115 @@ +package ai.gradientlabs.client.request; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; +import java.util.Map; + +/** + * Parameters for bulk uploading a batch of memories scoped to a conversation. + *

+ * Memories are stored verbatim as raw JSON payloads for the AI agent to search + * over on demand during the conversation. + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ConversationMemoriesBulkUploadRequest { + + @JsonProperty("idempotency_key") + private final String idempotencyKey; + + @JsonProperty("memories") + private final List> memories; + + @JsonProperty("created_at_keys") + private final List createdAtKeys; + + private ConversationMemoriesBulkUploadRequest(Builder builder) { + this.idempotencyKey = builder.idempotencyKey; + this.memories = builder.memories; + this.createdAtKeys = builder.createdAtKeys; + } + + public static Builder builder() { + return new Builder(); + } + + public String getIdempotencyKey() { + return idempotencyKey; + } + + public List> getMemories() { + return memories; + } + + public List getCreatedAtKeys() { + return createdAtKeys; + } + + public static class Builder { + private String idempotencyKey; + private List> memories; + private List createdAtKeys; + + private Builder() { + } + + /** + * Sets the idempotency key (required). + *

+ * De-duplicates retries of the same upload. Re-uploading with the same key + * returns the original upload instead of inserting again. + * + * @param idempotencyKey the idempotency key + * @return this builder + */ + public Builder idempotencyKey(String idempotencyKey) { + this.idempotencyKey = idempotencyKey; + return this; + } + + /** + * Sets the batch of memories to store (required). + *

+ * Each element is an arbitrary JSON object stored verbatim as the memory's + * raw payload. + * + * @param memories the memories to store + * @return this builder + */ + public Builder memories(List> memories) { + this.memories = memories; + return this; + } + + /** + * Sets the JSON keys tried in order to read each memory's timestamp (optional). + *

+ * The keys are tried in order against each memory's payload. When none match, + * the upload time is used. + * + * @param createdAtKeys the created-at keys + * @return this builder + */ + public Builder createdAtKeys(List createdAtKeys) { + this.createdAtKeys = createdAtKeys; + return this; + } + + /** + * Builds the request. + * + * @return a new request instance + * @throws IllegalStateException if required fields are missing + */ + public ConversationMemoriesBulkUploadRequest build() { + if (idempotencyKey == null || idempotencyKey.isBlank()) { + throw new IllegalStateException("idempotencyKey is required"); + } + if (memories == null || memories.isEmpty()) { + throw new IllegalStateException("memories is required and cannot be empty"); + } + return new ConversationMemoriesBulkUploadRequest(this); + } + } +} diff --git a/gradient-labs-client/src/main/java/ai/gradientlabs/client/response/ConversationMemoriesBulkUploadResponse.java b/gradient-labs-client/src/main/java/ai/gradientlabs/client/response/ConversationMemoriesBulkUploadResponse.java new file mode 100644 index 0000000..f670040 --- /dev/null +++ b/gradient-labs-client/src/main/java/ai/gradientlabs/client/response/ConversationMemoriesBulkUploadResponse.java @@ -0,0 +1,41 @@ +package ai.gradientlabs.client.response; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Response from a conversation memories bulk upload. + */ +public class ConversationMemoriesBulkUploadResponse { + + @JsonProperty("upload_id") + private String uploadId; + + @JsonProperty("memories_inserted") + private long memoriesInserted; + + /** + * Gets the identifier for this upload. + * + * @return the upload ID + */ + public String getUploadId() { + return uploadId; + } + + public void setUploadId(String uploadId) { + this.uploadId = uploadId; + } + + /** + * Gets the number of memories inserted by this upload. + * + * @return the number of memories inserted + */ + public long getMemoriesInserted() { + return memoriesInserted; + } + + public void setMemoriesInserted(long memoriesInserted) { + this.memoriesInserted = memoriesInserted; + } +} diff --git a/gradient-labs-client/src/test/java/ai/gradientlabs/client/request/ConversationMemoriesBulkUploadRequestTest.java b/gradient-labs-client/src/test/java/ai/gradientlabs/client/request/ConversationMemoriesBulkUploadRequestTest.java new file mode 100644 index 0000000..67d919b --- /dev/null +++ b/gradient-labs-client/src/test/java/ai/gradientlabs/client/request/ConversationMemoriesBulkUploadRequestTest.java @@ -0,0 +1,67 @@ +package ai.gradientlabs.client.request; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ConversationMemoriesBulkUploadRequestTest { + + private final ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule()); + + @Test + void serializesAllFields() throws Exception { + ConversationMemoriesBulkUploadRequest request = ConversationMemoriesBulkUploadRequest.builder() + .idempotencyKey("key-123") + .memories(List.of( + Map.of("title", "Order #1", "amount", 42), + Map.of("title", "Order #2"))) + .createdAtKeys(List.of("created_at", "timestamp")) + .build(); + + JsonNode json = objectMapper.valueToTree(request); + + assertEquals("key-123", json.get("idempotency_key").asText()); + assertTrue(json.get("memories").isArray()); + assertEquals(2, json.get("memories").size()); + assertEquals("Order #1", json.get("memories").get(0).get("title").asText()); + assertEquals(42, json.get("memories").get(0).get("amount").asInt()); + assertEquals("created_at", json.get("created_at_keys").get(0).asText()); + assertEquals("timestamp", json.get("created_at_keys").get(1).asText()); + } + + @Test + void omitsCreatedAtKeysWhenNotSet() throws Exception { + ConversationMemoriesBulkUploadRequest request = ConversationMemoriesBulkUploadRequest.builder() + .idempotencyKey("key-123") + .memories(List.of(Map.of("title", "Order #1"))) + .build(); + + JsonNode json = objectMapper.valueToTree(request); + + assertFalse(json.has("created_at_keys")); + } + + @Test + void requiresIdempotencyKey() { + assertThrows(IllegalStateException.class, () -> ConversationMemoriesBulkUploadRequest.builder() + .memories(List.of(Map.of("title", "Order #1"))) + .build()); + } + + @Test + void requiresNonEmptyMemories() { + assertThrows(IllegalStateException.class, () -> ConversationMemoriesBulkUploadRequest.builder() + .idempotencyKey("key-123") + .memories(List.of()) + .build()); + } +} diff --git a/gradient-labs-spring-boot-starter/pom.xml b/gradient-labs-spring-boot-starter/pom.xml index 834b0c2..ebc09c9 100644 --- a/gradient-labs-spring-boot-starter/pom.xml +++ b/gradient-labs-spring-boot-starter/pom.xml @@ -7,7 +7,7 @@ ai.gradientlabs gradient-labs-parent - 1.0.1-SNAPSHOT + 1.1.0-SNAPSHOT gradient-labs-spring-boot-starter diff --git a/pom.xml b/pom.xml index 8873162..f23525b 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ ai.gradientlabs gradient-labs-parent - 1.0.1-SNAPSHOT + 1.1.0-SNAPSHOT pom Gradient Labs Java SDK