diff --git a/CHANGELOG.md b/CHANGELOG.md index aa87326..e8c795d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.3] + +### Removed + +- **Breaking**: `client.outboundConversations.start()` and + `StartOutboundConversationParams`, following the removal of + `POST /outbound/conversations` from the API. +- **Breaking**: the `customer_source` field on outbound conversation start. It + has no replacement — `customer_id` is now always your own customer ID, and + third-party platform IDs go in `customer_support_platform_identifiers`. + +### Added + +- `client.outboundConversations.startChat()`, `.startEmail()` and + `.startPhone()`, one per channel, with + `StartOutboundChatConversationParams`, `StartOutboundEmailConversationParams` + and `StartOutboundPhoneConversationParams`. `support_platform` is now required + on chat and email; phone takes `to_phone_number` and `from_phone_number` + instead. + ## [Unreleased] ### Added diff --git a/README.md b/README.md index ede9cdf..56d4f98 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,51 @@ const page = await client.procedures.list(); const next = await client.procedures.list({ cursor: page.pageInfo.next }); ``` +## Outbound conversations + +`client.outboundConversations` starts conversations the AI agent initiates, one +method per channel. All three return `{ conversation_id }`. + +```ts +// Live chat — support_platform is required. +await client.outboundConversations.startChat({ + customer_id: "customer-678", + procedure_id: "proc_abc", + support_platform: "intercom", + body: "Hi! Your order has shipped.", // optional; the agent writes one if omitted + customer_support_platform_identifiers: [ + { support_platform: "intercom", type: "intercom_user", value: "6953e162a988d9ef0f73ef9b" }, + ], +}); + +// Email — subject and body must be supplied together, or both omitted. +await client.outboundConversations.startEmail({ + customer_id: "customer-678", + procedure_id: "proc_abc", + support_platform: "zendesk", + subject: "Your recent order", + body: "Your order has shipped.", + customer_support_platform_identifiers: [ + { support_platform: "zendesk", type: "zendesk_support_user", value: "42" }, + ], +}); + +// Phone — no support_platform; the call is placed over voice. +await client.outboundConversations.startPhone({ + customer_id: "customer-678", + procedure_id: "proc_abc", + to_phone_number: "+14155551234", + from_phone_number: "+14155559876", // must be provisioned for your company +}); +``` + +`customer_id` is always your own identifier for the customer. Third-party +platform IDs go in `customer_support_platform_identifiers`, keyed by platform — +Zendesk requires type `zendesk_support_user` and Salesforce +`salesforce_contact_id`. The platform the message is delivered on needs an +identifier there, unless the customer already carries one from an earlier +conversation. + ## Webhook verification Construct the client with your `webhookSigningKey`, then verify and parse diff --git a/package.json b/package.json index d1498e1..6cf6d8c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@gradientlabs/client", - "version": "0.1.2", + "version": "0.1.3", "description": "Official Node.js / TypeScript client for the Gradient Labs API", "type": "module", "engines": { diff --git a/src/internal/version.ts b/src/internal/version.ts index b8fa277..e0d9673 100644 --- a/src/internal/version.ts +++ b/src/internal/version.ts @@ -1,2 +1,2 @@ // Kept in sync with the "version" field in package.json. -export const VERSION = "0.1.2"; +export const VERSION = "0.1.3"; diff --git a/src/models/conversations.ts b/src/models/conversations.ts index b56ffb8..4ce93c1 100644 --- a/src/models/conversations.ts +++ b/src/models/conversations.ts @@ -1,5 +1,5 @@ import type { Attachment, CustomerSupportPlatformIdentifier } from "./common.js"; -import type { Channel, ConversationEventType, CustomerSource, ParticipantType } from "./enums.js"; +import type { Channel, ConversationEventType, ParticipantType, SupportPlatform } from "./enums.js"; /** Agent-derived metadata about how a conversation was processed. */ export interface AgentMetadata { @@ -133,17 +133,78 @@ export interface ReadConversationParams { support_platform?: string; } -export interface StartOutboundConversationParams { +/** Fields shared by every outbound conversation start request. */ +interface StartOutboundConversationParamsBase { + /** + * Your own identifier for the customer, as used in your systems. It is stored + * as the customer's company customer ID and is echoed back to you in tool and + * webhook payloads. + */ customer_id: string; - customer_source: CustomerSource; + /** + * ID of the outbound procedure that defines what the AI agent should + * accomplish. It must be of type "outbound", live (deployed), and enabled for + * this channel. + */ procedure_id: string; - channel?: Channel; - support_platform?: string; - body?: string; - subject?: string; + /** + * Optional identifiers linking the customer to their record(s) in third-party + * support platforms (e.g. Intercom, Zendesk, Salesforce). Added to the + * customer alongside customer_id, and used to match against customers created + * via those platforms' native integrations. + */ + customer_support_platform_identifiers?: CustomerSupportPlatformIdentifier[]; + /** Structured context data the AI agent can use, keyed by resource type. */ resources?: Record; } +export interface StartOutboundChatConversationParams extends StartOutboundConversationParamsBase { + /** + * The platform the chat is delivered on. It needs an identifier for the + * customer in `customer_support_platform_identifiers`, unless the customer + * already carries one from an earlier conversation. + */ + support_platform: SupportPlatform; + /** + * Content of the opening message. If omitted, the AI agent generates one + * based on the procedure. + */ + body?: string; +} + +/** Opening email content: subject and body must be supplied together, or both omitted. */ +type OutboundEmailOpeningMessage = + | { + /** Subject line for the opening email. */ + subject: string; + /** Content of the opening email. */ + body: string; + } + | { + subject?: never; + body?: never; + }; + +export type StartOutboundEmailConversationParams = StartOutboundConversationParamsBase & { + /** + * The platform the email is sent from. It needs an identifier for the + * customer in `customer_support_platform_identifiers`, unless the customer + * already carries one from an earlier conversation. Zendesk requires type + * "zendesk_support_user"; Salesforce requires type "salesforce_contact_id". + */ + support_platform: SupportPlatform; +} & OutboundEmailOpeningMessage; + +export interface StartOutboundPhoneConversationParams extends StartOutboundConversationParamsBase { + /** The customer's phone number to dial, in E.164 format (e.g. "+14155551234"). */ + to_phone_number: string; + /** + * The caller ID to place the call from, in E.164 format. Must be a phone + * number already provisioned for your company. + */ + from_phone_number: string; +} + export interface OutboundConversation { conversation_id: string; } diff --git a/src/resources/outbound-conversations.ts b/src/resources/outbound-conversations.ts index aa55815..aa11b14 100644 --- a/src/resources/outbound-conversations.ts +++ b/src/resources/outbound-conversations.ts @@ -2,21 +2,60 @@ import type { HttpClient } from "../internal/http.js"; import type { RequestConfig } from "../request-config.js"; import type { OutboundConversation, - StartOutboundConversationParams, + StartOutboundChatConversationParams, + StartOutboundEmailConversationParams, + StartOutboundPhoneConversationParams, } from "../models/conversations.js"; /** - * Outbound conversation endpoints. Requires an Integration API key. + * Outbound conversation endpoints, where the AI agent proactively initiates + * contact with a customer following an outbound procedure. Requires an + * Integration API key. + * + * The customer is created, or matched to an existing record, from `customer_id` + * and any `customer_support_platform_identifiers` supplied. */ export class OutboundConversations { constructor(private readonly http: HttpClient) {} - /** Creates and starts a new outbound conversation initiated by the AI agent. */ - start( - params: StartOutboundConversationParams, + /** + * Creates and starts an outbound live chat conversation. Pass `body` to send + * a specific opening message, otherwise the AI agent writes one. + */ + startChat( + params: StartOutboundChatConversationParams, config: RequestConfig = {}, ): Promise { - return this.http.request("POST", "outbound/conversations", { + return this.http.request("POST", "outbound/conversations/chat", { + body: params, + signal: config.signal, + }); + } + + /** + * Creates and starts an outbound email conversation. Pass `subject` and + * `body` together to send a specific opening email, otherwise the AI agent + * writes one. + */ + startEmail( + params: StartOutboundEmailConversationParams, + config: RequestConfig = {}, + ): Promise { + return this.http.request("POST", "outbound/conversations/email", { + body: params, + signal: config.signal, + }); + } + + /** + * Places an outbound phone call. `from_phone_number` must be a number already + * provisioned for your company. + */ + startPhone( + params: StartOutboundPhoneConversationParams, + config: RequestConfig = {}, + ): Promise { + return this.http.request("POST", "outbound/conversations/phone", { body: params, signal: config.signal, }); diff --git a/test/http.test.ts b/test/http.test.ts index f17eee2..c330c20 100644 --- a/test/http.test.ts +++ b/test/http.test.ts @@ -34,6 +34,8 @@ const conversationJson = JSON.stringify({ latest_handoff_target: "", }); +const outboundJson = JSON.stringify({ conversation_id: "conv_out_1" }); + describe("HttpClient", () => { it("sets the Authorization bearer header on every request", async () => { const record: RecordedRequest[] = []; @@ -154,6 +156,95 @@ describe("HttpClient", () => { expect(record[0]!.body).toBeUndefined(); }); + it("starts an outbound chat conversation", async () => { + const record: RecordedRequest[] = []; + const client = new GradientLabs({ + apiKey: "sk_test_123", + fetch: fakeFetch({ status: 200, body: outboundJson }, record), + }); + + const result = await client.outboundConversations.startChat({ + customer_id: "cust_1", + procedure_id: "proc_1", + support_platform: "intercom", + body: "Hi, just checking in about your order.", + customer_support_platform_identifiers: [ + { support_platform: "intercom", type: "intercom_user", value: "6953e162a988d9ef0f73ef9b" }, + ], + }); + + expect(result.conversation_id).toBe("conv_out_1"); + expect(record[0]!.method).toBe("POST"); + expect(record[0]!.input).toBe("https://api.gradient-labs.ai/outbound/conversations/chat"); + const body = JSON.parse(record[0]!.body!); + expect(body.support_platform).toBe("intercom"); + expect(body.body).toBe("Hi, just checking in about your order."); + expect(body.customer_support_platform_identifiers).toHaveLength(1); + }); + + it("starts an outbound email conversation with a subject and body", async () => { + const record: RecordedRequest[] = []; + const client = new GradientLabs({ + apiKey: "sk_test_123", + fetch: fakeFetch({ status: 200, body: outboundJson }, record), + }); + + await client.outboundConversations.startEmail({ + customer_id: "cust_1", + procedure_id: "proc_1", + support_platform: "zendesk", + subject: "Your recent order", + body: "Your order has shipped.", + customer_support_platform_identifiers: [ + { support_platform: "zendesk", type: "zendesk_support_user", value: "42" }, + ], + }); + + expect(record[0]!.input).toBe("https://api.gradient-labs.ai/outbound/conversations/email"); + const body = JSON.parse(record[0]!.body!); + expect(body.subject).toBe("Your recent order"); + expect(body.body).toBe("Your order has shipped."); + }); + + it("starts an outbound email conversation without an opening message", async () => { + const record: RecordedRequest[] = []; + const client = new GradientLabs({ + apiKey: "sk_test_123", + fetch: fakeFetch({ status: 200, body: outboundJson }, record), + }); + + await client.outboundConversations.startEmail({ + customer_id: "cust_1", + procedure_id: "proc_1", + support_platform: "zendesk", + }); + + const body = JSON.parse(record[0]!.body!); + expect(body.subject).toBeUndefined(); + expect(body.body).toBeUndefined(); + }); + + it("starts an outbound phone conversation", async () => { + const record: RecordedRequest[] = []; + const client = new GradientLabs({ + apiKey: "sk_test_123", + fetch: fakeFetch({ status: 200, body: outboundJson }, record), + }); + + await client.outboundConversations.startPhone({ + customer_id: "cust_1", + procedure_id: "proc_1", + to_phone_number: "+14155551234", + from_phone_number: "+14155559876", + }); + + expect(record[0]!.input).toBe("https://api.gradient-labs.ai/outbound/conversations/phone"); + const body = JSON.parse(record[0]!.body!); + expect(body.to_phone_number).toBe("+14155551234"); + expect(body.from_phone_number).toBe("+14155559876"); + expect(body.support_platform).toBeUndefined(); + }); + it("respects a custom base URL", async () => { const record: RecordedRequest[] = []; const client = new GradientLabs({