Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion src/internal/version.ts
Original file line number Diff line number Diff line change
@@ -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";
75 changes: 68 additions & 7 deletions src/models/conversations.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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<string, unknown>;
}

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;
}
51 changes: 45 additions & 6 deletions src/resources/outbound-conversations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<OutboundConversation> {
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<OutboundConversation> {
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<OutboundConversation> {
return this.http.request("POST", "outbound/conversations/phone", {
body: params,
signal: config.signal,
});
Expand Down
91 changes: 91 additions & 0 deletions test/http.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand Down Expand Up @@ -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({
Expand Down
Loading