From 27916e951de15ccd8013aefb2adc4f8332b7d9ce Mon Sep 17 00:00:00 2001 From: Arthur Ceccotti Date: Fri, 31 Jul 2026 14:19:50 +0100 Subject: [PATCH 1/2] [outbound] split outbound conversation start into chat/email/phone POST /outbound/conversations has been removed from the public API and replaced by /outbound/conversations/{chat,email,phone}. startOutboundConversation is replaced by startOutboundChatConversation, startOutboundEmailConversation and startOutboundPhoneConversation, each with its own request type. customer_source has no replacement: customer_id is now always your own customer ID, and third-party platform IDs go in customer_support_platform_identifiers. support_platform is required on chat and email; phone has no channel or support platform field. Co-Authored-By: Claude Opus 5 (1M context) --- .../client/GradientLabsClient.java | 67 +++++- .../StartOutboundChatConversationRequest.java | 219 ++++++++++++++++++ ...tartOutboundEmailConversationRequest.java} | 166 ++++++------- ...StartOutboundPhoneConversationRequest.java | 208 +++++++++++++++++ .../StartOutboundConversationRequestTest.java | 150 ++++++++++++ 5 files changed, 702 insertions(+), 108 deletions(-) create mode 100644 gradient-labs-client/src/main/java/ai/gradientlabs/client/request/StartOutboundChatConversationRequest.java rename gradient-labs-client/src/main/java/ai/gradientlabs/client/request/{StartOutboundConversationRequest.java => StartOutboundEmailConversationRequest.java} (50%) create mode 100644 gradient-labs-client/src/main/java/ai/gradientlabs/client/request/StartOutboundPhoneConversationRequest.java create mode 100644 gradient-labs-client/src/test/java/ai/gradientlabs/client/request/StartOutboundConversationRequestTest.java 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..ecd4134 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 @@ -4,6 +4,7 @@ import ai.gradientlabs.client.internal.HttpClientWrapper; import ai.gradientlabs.client.model.*; import ai.gradientlabs.client.request.*; +import ai.gradientlabs.client.response.StartOutboundConversationResponse; import ai.gradientlabs.client.webhook.Webhook; import ai.gradientlabs.client.webhook.WebhookVerifier; @@ -80,24 +81,68 @@ public Conversation startConversation(StartConversationRequest request) { } /** - * Starts an outbound conversation. + * Starts an outbound live chat conversation. *

- * Creates and starts a new outbound conversation where the AI agent proactively - * initiates contact with a customer. The conversation follows the instructions - * defined in the specified outbound procedure. + * Creates and starts a new outbound live chat conversation in which the AI agent + * proactively initiates contact with a customer, following the instructions defined in + * the specified outbound procedure. *

- * If support_platform is not provided, the system will automatically select the - * highest priority platform that has integration settings configured for your company. + * If a body is provided, that message will be sent as the opening message. Otherwise, + * the AI agent will generate one based on the procedure. *

- * If body and subject are provided, that message will be sent as the initial message. - * Otherwise, the AI agent will generate an appropriate initial message based on the procedure. + * The customer is created, or matched to an existing record, from the customer ID and + * any customer support platform identifiers you supply. The platform the chat is + * delivered on needs an identifier for that customer. * - * @param request the outbound conversation parameters + * @param request the outbound chat conversation parameters * @return the response containing the conversation ID * @throws GradientLabsException if the request fails */ - public ai.gradientlabs.client.response.StartOutboundConversationResponse startOutboundConversation(StartOutboundConversationRequest request) { - return httpClient.post("/outbound/conversations", request, ai.gradientlabs.client.response.StartOutboundConversationResponse.class); + public StartOutboundConversationResponse startOutboundChatConversation(StartOutboundChatConversationRequest request) { + return httpClient.post("/outbound/conversations/chat", request, StartOutboundConversationResponse.class); + } + + /** + * Starts an outbound email conversation. + *

+ * Creates and starts a new outbound email conversation in which the AI agent + * proactively initiates contact with a customer, following the instructions defined in + * the specified outbound procedure. + *

+ * If a subject and body are provided, that email will be sent as the opening message. + * Otherwise, the AI agent will write one based on the procedure. + *

+ * The customer is created, or matched to an existing record, from the customer ID and + * any customer support platform identifiers you supply. The platform the email is sent + * from needs an identifier for that customer, so sending from Zendesk needs a Zendesk + * identifier, and so on. + * + * @param request the outbound email conversation parameters + * @return the response containing the conversation ID + * @throws GradientLabsException if the request fails + */ + public StartOutboundConversationResponse startOutboundEmailConversation(StartOutboundEmailConversationRequest request) { + return httpClient.post("/outbound/conversations/email", request, StartOutboundConversationResponse.class); + } + + /** + * Places an outbound phone call. + *

+ * Places an outbound phone call in which the AI agent proactively contacts a customer, + * following the instructions defined in the specified outbound procedure. + *

+ * The number the call is placed from must already be provisioned for your company. + *

+ * The customer is created, or matched to an existing record, from the customer ID and + * any customer support platform identifiers you supply. The dialled number is recorded + * against that same customer. + * + * @param request the outbound phone conversation parameters + * @return the response containing the conversation ID + * @throws GradientLabsException if the request fails + */ + public StartOutboundConversationResponse startOutboundPhoneConversation(StartOutboundPhoneConversationRequest request) { + return httpClient.post("/outbound/conversations/phone", request, StartOutboundConversationResponse.class); } /** diff --git a/gradient-labs-client/src/main/java/ai/gradientlabs/client/request/StartOutboundChatConversationRequest.java b/gradient-labs-client/src/main/java/ai/gradientlabs/client/request/StartOutboundChatConversationRequest.java new file mode 100644 index 0000000..54ca783 --- /dev/null +++ b/gradient-labs-client/src/main/java/ai/gradientlabs/client/request/StartOutboundChatConversationRequest.java @@ -0,0 +1,219 @@ +package ai.gradientlabs.client.request; + +import ai.gradientlabs.client.model.CustomerSupportPlatformIdentifier; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Parameters for starting an outbound live chat conversation. + *

+ * This kicks off a proactive conversation where your AI agent initiates contact with a + * customer over chat. + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class StartOutboundChatConversationRequest { + + @JsonProperty("customer_id") + private final String customerId; + + @JsonProperty("customer_support_platform_identifiers") + private final List customerSupportPlatformIdentifiers; + + @JsonProperty("procedure_id") + private final String procedureId; + + @JsonProperty("support_platform") + private final String supportPlatform; + + @JsonProperty("body") + private final String body; + + @JsonProperty("resources") + private final Map resources; + + private StartOutboundChatConversationRequest(Builder builder) { + this.customerId = builder.customerId; + this.customerSupportPlatformIdentifiers = builder.customerSupportPlatformIdentifiers; + this.procedureId = builder.procedureId; + this.supportPlatform = builder.supportPlatform; + this.body = builder.body; + this.resources = builder.resources; + } + + public static Builder builder() { + return new Builder(); + } + + public String getCustomerId() { + return customerId; + } + + public List getCustomerSupportPlatformIdentifiers() { + return customerSupportPlatformIdentifiers; + } + + public String getProcedureId() { + return procedureId; + } + + public String getSupportPlatform() { + return supportPlatform; + } + + public String getBody() { + return body; + } + + public Map getResources() { + return resources; + } + + /** + * The support platforms an outbound chat can be delivered on. + */ + public static class SupportPlatform { + public static final String INTERCOM = "intercom"; + + /** + * Delivers the conversation to your own webhook endpoint. + */ + public static final String PUBLIC_API = "public-api"; + } + + public static class Builder { + private String customerId; + private List customerSupportPlatformIdentifiers; + private String procedureId; + private String supportPlatform; + private String body; + private Map resources; + + private Builder() { + } + + /** + * Sets your own identifier for the customer, as used in your systems (required). + *

+ * It is stored as the customer's company customer ID, and is the identifier echoed + * back to you in tool and webhook payloads. + * + * @param customerId the customer ID + * @return this builder + */ + public Builder customerId(String customerId) { + this.customerId = customerId; + return this; + } + + /** + * Sets the customer's identifiers in third-party customer support platforms (optional). + *

+ * These are added to the customer alongside {@code customerId}, and can be used to + * match against customers created via those platforms' native integrations. The + * platform named in {@code supportPlatform} needs an identifier here, unless the + * customer already carries one from an earlier conversation. + * + * @param customerSupportPlatformIdentifiers the customer support platform identifiers + * @return this builder + */ + public Builder customerSupportPlatformIdentifiers(List customerSupportPlatformIdentifiers) { + this.customerSupportPlatformIdentifiers = customerSupportPlatformIdentifiers; + return this; + } + + /** + * Adds a single customer support platform identifier. + * + * @param customerSupportPlatformIdentifier the identifier to add + * @return this builder + */ + public Builder addCustomerSupportPlatformIdentifier(CustomerSupportPlatformIdentifier customerSupportPlatformIdentifier) { + if (this.customerSupportPlatformIdentifiers == null) { + this.customerSupportPlatformIdentifiers = new ArrayList<>(); + } + this.customerSupportPlatformIdentifiers.add(customerSupportPlatformIdentifier); + return this; + } + + /** + * Sets the procedure ID (required). + *

+ * The ID of the outbound procedure that defines what the AI agent should accomplish + * in this conversation. The procedure must be of type "outbound", must be live + * (deployed), and must be enabled for the chat channel. + * + * @param procedureId the procedure ID + * @return this builder + */ + public Builder procedureId(String procedureId) { + this.procedureId = procedureId; + return this; + } + + /** + * Sets the support platform the chat is delivered on (required). + *

+ * Use constants from {@link SupportPlatform}. + * + * @param supportPlatform the support platform + * @return this builder + */ + public Builder supportPlatform(String supportPlatform) { + this.supportPlatform = supportPlatform; + return this; + } + + /** + * Sets the content of the initial message to send to the customer (optional). + *

+ * If omitted, the AI agent will generate an appropriate opening message based on the + * procedure. + * + * @param body the body + * @return this builder + */ + public Builder body(String body) { + this.body = body; + return this; + } + + /** + * Sets resources (optional). + *

+ * A JSON object containing structured data that the AI agent can use during the + * conversation. This should be organized as a map where keys are resource type names + * and values are the corresponding data. + * Example: {"customer_profile": {"tier": "premium", "lifetime_value": 5000}} + * + * @param resources the resources + * @return this builder + */ + public Builder resources(Map resources) { + this.resources = resources; + return this; + } + + /** + * Builds the request. + * + * @return a new request instance + * @throws IllegalArgumentException if required fields are missing + */ + public StartOutboundChatConversationRequest build() { + if (customerId == null || customerId.isBlank()) { + throw new IllegalArgumentException("customerId is required"); + } + if (procedureId == null || procedureId.isBlank()) { + throw new IllegalArgumentException("procedureId is required"); + } + if (supportPlatform == null || supportPlatform.isBlank()) { + throw new IllegalArgumentException("supportPlatform is required"); + } + return new StartOutboundChatConversationRequest(this); + } + } +} diff --git a/gradient-labs-client/src/main/java/ai/gradientlabs/client/request/StartOutboundConversationRequest.java b/gradient-labs-client/src/main/java/ai/gradientlabs/client/request/StartOutboundEmailConversationRequest.java similarity index 50% rename from gradient-labs-client/src/main/java/ai/gradientlabs/client/request/StartOutboundConversationRequest.java rename to gradient-labs-client/src/main/java/ai/gradientlabs/client/request/StartOutboundEmailConversationRequest.java index 4f9aea0..e64d509 100644 --- a/gradient-labs-client/src/main/java/ai/gradientlabs/client/request/StartOutboundConversationRequest.java +++ b/gradient-labs-client/src/main/java/ai/gradientlabs/client/request/StartOutboundEmailConversationRequest.java @@ -1,24 +1,26 @@ package ai.gradientlabs.client.request; +import ai.gradientlabs.client.model.CustomerSupportPlatformIdentifier; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.ArrayList; +import java.util.List; import java.util.Map; /** - * Parameters for starting an outbound conversation. + * Parameters for starting an outbound email conversation. *

- * This kicks off a proactive conversation where your AI agent initiates - * contact with a customer. + * This kicks off a proactive conversation where your AI agent emails a customer. */ @JsonInclude(JsonInclude.Include.NON_NULL) -public class StartOutboundConversationRequest { +public class StartOutboundEmailConversationRequest { @JsonProperty("customer_id") private final String customerId; - @JsonProperty("customer_source") - private final String customerSource; + @JsonProperty("customer_support_platform_identifiers") + private final List customerSupportPlatformIdentifiers; @JsonProperty("procedure_id") private final String procedureId; @@ -26,9 +28,6 @@ public class StartOutboundConversationRequest { @JsonProperty("support_platform") private final String supportPlatform; - @JsonProperty("channel") - private final String channel; - @JsonProperty("subject") private final String subject; @@ -38,12 +37,11 @@ public class StartOutboundConversationRequest { @JsonProperty("resources") private final Map resources; - private StartOutboundConversationRequest(Builder builder) { + private StartOutboundEmailConversationRequest(Builder builder) { this.customerId = builder.customerId; - this.customerSource = builder.customerSource; + this.customerSupportPlatformIdentifiers = builder.customerSupportPlatformIdentifiers; this.procedureId = builder.procedureId; this.supportPlatform = builder.supportPlatform; - this.channel = builder.channel; this.subject = builder.subject; this.body = builder.body; this.resources = builder.resources; @@ -57,8 +55,8 @@ public String getCustomerId() { return customerId; } - public String getCustomerSource() { - return customerSource; + public List getCustomerSupportPlatformIdentifiers() { + return customerSupportPlatformIdentifiers; } public String getProcedureId() { @@ -69,10 +67,6 @@ public String getSupportPlatform() { return supportPlatform; } - public String getChannel() { - return channel; - } - public String getSubject() { return subject; } @@ -86,46 +80,24 @@ public Map getResources() { } /** - * Customer source constants. + * The support platforms an outbound email can be sent from. */ - public static class CustomerSource { + public static class SupportPlatform { public static final String INTERCOM = "intercom"; - public static final String FRESHCHAT = "freshchat"; - public static final String FRESHDESK = "freshdesk"; - public static final String PUBLIC_API = "public-api"; - public static final String SALESFORCE = "salesforce"; public static final String ZENDESK = "zendesk"; - public static final String VOICE = "livekit"; - public static final String VOICE_TWILIO = "twilio"; - public static final String VOICE_TALKDESK = "talkdesk"; - public static final String VOICE_INTERCOM = "intercom-voice"; - public static final String WEB_APP = "web-app"; - public static final String FILE = "file"; - } + public static final String SALESFORCE = "salesforce"; - /** - * Support platform constants. - */ - public static class SupportPlatform { - public static final String FRESHCHAT = "freshchat"; - public static final String FRESHDESK = "freshdesk"; - public static final String INTERCOM = "intercom"; + /** + * Delivers the conversation to your own webhook endpoint. + */ public static final String PUBLIC_API = "public-api"; - public static final String SALESFORCE = "salesforce"; - public static final String ZENDESK = "zendesk"; - public static final String VOICE = "livekit"; - public static final String VOICE_TWILIO = "twilio"; - public static final String VOICE_TALKDESK = "talkdesk"; - public static final String VOICE_INTERCOM = "intercom-voice"; - public static final String WEB_APP = "web-app"; } public static class Builder { private String customerId; - private String customerSource; + private List customerSupportPlatformIdentifiers; private String procedureId; private String supportPlatform; - private String channel; private String subject; private String body; private Map resources; @@ -134,11 +106,10 @@ private Builder() { } /** - * Sets the customer ID (required). + * Sets your own identifier for the customer, as used in your systems (required). *

- * The external identifier for the customer in your support platform. - * For Intercom, this is the external ID you've defined for the user (e.g., "user-123456"). - * For other platforms, this is the customer identifier used by that platform. + * It is stored as the customer's company customer ID, and is the identifier echoed + * back to you in tool and webhook payloads. * * @param customerId the customer ID * @return this builder @@ -149,17 +120,33 @@ public Builder customerId(String customerId) { } /** - * Sets the customer source (required). + * Sets the customer's identifiers in third-party customer support platforms (optional). *

- * The source of the customer data. For example, a customer ID and phone number - * might be from Intercom, but the outbound conversation is initiated via Twilio. - * Use constants from {@link CustomerSource}. + * These are added to the customer alongside {@code customerId}, and can be used to + * match against customers created via those platforms' native integrations. The + * platform named in {@code supportPlatform} needs an identifier here, unless the + * customer already carries one from an earlier conversation. Zendesk requires type + * {@code zendesk_support_user}; Salesforce requires type {@code salesforce_contact_id}. * - * @param customerSource the customer source + * @param customerSupportPlatformIdentifiers the customer support platform identifiers * @return this builder */ - public Builder customerSource(String customerSource) { - this.customerSource = customerSource; + public Builder customerSupportPlatformIdentifiers(List customerSupportPlatformIdentifiers) { + this.customerSupportPlatformIdentifiers = customerSupportPlatformIdentifiers; + return this; + } + + /** + * Adds a single customer support platform identifier. + * + * @param customerSupportPlatformIdentifier the identifier to add + * @return this builder + */ + public Builder addCustomerSupportPlatformIdentifier(CustomerSupportPlatformIdentifier customerSupportPlatformIdentifier) { + if (this.customerSupportPlatformIdentifiers == null) { + this.customerSupportPlatformIdentifiers = new ArrayList<>(); + } + this.customerSupportPlatformIdentifiers.add(customerSupportPlatformIdentifier); return this; } @@ -167,7 +154,8 @@ public Builder customerSource(String customerSource) { * Sets the procedure ID (required). *

* The ID of the outbound procedure that defines what the AI agent should accomplish - * in this conversation. The procedure must be of type "outbound" and must be live (deployed). + * in this conversation. The procedure must be of type "outbound", must be live + * (deployed), and must be enabled for the email channel. * * @param procedureId the procedure ID * @return this builder @@ -178,12 +166,8 @@ public Builder procedureId(String procedureId) { } /** - * Sets the support platform (optional). + * Sets the support platform the email is sent from (required). *

- * The support platform where the conversation should be created. - * Valid values include "intercom", "zendesk", "freshdesk", "freshchat". - * If not provided, the system will automatically select the first connected platform - * in priority order: intercom, zendesk, freshchat, freshdesk, public-api. * Use constants from {@link SupportPlatform}. * * @param supportPlatform the support platform @@ -195,26 +179,10 @@ public Builder supportPlatform(String supportPlatform) { } /** - * Sets the channel (optional). + * Sets the subject line for the initial email (optional). *

- * Specifies the communication channel for this conversation. - * If not provided, defaults to "email". - * Valid values: "email", "web", "sms", "voice", etc. - * - * @param channel the channel - * @return this builder - */ - public Builder channel(String channel) { - this.channel = channel; - return this; - } - - /** - * Sets the subject (optional). - *

- * The subject line for the initial message (primarily used for email channels). - * Only used if body is also provided. If both subject and body are omitted, - * the AI agent will generate the initial message. + * Required if a body is provided, and forbidden otherwise. If both are omitted, the + * AI agent will write the opening email. * * @param subject the subject * @return this builder @@ -225,11 +193,10 @@ public Builder subject(String subject) { } /** - * Sets the body (optional). + * Sets the content of the initial email to send to the customer (optional). *

- * The content of the initial message to send to the customer. - * If provided, this message will be sent instead of having the AI agent generate one. - * If omitted, the AI agent will generate an appropriate initial message based on the procedure. + * Required if a subject is provided, and forbidden otherwise. If both are omitted, + * the AI agent will write the opening email. * * @param body the body * @return this builder @@ -242,11 +209,10 @@ public Builder body(String body) { /** * Sets resources (optional). *

- * A JSON object containing structured data that the AI agent can use during the conversation. - * This should be organized as a map where keys are resource type names and values are the - * corresponding data. + * A JSON object containing structured data that the AI agent can use during the + * conversation. This should be organized as a map where keys are resource type names + * and values are the corresponding data. * Example: {"customer_profile": {"tier": "premium", "lifetime_value": 5000}} - * The data will be made available to the AI agent for context during conversation processing. * * @param resources the resources * @return this builder @@ -260,19 +226,25 @@ public Builder resources(Map resources) { * Builds the request. * * @return a new request instance - * @throws IllegalArgumentException if required fields are missing + * @throws IllegalArgumentException if required fields are missing, or if only one of + * subject and body is set */ - public StartOutboundConversationRequest build() { + public StartOutboundEmailConversationRequest build() { if (customerId == null || customerId.isBlank()) { throw new IllegalArgumentException("customerId is required"); } - if (customerSource == null || customerSource.isBlank()) { - throw new IllegalArgumentException("customerSource is required"); - } if (procedureId == null || procedureId.isBlank()) { throw new IllegalArgumentException("procedureId is required"); } - return new StartOutboundConversationRequest(this); + if (supportPlatform == null || supportPlatform.isBlank()) { + throw new IllegalArgumentException("supportPlatform is required"); + } + boolean hasSubject = subject != null && !subject.isBlank(); + boolean hasBody = body != null && !body.isBlank(); + if (hasSubject != hasBody) { + throw new IllegalArgumentException("subject and body must be set together, or both omitted"); + } + return new StartOutboundEmailConversationRequest(this); } } } diff --git a/gradient-labs-client/src/main/java/ai/gradientlabs/client/request/StartOutboundPhoneConversationRequest.java b/gradient-labs-client/src/main/java/ai/gradientlabs/client/request/StartOutboundPhoneConversationRequest.java new file mode 100644 index 0000000..230ec39 --- /dev/null +++ b/gradient-labs-client/src/main/java/ai/gradientlabs/client/request/StartOutboundPhoneConversationRequest.java @@ -0,0 +1,208 @@ +package ai.gradientlabs.client.request; + +import ai.gradientlabs.client.model.CustomerSupportPlatformIdentifier; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Parameters for placing an outbound phone call. + *

+ * This kicks off a proactive conversation where your AI agent calls a customer and works + * through an outbound procedure. + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class StartOutboundPhoneConversationRequest { + + @JsonProperty("customer_id") + private final String customerId; + + @JsonProperty("customer_support_platform_identifiers") + private final List customerSupportPlatformIdentifiers; + + @JsonProperty("procedure_id") + private final String procedureId; + + @JsonProperty("to_phone_number") + private final String toPhoneNumber; + + @JsonProperty("from_phone_number") + private final String fromPhoneNumber; + + @JsonProperty("resources") + private final Map resources; + + private StartOutboundPhoneConversationRequest(Builder builder) { + this.customerId = builder.customerId; + this.customerSupportPlatformIdentifiers = builder.customerSupportPlatformIdentifiers; + this.procedureId = builder.procedureId; + this.toPhoneNumber = builder.toPhoneNumber; + this.fromPhoneNumber = builder.fromPhoneNumber; + this.resources = builder.resources; + } + + public static Builder builder() { + return new Builder(); + } + + public String getCustomerId() { + return customerId; + } + + public List getCustomerSupportPlatformIdentifiers() { + return customerSupportPlatformIdentifiers; + } + + public String getProcedureId() { + return procedureId; + } + + public String getToPhoneNumber() { + return toPhoneNumber; + } + + public String getFromPhoneNumber() { + return fromPhoneNumber; + } + + public Map getResources() { + return resources; + } + + public static class Builder { + private String customerId; + private List customerSupportPlatformIdentifiers; + private String procedureId; + private String toPhoneNumber; + private String fromPhoneNumber; + private Map resources; + + private Builder() { + } + + /** + * Sets your own identifier for the customer, as used in your systems (required). + *

+ * It is stored as the customer's company customer ID, and is the identifier echoed + * back to you in tool and webhook payloads. + * + * @param customerId the customer ID + * @return this builder + */ + public Builder customerId(String customerId) { + this.customerId = customerId; + return this; + } + + /** + * Sets the customer's identifiers in third-party customer support platforms (optional). + *

+ * These are added to the customer alongside {@code customerId}, and can be used to + * match against customers created via those platforms' native integrations, and to + * pull that platform's customer data into the call as context. + * + * @param customerSupportPlatformIdentifiers the customer support platform identifiers + * @return this builder + */ + public Builder customerSupportPlatformIdentifiers(List customerSupportPlatformIdentifiers) { + this.customerSupportPlatformIdentifiers = customerSupportPlatformIdentifiers; + return this; + } + + /** + * Adds a single customer support platform identifier. + * + * @param customerSupportPlatformIdentifier the identifier to add + * @return this builder + */ + public Builder addCustomerSupportPlatformIdentifier(CustomerSupportPlatformIdentifier customerSupportPlatformIdentifier) { + if (this.customerSupportPlatformIdentifiers == null) { + this.customerSupportPlatformIdentifiers = new ArrayList<>(); + } + this.customerSupportPlatformIdentifiers.add(customerSupportPlatformIdentifier); + return this; + } + + /** + * Sets the procedure ID (required). + *

+ * The ID of the outbound procedure that defines what the AI agent should accomplish + * on the call. The procedure must be of type "outbound", must be live (deployed), and + * must be enabled for the "voice" channel. + * + * @param procedureId the procedure ID + * @return this builder + */ + public Builder procedureId(String procedureId) { + this.procedureId = procedureId; + return this; + } + + /** + * Sets the customer's phone number to dial (required). + *

+ * E.164 format, e.g. "+14155551234". + * + * @param toPhoneNumber the number to dial + * @return this builder + */ + public Builder toPhoneNumber(String toPhoneNumber) { + this.toPhoneNumber = toPhoneNumber; + return this; + } + + /** + * Sets the caller ID to place the call from (required). + *

+ * E.164 format. It must be a phone number already provisioned for your company. + * + * @param fromPhoneNumber the caller ID + * @return this builder + */ + public Builder fromPhoneNumber(String fromPhoneNumber) { + this.fromPhoneNumber = fromPhoneNumber; + return this; + } + + /** + * Sets resources (optional). + *

+ * A JSON object containing structured data that the AI agent can use during the + * conversation. This should be organized as a map where keys are resource type names + * and values are the corresponding data. + * Example: {"customer_profile": {"tier": "premium", "lifetime_value": 5000}} + * + * @param resources the resources + * @return this builder + */ + public Builder resources(Map resources) { + this.resources = resources; + return this; + } + + /** + * Builds the request. + * + * @return a new request instance + * @throws IllegalArgumentException if required fields are missing + */ + public StartOutboundPhoneConversationRequest build() { + if (customerId == null || customerId.isBlank()) { + throw new IllegalArgumentException("customerId is required"); + } + if (procedureId == null || procedureId.isBlank()) { + throw new IllegalArgumentException("procedureId is required"); + } + if (toPhoneNumber == null || toPhoneNumber.isBlank()) { + throw new IllegalArgumentException("toPhoneNumber is required"); + } + if (fromPhoneNumber == null || fromPhoneNumber.isBlank()) { + throw new IllegalArgumentException("fromPhoneNumber is required"); + } + return new StartOutboundPhoneConversationRequest(this); + } + } +} diff --git a/gradient-labs-client/src/test/java/ai/gradientlabs/client/request/StartOutboundConversationRequestTest.java b/gradient-labs-client/src/test/java/ai/gradientlabs/client/request/StartOutboundConversationRequestTest.java new file mode 100644 index 0000000..734b694 --- /dev/null +++ b/gradient-labs-client/src/test/java/ai/gradientlabs/client/request/StartOutboundConversationRequestTest.java @@ -0,0 +1,150 @@ +package ai.gradientlabs.client.request; + +import ai.gradientlabs.client.model.CustomerSupportPlatformIdentifier; +import ai.gradientlabs.client.model.CustomerSupportPlatformIdentifierType; +import ai.gradientlabs.client.model.SupportPlatform; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +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; + +class StartOutboundConversationRequestTest { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void serializesChatRequest() { + StartOutboundChatConversationRequest request = StartOutboundChatConversationRequest.builder() + .customerId("user-1") + .procedureId("procedure-1") + .supportPlatform(StartOutboundChatConversationRequest.SupportPlatform.INTERCOM) + .body("Hi there") + .addCustomerSupportPlatformIdentifier(new CustomerSupportPlatformIdentifier( + SupportPlatform.INTERCOM, + CustomerSupportPlatformIdentifierType.INTERCOM_USER, + "6953e162a988d9ef0f73ef9b")) + .resources(Map.of("customer_profile", Map.of("tier", "premium"))) + .build(); + + JsonNode json = objectMapper.valueToTree(request); + + assertEquals("user-1", json.get("customer_id").asText()); + assertEquals("procedure-1", json.get("procedure_id").asText()); + assertEquals("intercom", json.get("support_platform").asText()); + assertEquals("Hi there", json.get("body").asText()); + assertEquals("premium", json.get("resources").get("customer_profile").get("tier").asText()); + + JsonNode identifier = json.get("customer_support_platform_identifiers").get(0); + assertEquals("intercom", identifier.get("support_platform").asText()); + assertEquals("intercom_user", identifier.get("type").asText()); + assertEquals("6953e162a988d9ef0f73ef9b", identifier.get("value").asText()); + } + + @Test + void omitsUnsetOptionalChatFields() { + StartOutboundChatConversationRequest request = StartOutboundChatConversationRequest.builder() + .customerId("user-1") + .procedureId("procedure-1") + .supportPlatform(StartOutboundChatConversationRequest.SupportPlatform.PUBLIC_API) + .build(); + + JsonNode json = objectMapper.valueToTree(request); + + assertFalse(json.has("body")); + assertFalse(json.has("resources")); + assertFalse(json.has("customer_support_platform_identifiers")); + } + + @Test + void chatRequiresSupportPlatform() { + StartOutboundChatConversationRequest.Builder builder = StartOutboundChatConversationRequest.builder() + .customerId("user-1") + .procedureId("procedure-1"); + + assertThrows(IllegalArgumentException.class, builder::build); + } + + @Test + void serializesEmailRequest() { + StartOutboundEmailConversationRequest request = StartOutboundEmailConversationRequest.builder() + .customerId("user-1") + .procedureId("procedure-1") + .supportPlatform(StartOutboundEmailConversationRequest.SupportPlatform.ZENDESK) + .subject("Your order") + .body("It has shipped.") + .addCustomerSupportPlatformIdentifier(new CustomerSupportPlatformIdentifier( + SupportPlatform.ZENDESK, + CustomerSupportPlatformIdentifierType.ZENDESK_SUPPORT_USER, + "42")) + .build(); + + JsonNode json = objectMapper.valueToTree(request); + + assertEquals("zendesk", json.get("support_platform").asText()); + assertEquals("Your order", json.get("subject").asText()); + assertEquals("It has shipped.", json.get("body").asText()); + assertEquals("zendesk_support_user", + json.get("customer_support_platform_identifiers").get(0).get("type").asText()); + } + + @Test + void emailRejectsSubjectWithoutBody() { + StartOutboundEmailConversationRequest.Builder builder = StartOutboundEmailConversationRequest.builder() + .customerId("user-1") + .procedureId("procedure-1") + .supportPlatform(StartOutboundEmailConversationRequest.SupportPlatform.INTERCOM) + .subject("Your order"); + + assertThrows(IllegalArgumentException.class, builder::build); + } + + @Test + void emailRejectsBodyWithoutSubject() { + StartOutboundEmailConversationRequest.Builder builder = StartOutboundEmailConversationRequest.builder() + .customerId("user-1") + .procedureId("procedure-1") + .supportPlatform(StartOutboundEmailConversationRequest.SupportPlatform.INTERCOM) + .body("It has shipped."); + + assertThrows(IllegalArgumentException.class, builder::build); + } + + @Test + void serializesPhoneRequest() { + StartOutboundPhoneConversationRequest request = StartOutboundPhoneConversationRequest.builder() + .customerId("user-1") + .procedureId("procedure-1") + .toPhoneNumber("+14155551234") + .fromPhoneNumber("+14155555678") + .build(); + + JsonNode json = objectMapper.valueToTree(request); + + assertEquals("+14155551234", json.get("to_phone_number").asText()); + assertEquals("+14155555678", json.get("from_phone_number").asText()); + assertFalse(json.has("support_platform")); + assertFalse(json.has("channel")); + } + + @Test + void phoneRequiresBothPhoneNumbers() { + StartOutboundPhoneConversationRequest.Builder missingFrom = StartOutboundPhoneConversationRequest.builder() + .customerId("user-1") + .procedureId("procedure-1") + .toPhoneNumber("+14155551234"); + + assertThrows(IllegalArgumentException.class, missingFrom::build); + + StartOutboundPhoneConversationRequest.Builder missingTo = StartOutboundPhoneConversationRequest.builder() + .customerId("user-1") + .procedureId("procedure-1") + .fromPhoneNumber("+14155555678"); + + assertThrows(IllegalArgumentException.class, missingTo::build); + } +} From 2453b48a853c4310030b873e1033a8bfa7a90f8a Mon Sep 17 00:00:00 2001 From: Arthur Ceccotti Date: Fri, 31 Jul 2026 14:23:33 +0100 Subject: [PATCH 2/2] [release] bump to 1.1.0 --- README.md | 8 ++++---- .../examples/spring-boot-example/pom.xml | 2 +- gradient-labs-client/pom.xml | 16 +++++++++++++++- gradient-labs-spring-boot-starter/README.md | 4 ++-- gradient-labs-spring-boot-starter/pom.xml | 2 +- pom.xml | 2 +- 6 files changed, 24 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 85f5b47..a8f6dc1 100644 --- a/README.md +++ b/README.md @@ -23,14 +23,14 @@ For Spring Boot applications, use the starter dependency for zero-configuration ai.gradientlabs gradient-labs-spring-boot-starter - 1.0.0 + 1.1.0 ``` ### Gradle ```gradle -implementation 'ai.gradientlabs:gradient-labs-spring-boot-starter:1.0.0' +implementation 'ai.gradientlabs:gradient-labs-spring-boot-starter:1.1.0' ``` ### Configuration @@ -95,14 +95,14 @@ For non-Spring applications, use the core client library: ai.gradientlabs gradient-labs-client - 1.0.0 + 1.1.0 ``` ### Gradle ```gradle -implementation 'ai.gradientlabs:gradient-labs-client:1.0.0' +implementation 'ai.gradientlabs:gradient-labs-client:1.1.0' ``` ## Error Handling diff --git a/gradient-labs-client/examples/spring-boot-example/pom.xml b/gradient-labs-client/examples/spring-boot-example/pom.xml index 1e2888d..c03d4fe 100644 --- a/gradient-labs-client/examples/spring-boot-example/pom.xml +++ b/gradient-labs-client/examples/spring-boot-example/pom.xml @@ -28,7 +28,7 @@ ai.gradientlabs gradient-labs-spring-boot-starter - 1.0.0-SNAPSHOT + 1.1.0 diff --git a/gradient-labs-client/pom.xml b/gradient-labs-client/pom.xml index f325c56..1d6f90a 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 gradient-labs-client @@ -72,6 +72,20 @@ org.apache.maven.plugins maven-source-plugin + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.2 + + + + true + + + + diff --git a/gradient-labs-spring-boot-starter/README.md b/gradient-labs-spring-boot-starter/README.md index 65ae322..c118c41 100644 --- a/gradient-labs-spring-boot-starter/README.md +++ b/gradient-labs-spring-boot-starter/README.md @@ -20,14 +20,14 @@ This starter provides zero-configuration setup for Spring Boot applications usin ai.gradientlabs gradient-labs-spring-boot-starter - 1.0.0 + 1.1.0 ``` ### Gradle ```gradle -implementation 'ai.gradientlabs:gradient-labs-spring-boot-starter:1.0.0' +implementation 'ai.gradientlabs:gradient-labs-spring-boot-starter:1.1.0' ``` ## Configuration diff --git a/gradient-labs-spring-boot-starter/pom.xml b/gradient-labs-spring-boot-starter/pom.xml index 834b0c2..39a7118 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 gradient-labs-spring-boot-starter diff --git a/pom.xml b/pom.xml index 8873162..f802f46 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ ai.gradientlabs gradient-labs-parent - 1.0.1-SNAPSHOT + 1.1.0 pom Gradient Labs Java SDK