diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e08c794 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target/ +.DS_Store diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index aaa0a6e..0000000 --- a/IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1,408 +0,0 @@ -# Java Client Implementation Summary - -This document summarizes the Java client library implementation based on the Go client at `client.go` and user-friendly Java library design best practices. - -## Research Summary - -### Java Library Design Best Practices - -Based on comprehensive research (sources below), the key principles applied: - -1. **Minimalism** - Simple, focused API with justified methods -2. **Constructor Injection** - Dependencies through constructors (via builder pattern) -3. **User-Centric Design** - Focus on developer experience and ease of use -4. **Builder Pattern** - Fluent builders for objects with multiple parameters -5. **Type Safety** - Strong typing with enums and POJOs -6. **Clear Error Handling** - Structured exception hierarchy -7. **Immutability** - Thread-safe immutable models where appropriate - -## Structure Overview - -### Multi-Module Maven Structure - -``` -gradientlabs-java/ -├── pom.xml # Parent POM (packaging=pom) -├── gradient-labs-client/ # Core client library module -│ ├── pom.xml -│ └── src/main/java/ -│ └── ai.gradientlabs.client/ -│ ├── GradientLabsClient.java # Main client with fluent builder -│ ├── exception/ # Exception hierarchy -│ │ ├── GradientLabsException.java # Base exception -│ │ ├── ResponseException.java # API error responses -│ │ └── InvalidWebhookSignatureException.java -│ ├── model/ # Domain models (POJOs) -│ │ ├── Conversation.java, Message.java, Tool.java, Note.java -│ │ ├── Channel.java, ConversationStatus.java, NoteStatus.java (enums) -│ │ ├── ParticipantType.java, AttachmentType.java (enums) -│ │ └── AgentMetadata.java, Attachment.java -│ ├── request/ # Request builders -│ │ ├── StartConversationRequest.java -│ │ ├── AddMessageRequest.java -│ │ ├── AssignmentRequest.java -│ │ ├── CreateNoteRequest.java -│ │ ├── UpdateNoteRequest.java -│ │ ├── SetNoteStatusRequest.java -│ │ └── ... (all operations) -│ ├── webhook/ # Webhook handling -│ │ ├── Webhook.java, WebhookType.java -│ │ ├── WebhookVerifier.java # HMAC signature verification -│ │ └── event/ # Event types -│ │ ├── AgentMessageEvent.java -│ │ ├── ConversationHandOffEvent.java -│ │ └── ... (all event types) -│ └── internal/ # Internal implementation -│ └── HttpClientWrapper.java # HTTP client abstraction -└── gradient-labs-spring-boot-starter/ # Spring Boot auto-configuration module - ├── pom.xml - ├── README.md - └── src/main/ - ├── java/ai.gradientlabs.spring/ - │ ├── GradientLabsProperties.java # Configuration properties - │ └── GradientLabsAutoConfiguration.java # Auto-configuration - └── resources/META-INF/spring/ - └── org.springframework.boot.autoconfigure.AutoConfiguration.imports -``` - -## Key Implementation Details - -### 1. Client Creation (Builder Pattern) - -**Go Pattern:** -```go -client, err := glabs.NewClient( - glabs.WithAPIKey(apiKey), - glabs.WithWebhookSigningKey(webhookKey), -) -``` - -**Java Pattern:** -```java -GradientLabsClient client = GradientLabsClient.builder() - .apiKey(apiKey) - .webhookSigningKey(webhookKey) - .build(); -``` - -### 2. Request Objects (Immutable Builders) - -All request objects follow this pattern: -- Immutable final fields -- Builder for construction -- Validation in `build()` method -- Fluent helper methods (e.g., `addMetadata()`, `addResource()`) - -### 3. Error Handling - -**Hierarchy:** -- `GradientLabsException` (unchecked base) - - `ResponseException` - API errors with status code, message, details, trace ID - - `InvalidWebhookSignatureException` - Webhook verification failures - -### 4. Webhook Verification - -Implements HMAC-SHA256 signature verification matching the Go implementation: -- Parses `X-GradientLabs-Signature` header -- Verifies timestamp is within leeway (default 5 minutes) -- Computes and compares signature -- Type-safe event parsing - -### 5. HTTP Client - -Uses Java 11+ `HttpClient` with: -- Automatic JSON serialization/deserialization (Jackson) -- Bearer token authentication -- Custom User-Agent: `Gradient-Labs-Java/{version} (Java/{javaVersion})` -- Synchronous and asynchronous variants - -### 6. Type Safety - -- Enums for all constants (Channel, ConversationStatus, ParticipantType, etc.) -- Strong typing throughout -- Jackson annotations for JSON mapping -- Java 8+ time API (`Instant`, `Duration`) - -## Files Created - -### Core Library - -1. **Build Configuration** - - `pom.xml` - Maven project configuration - -2. **Main Client** (1 file) - - `GradientLabsClient.java` - Main client with builder - -3. **Exceptions** (3 files) - - `GradientLabsException.java` - - `ResponseException.java` - - `InvalidWebhookSignatureException.java` - -4. **Models** (24 files) - - `Conversation.java`, `AgentMetadata.java` - - `Message.java`, `Attachment.java` - - `Tool.java`, `Note.java` - - `HandOffTarget.java`, `HandOffTargetsResponse.java` - - `Procedure.java`, `ProcedureVersion.java` - - `UserDetails.java`, `GatedConfig.java` - - `PaginationInfo.java` - - `Channel.java`, `ConversationStatus.java`, `NoteStatus.java` - - `ParticipantType.java`, `AttachmentType.java` - - `Visibility.java`, `PublicationStatus.java`, `UsageStatus.java` - - `ProcedureStatus.java` - -4a. **Response Classes** (2 files, in client package) - - `ProcedureListResponse.java` - Wraps procedures list with pagination - - `ListProcedureVersionsResponse.java` - Wraps procedure versions list - -5. **Request Objects** (20 files) - - `StartConversationRequest.java` - - `AddMessageRequest.java` - - `AssignmentRequest.java` - - `EventRequest.java` - - `FinishConversationRequest.java` - - `ReadConversationRequest.java` - - `CreateToolRequest.java` - - `UpdateToolRequest.java` - - `CreateNoteRequest.java` - - `UpdateNoteRequest.java` - - `SetNoteStatusRequest.java` - - `UpsertHandOffTargetRequest.java` - - `DeleteHandOffTargetRequest.java` - - `SetDefaultHandOffTargetRequest.java` - - `UpsertArticleRequest.java` - - `UpsertArticleTopicRequest.java` - - `SetArticleUsageStatusRequest.java` - - `ListProceduresRequest.java` - - `SetProcedureLimitRequest.java` - - `SetProcedureGatedVersionRequest.java` - -6. **Webhook Handling** (9 files) - - `Webhook.java`, `WebhookType.java` - - `WebhookVerifier.java` - Signature verification - - `WebhookConversation.java` - - `AgentMessageEvent.java` - - `ConversationHandOffEvent.java` - - `ConversationFinishedEvent.java` - - `ActionExecuteEvent.java` - - `ResourcePullEvent.java` - -7. **Internal** (1 file) - - `HttpClientWrapper.java` - HTTP operations (extended with DELETE body support) - -### Documentation (4 files) - -- `DESIGN.md` - Comprehensive design document -- `README.md` - User documentation with examples -- `IMPLEMENTATION_SUMMARY.md` - This file -- `.gitignore` - Git ignore rules - -### Examples (6 files) - -- `BasicConversationExample.java` - Complete conversation flow -- `WebhookHandlerExample.java` - Webhook handling -- `NotesExample.java` - Note management operations -- `HandOffTargetsExample.java` - Hand-off target management -- `ArticlesExample.java` - Article and topic management for knowledge base -- `ProceduresExample.java` - Procedure and version management for AI agent instructions - -## API Coverage - -### Implemented Operations - -✅ **Conversations** -- Start, Read, Finish, Cancel, Resume, Rate -- Add Message, Assign, Add Event - -✅ **Tools** -- List, Create, Read, Update, Delete - -✅ **Notes** -- Create, Update, Delete, Set Status -- Support for draft/live/deleted status -- Time-based relevance (valid_from, valid_to) -- External ID mapping - -✅ **Hand-Off Targets** -- List, Upsert (Create/Update), Delete -- Set Default by Channel -- Requires Management API key - -✅ **Articles** -- Upsert Article (Create/Update), Delete -- Upsert Article Topic (Create/Update) -- Set Usage Status (enable/disable for AI agent) -- Support for visibility levels (public/users/internal) -- Support for publication status (draft/published) -- Requires Integration API key - -✅ **Procedures** -- List, Read (with pagination and filtering) -- Set Limit (daily usage limits) -- List Versions -- Set/Unset Gated Version (gradual rollout) -- Set/Unset Live Version -- Support for gated and live version management -- Requires Management API key - -✅ **Webhooks** -- Parse and verify -- All event types (agent.message, conversation.hand_off, etc.) - -### Spring Boot Integration - -✅ **Spring Boot Starter** (completed) -- Auto-configuration for Spring Boot 3.x+ -- Type-safe configuration properties via `application.yml` or `application.properties` -- Automatic bean creation with `@ConditionalOnMissingBean` for customization -- IDE autocomplete support via configuration metadata -- See [Spring Boot Starter README](gradient-labs-spring-boot-starter/README.md) for details - -**Configuration Example:** -```yaml -gradientlabs: - api-key: ${GLABS_API_KEY} - base-url: https://api.gradient-labs.ai - webhook-signing-key: ${GLABS_WEBHOOK_KEY} - webhook-leeway: 10m -``` - -**Usage Example:** -```java -@Service -public class ConversationService { - private final GradientLabsClient client; - - public ConversationService(GradientLabsClient client) { - this.client = client; // Auto-injected - } -} -``` - -### Future Enhancements - -The following could be added in future versions: -- Resource Sources/Types API -- List Notes operation (with filtering/pagination) -- Pagination helpers for list operations -- Retry logic with exponential backoff - -## Key Differences from Go Client - -1. **Builders vs Functional Options** - - Go uses functional options pattern - - Java uses builder pattern (more idiomatic) - -2. **Error Handling** - - Go returns `(result, error)` tuple - - Java throws exceptions (unchecked) - -3. **Context** - - Go uses `context.Context` for cancellation - - Java uses method overloads or CompletableFuture - -4. **Time** - - Go uses `time.Time` and `time.Duration` - - Java uses `java.time.Instant` and `Duration` - -5. **JSON** - - Go uses struct tags - - Java uses Jackson annotations - -## Dependencies - -**Runtime:** -- Jackson 2.18.2 (JSON) -- SLF4J 2.0.16 (logging, optional) - -**Build:** -- Java 11+ (LTS) -- Maven or Gradle - -**Test:** -- JUnit 5.11.4 -- Mockito 5.14.2 - -## Validation - -The implementation follows best practices: -- ✅ Minimal API surface -- ✅ Type-safe throughout -- ✅ Clear error messages with trace IDs -- ✅ Comprehensive documentation -- ✅ Complete examples -- ✅ Thread-safe immutable models -- ✅ No dependencies except Jackson -- ✅ Java 11+ compatible - -## Usage Example - -```java -// Create client -GradientLabsClient client = GradientLabsClient.builder() - .apiKey(System.getenv("GLABS_API_KEY")) - .webhookSigningKey(System.getenv("GLABS_WEBHOOK_KEY")) - .build(); - -// Start conversation -Conversation conv = client.startConversation( - StartConversationRequest.builder() - .id("conv-123") - .customerId("user-456") - .channel(Channel.CHAT) - .build() -); - -// Add message -Message msg = client.addMessage( - conv.getId(), - AddMessageRequest.builder() - .id("msg-789") - .body("Hello!") - .participantId("user-456") - .participantType(ParticipantType.CUSTOMER) - .build() -); - -// Create a note -Note note = client.createNote( - CreateNoteRequest.builder() - .externalId("note-001") - .title("Support Hours") - .body("Our support team is available Monday-Friday, 9am-5pm EST") - .build() -); - -// Handle webhook -Webhook webhook = client.parseWebhook(request); -if (webhook.getType() == WebhookType.AGENT_MESSAGE) { - AgentMessageEvent event = webhook.asAgentMessage(); - System.out.println("Agent: " + event.getBody()); -} -``` - -## Research Sources - -The design is based on industry best practices from: - -- [Designing a User Friendly Java Library | Baeldung](https://www.baeldung.com/design-a-user-friendly-java-library) -- [Java API Best Practices - DZone Refcards](https://dzone.com/refcardz/java-api-best-practices) -- [API design: principles and best practices · YourBasic](https://yourbasic.org/algorithms/your-basic-api/) -- [Google Best Practices for Java Libraries](https://jlbp.dev/) -- [API design practices for Java - IBM](https://developer.ibm.com/articles/api-design-practices-for-java/) -- [Best Practices for API Design in Java - JAVAPRO International](https://javapro.io/2025/06/04/best-practices-for-api-design-in-java/) -- [Effective API Design in Java — A Guide to Creating Strong and User-Friendly APIs - Medium](https://medium.com/@AlexanderObregon/effective-api-design-in-java-a-guide-to-creating-robust-and-user-friendly-apis-e75942348bc0) - -## Next Steps - -To complete the implementation: - -1. **Add remaining models** - Procedure, ResourceSource, ResourceType -2. **Add remaining operations** - Procedure, ResourceSource/Type APIs -3. **Add tests** - Unit tests and integration tests -4. **Add pagination helpers** - Iterator-style API for list operations -5. **Add retry logic** - Configurable retry with exponential backoff -6. **Publish to Maven Central** - For easy consumption -7. **Create Spring Boot starter** - Auto-configuration for Spring apps - -The base structure is complete and ready for these enhancements! diff --git a/gradient-labs-client/pom.xml b/gradient-labs-client/pom.xml index 1021451..f325c56 100644 --- a/gradient-labs-client/pom.xml +++ b/gradient-labs-client/pom.xml @@ -7,7 +7,7 @@ ai.gradientlabs gradient-labs-parent - 1.0.0-SNAPSHOT + 1.0.1-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 8705727..e22ce87 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 @@ -227,6 +227,22 @@ public void returnAsyncToolResult(String conversationId, ReturnAsyncToolResultRe httpClient.put(path, request, Void.class); } + /** + * Deletes a conversation. + *

+ * Deletion is performed asynchronously; a successful call is acknowledged + * with a 202 Accepted response. + *

+ * Note: Requires a Management API key. + * + * @param conversationId the conversation ID + * @throws GradientLabsException if the request fails + */ + public void deleteConversation(String conversationId) { + String path = String.format("/conversations/%s", conversationId); + httpClient.delete(path, Void.class); + } + // ==================== Tool Operations ==================== /** @@ -1123,6 +1139,22 @@ public BackOfficeTask readBackOfficeTask(String taskId) { return httpClient.get(path, null, BackOfficeTask.class); } + /** + * Deletes a back office task. + *

+ * Deletion is performed asynchronously; a successful call is acknowledged + * with a 202 Accepted response. + *

+ * Note: Requires a Management API key. + * + * @param taskId the task ID + * @throws GradientLabsException if the request fails + */ + public void deleteBackOfficeTask(String taskId) { + String path = String.format("/back-office-tasks/%s", taskId); + httpClient.delete(path, Void.class); + } + // ==================== Voice Call Context Operations ==================== /** diff --git a/gradient-labs-client/src/test/java/ai/gradientlabs/client/GradientLabsClientTest.java b/gradient-labs-client/src/test/java/ai/gradientlabs/client/GradientLabsClientTest.java new file mode 100644 index 0000000..71f96b8 --- /dev/null +++ b/gradient-labs-client/src/test/java/ai/gradientlabs/client/GradientLabsClientTest.java @@ -0,0 +1,185 @@ +package ai.gradientlabs.client; + +import ai.gradientlabs.client.internal.HttpClientWrapper; +import org.junit.jupiter.api.Test; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSession; +import java.net.Authenticator; +import java.net.CookieHandler; +import java.net.ProxySelector; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.WebSocket; +import java.time.Duration; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Exercises the DELETE plumbing that {@code deleteConversation} and + * {@code deleteBackOfficeTask} delegate to. + */ +class GradientLabsClientTest { + + @Test + void deleteSendsDeleteRequestWithoutBodyAndReturnsNullOnAccepted() { + RecordingHttpClient httpClient = new RecordingHttpClient(); + HttpClientWrapper wrapper = new HttpClientWrapper(httpClient, "test-key", "test-agent"); + + Void result = wrapper.delete("https://api.example.com/conversations/conv-123", Void.class); + + assertNull(result); + assertEquals("DELETE", httpClient.lastRequest.method()); + assertEquals("https://api.example.com/conversations/conv-123", httpClient.lastRequest.uri().toString()); + assertTrue(httpClient.lastRequest.bodyPublisher().isPresent()); + assertEquals(0, httpClient.lastRequest.bodyPublisher().get().contentLength()); + } + + @Test + void deleteSendsDeleteToBackOfficeTaskPath() { + RecordingHttpClient httpClient = new RecordingHttpClient(); + HttpClientWrapper wrapper = new HttpClientWrapper(httpClient, "test-key", "test-agent"); + + wrapper.delete("https://api.example.com/back-office-tasks/task-456", Void.class); + + assertEquals("DELETE", httpClient.lastRequest.method()); + assertEquals("https://api.example.com/back-office-tasks/task-456", httpClient.lastRequest.uri().toString()); + } + + /** + * Minimal {@link HttpClient} that records the last request and always replies + * with an empty 202 Accepted response, mirroring the API's delete endpoints. + */ + private static final class RecordingHttpClient extends HttpClient { + private HttpRequest lastRequest; + + @Override + @SuppressWarnings("unchecked") + public HttpResponse send(HttpRequest request, HttpResponse.BodyHandler responseBodyHandler) { + this.lastRequest = request; + return (HttpResponse) new AcceptedResponse(request); + } + + @Override + public CompletableFuture> sendAsync(HttpRequest request, HttpResponse.BodyHandler responseBodyHandler) { + return CompletableFuture.completedFuture(send(request, responseBodyHandler)); + } + + @Override + public CompletableFuture> sendAsync(HttpRequest request, HttpResponse.BodyHandler responseBodyHandler, HttpResponse.PushPromiseHandler pushPromiseHandler) { + return CompletableFuture.completedFuture(send(request, responseBodyHandler)); + } + + @Override + public Optional cookieHandler() { + return Optional.empty(); + } + + @Override + public Optional connectTimeout() { + return Optional.empty(); + } + + @Override + public Redirect followRedirects() { + return Redirect.NEVER; + } + + @Override + public Optional proxy() { + return Optional.empty(); + } + + @Override + public SSLContext sslContext() { + try { + return SSLContext.getDefault(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public SSLParameters sslParameters() { + return new SSLParameters(); + } + + @Override + public Optional authenticator() { + return Optional.empty(); + } + + @Override + public Version version() { + return Version.HTTP_1_1; + } + + @Override + public Optional executor() { + return Optional.empty(); + } + + @Override + public WebSocket.Builder newWebSocketBuilder() { + throw new UnsupportedOperationException(); + } + } + + private static final class AcceptedResponse implements HttpResponse { + private final HttpRequest request; + + private AcceptedResponse(HttpRequest request) { + this.request = request; + } + + @Override + public int statusCode() { + return 202; + } + + @Override + public HttpRequest request() { + return request; + } + + @Override + public Optional> previousResponse() { + return Optional.empty(); + } + + @Override + public HttpHeaders headers() { + return HttpHeaders.of(Map.of(), (a, b) -> true); + } + + @Override + public String body() { + return ""; + } + + @Override + public Optional sslSession() { + return Optional.empty(); + } + + @Override + public URI uri() { + return request.uri(); + } + + @Override + public HttpClient.Version version() { + return HttpClient.Version.HTTP_1_1; + } + } +} diff --git a/pom.xml b/pom.xml index 0744945..8873162 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ ai.gradientlabs gradient-labs-parent - 1.0.0-SNAPSHOT + 1.0.1-SNAPSHOT pom Gradient Labs Java SDK