From a38cc7916ab24e11f8b165a22c3c36cf1c5302f0 Mon Sep 17 00:00:00 2001 From: Arthur Ceccotti Date: Fri, 31 Jul 2026 14:18:34 +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 POST /outbound/conversations/{chat,email,phone}. Replaces start_outbound_conversation with start_outbound_chat_conversation, start_outbound_email_conversation and start_outbound_phone_conversation. 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 takes to_phone_number/from_phone_number and no platform. Co-Authored-By: Claude Opus 5 (1M context) --- .../_outbound_conversation_start.py | 284 +++++++++++++----- src/gradient_labs/client.py | 71 ++++- tests/test_outbound_conversation_start.py | 185 ++++++++++++ 3 files changed, 448 insertions(+), 92 deletions(-) create mode 100644 tests/test_outbound_conversation_start.py diff --git a/src/gradient_labs/_outbound_conversation_start.py b/src/gradient_labs/_outbound_conversation_start.py index a29bb7f..11859d8 100644 --- a/src/gradient_labs/_outbound_conversation_start.py +++ b/src/gradient_labs/_outbound_conversation_start.py @@ -1,95 +1,145 @@ -from typing import Optional, Dict, Any +from typing import Optional, Dict, Any, List from enum import Enum from dataclasses import dataclass from dataclasses_json import dataclass_json from ._http_client import HttpClient +from ._conversation_start import CustomerSupportPlatformIdentifier -class CustomerSource(str, Enum): - """Identifies where customer data originates from.""" +class OutboundSupportPlatform(str, Enum): + """Identifies the support platform an outbound chat or email is delivered on.""" - INTERCOM = "intercom" - FRESHCHAT = "freshchat" - FRESHDESK = "freshdesk" - PUBLIC_API = "public-api" - SALESFORCE = "salesforce" - ZENDESK = "zendesk" - VOICE = "livekit" - VOICE_TWILIO = "twilio" - VOICE_TALKDESK = "talkdesk" - VOICE_INTERCOM = "intercom-voice" - WEB_APP = "web-app" - FILE = "file" + INTERCOM: str = "intercom" + ZENDESK: str = "zendesk" + SALESFORCE: str = "salesforce" + # PUBLIC_API delivers the conversation to your own webhook endpoint. + PUBLIC_API: str = "public-api" -class SupportPlatform(str, Enum): - """Identifies the support platform where the conversation will be created.""" - FRESHCHAT = "freshchat" - FRESHDESK = "freshdesk" - INTERCOM = "intercom" - PUBLIC_API = "public-api" - SALESFORCE = "salesforce" - ZENDESK = "zendesk" - VOICE = "livekit" - VOICE_TWILIO = "twilio" - VOICE_TALKDESK = "talkdesk" - VOICE_INTERCOM = "intercom-voice" - WEB_APP = "web-app" +@dataclass_json +@dataclass(frozen=True) +class StartOutboundChatConversationParams: + """Parameters for starting an outbound live chat conversation.""" + + # customer_id is your own identifier for the customer, as used in your systems. + # It is stored as the customer's company customer ID, and is the identifier echoed + # back to you in tool and webhook payloads. + customer_id: str + + # procedure_id is 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. + procedure_id: str + + # support_platform is the support platform the chat is delivered on. + # Valid values: "intercom", "public-api". + support_platform: OutboundSupportPlatform + + # customer_support_platform_identifiers optionally links the customer to their + # record(s) in third-party support platforms (e.g. Intercom), alongside customer_id. + # + # The platform named in support_platform needs an identifier here, unless the + # customer already carries one from an earlier conversation. + customer_support_platform_identifiers: Optional[ + List[CustomerSupportPlatformIdentifier] + ] = None + + # body is the content of the initial message to send to the customer. + # If omitted, the AI agent will generate an appropriate opening message based on + # the procedure. + body: Optional[str] = None + + # resources is a JSON object containing structured data that the AI agent + # can use during the conversation. This should be organized as a dict + # where keys are resource type names and values are the corresponding data. + # Example: {"customer_profile": {"tier": "premium", "lifetime_value": 5000}} + resources: Optional[Dict[str, Any]] = None @dataclass_json @dataclass(frozen=True) -class StartOutboundConversationParams: - """Parameters for starting a new outbound conversation. +class StartOutboundEmailConversationParams: + """Parameters for starting an outbound email conversation.""" - This kicks off a proactive conversation where your AI agent initiates - contact with a customer. - """ - - # customer_id is 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. + # customer_id is your own identifier for the customer, as used in your systems. + # It is stored as the customer's company customer ID, and is the identifier echoed + # back to you in tool and webhook payloads. customer_id: str - # customer_source is 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. - customer_source: CustomerSource - # procedure_id is 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). + # should accomplish in this conversation. The procedure must be of type "outbound", + # must be live (deployed), and must be enabled for the email channel. procedure_id: str - # support_platform is 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. - support_platform: Optional[SupportPlatform] = None + # support_platform is the support platform the email is sent from. + # Valid values: "intercom", "zendesk", "salesforce", "public-api". + support_platform: OutboundSupportPlatform - # channel specifies the communication channel for this conversation. - # If not provided, defaults to "email". - # Valid values: "email", "web", "sms", "voice", etc. - channel: Optional[str] = None + # customer_support_platform_identifiers optionally links the customer to their + # record(s) in third-party support platforms (e.g. Intercom, Zendesk, Salesforce), + # alongside customer_id. + # + # The platform named in support_platform needs an identifier here, unless the + # customer already carries one from an earlier conversation. Zendesk requires type + # "zendesk_support_user"; Salesforce requires type "salesforce_contact_id". + customer_support_platform_identifiers: Optional[ + List[CustomerSupportPlatformIdentifier] + ] = None - # subject is 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. + # subject is the subject line for the initial email. Required if body is provided, + # and forbidden otherwise. If both are omitted, the AI agent will write the opening + # email. subject: Optional[str] = None - # body is 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. + # body is the content of the initial email to send to the customer. Required if + # subject is provided, and forbidden otherwise. body: Optional[str] = None # resources is a JSON object containing structured data that the AI agent # can use during the conversation. This should be organized as a dict # 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. + resources: Optional[Dict[str, Any]] = None + + +@dataclass_json +@dataclass(frozen=True) +class StartOutboundPhoneConversationParams: + """Parameters for placing an outbound phone call.""" + + # customer_id is your own identifier for the customer, as used in your systems. + # It is stored as the customer's company customer ID, and is the identifier echoed + # back to you in tool and webhook payloads. + customer_id: str + + # procedure_id is 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. + procedure_id: str + + # to_phone_number is the customer's phone number to dial (E.164 format, + # e.g. "+14155551234"). + to_phone_number: str + + # from_phone_number is the caller ID to place the call from (E.164 format). + # It must be a phone number already provisioned for your company. + from_phone_number: str + + # customer_support_platform_identifiers optionally links the customer to their + # record(s) in third-party support platforms (e.g. Intercom, Zendesk, Salesforce), + # alongside customer_id. They are also used to pull that platform's customer data + # into the call as context. + customer_support_platform_identifiers: Optional[ + List[CustomerSupportPlatformIdentifier] + ] = None + + # resources is a JSON object containing structured data that the AI agent + # can use during the conversation. This should be organized as a dict + # where keys are resource type names and values are the corresponding data. + # Example: {"customer_profile": {"tier": "premium", "lifetime_value": 5000}} resources: Optional[Dict[str, Any]] = None @@ -103,36 +153,77 @@ class StartOutboundConversationResponse: conversation_id: str -def start_outbound_conversation( - *, client: HttpClient, params: StartOutboundConversationParams +def _support_platform_value(platform: OutboundSupportPlatform) -> str: + return platform.value if isinstance(platform, OutboundSupportPlatform) else platform + + +def _identifiers( + identifiers: List[CustomerSupportPlatformIdentifier], +) -> List[Dict[str, Any]]: + return [i.to_dict() for i in identifiers] + + +def start_outbound_chat_conversation( + *, client: HttpClient, params: StartOutboundChatConversationParams ) -> StartOutboundConversationResponse: - """Creates and starts a new outbound conversation where the AI agent - proactively initiates contact with a customer. + """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. - The conversation follows the instructions defined in the specified outbound procedure. + If body is provided, that message will be sent as the opening message. Otherwise, + the AI agent will generate one based on the procedure. - If support_platform is not provided, the system will automatically select the highest - priority platform that has integration settings configured for your company. + The customer is created, or matched to an existing record, from customer_id and any + customer_support_platform_identifiers you supply. The platform the chat is delivered + on needs an identifier for that customer. + """ + body = { + "customer_id": params.customer_id, + "procedure_id": params.procedure_id, + "support_platform": _support_platform_value(params.support_platform), + } + + if params.customer_support_platform_identifiers is not None: + body["customer_support_platform_identifiers"] = _identifiers( + params.customer_support_platform_identifiers + ) + if params.body is not None: + body["body"] = params.body + if params.resources is not None: + body["resources"] = params.resources + + rsp = client.post( + path="outbound/conversations/chat", + body=body, + ) + return StartOutboundConversationResponse.from_dict(rsp) + + +def start_outbound_email_conversation( + *, client: HttpClient, params: StartOutboundEmailConversationParams +) -> StartOutboundConversationResponse: + """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 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. + If body and subject 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 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. """ body = { "customer_id": params.customer_id, - "customer_source": params.customer_source.value - if isinstance(params.customer_source, CustomerSource) - else params.customer_source, "procedure_id": params.procedure_id, + "support_platform": _support_platform_value(params.support_platform), } - if params.support_platform is not None: - body["support_platform"] = ( - params.support_platform.value - if isinstance(params.support_platform, SupportPlatform) - else params.support_platform + if params.customer_support_platform_identifiers is not None: + body["customer_support_platform_identifiers"] = _identifiers( + params.customer_support_platform_identifiers ) - if params.channel is not None: - body["channel"] = params.channel if params.subject is not None: body["subject"] = params.subject if params.body is not None: @@ -141,7 +232,40 @@ def start_outbound_conversation( body["resources"] = params.resources rsp = client.post( - path="outbound/conversations", + path="outbound/conversations/email", + body=body, + ) + return StartOutboundConversationResponse.from_dict(rsp) + + +def start_outbound_phone_conversation( + *, client: HttpClient, params: StartOutboundPhoneConversationParams +) -> StartOutboundConversationResponse: + """Places an outbound phone call in which the AI agent proactively contacts a + customer, following the instructions defined in the specified outbound procedure. + + from_phone_number must be a phone number already provisioned for your company. + + The customer is created, or matched to an existing record, from customer_id and any + customer_support_platform_identifiers you supply. The dialled number is recorded + against that same customer. + """ + body = { + "customer_id": params.customer_id, + "procedure_id": params.procedure_id, + "to_phone_number": params.to_phone_number, + "from_phone_number": params.from_phone_number, + } + + if params.customer_support_platform_identifiers is not None: + body["customer_support_platform_identifiers"] = _identifiers( + params.customer_support_platform_identifiers + ) + if params.resources is not None: + body["resources"] = params.resources + + rsp = client.post( + path="outbound/conversations/phone", body=body, ) return StartOutboundConversationResponse.from_dict(rsp) diff --git a/src/gradient_labs/client.py b/src/gradient_labs/client.py index eb8a1d6..a9c7e6b 100644 --- a/src/gradient_labs/client.py +++ b/src/gradient_labs/client.py @@ -32,8 +32,13 @@ ) from ._outbound_conversation_start import ( - start_outbound_conversation, - StartOutboundConversationParams, + start_outbound_chat_conversation, + start_outbound_email_conversation, + start_outbound_phone_conversation, + OutboundSupportPlatform as OutboundSupportPlatform, + StartOutboundChatConversationParams, + StartOutboundEmailConversationParams, + StartOutboundPhoneConversationParams, StartOutboundConversationResponse, ) @@ -341,23 +346,65 @@ def start_conversation( params=params, ) - def start_outbound_conversation( + def start_outbound_chat_conversation( self, *, - params: StartOutboundConversationParams, + params: StartOutboundChatConversationParams, ) -> StartOutboundConversationResponse: - """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 + The AI agent proactively initiates contact with a customer, following the + instructions defined in the specified outbound procedure. + + If body is provided, that message will be sent as the opening message. + Otherwise, the AI agent will generate one based on the procedure. + + The customer is created, or matched to an existing record, from customer_id and + any customer_support_platform_identifiers you supply. The platform the chat is + delivered on needs an identifier for that customer.""" + return start_outbound_chat_conversation( + client=self.http_client, + params=params, + ) + + def start_outbound_email_conversation( + self, + *, + params: StartOutboundEmailConversationParams, + ) -> StartOutboundConversationResponse: + """Starts an outbound email conversation. + + The AI agent proactively initiates contact with a customer, following the + instructions defined in the specified outbound procedure. + + If body and subject 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 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.""" + return start_outbound_email_conversation( + client=self.http_client, + params=params, + ) + + def start_outbound_phone_conversation( + self, + *, + params: StartOutboundPhoneConversationParams, + ) -> StartOutboundConversationResponse: + """Places an outbound phone call. + + The AI agent proactively contacts 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. + from_phone_number must be a phone number already provisioned for your company. - 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.""" - return start_outbound_conversation( + The customer is created, or matched to an existing record, from customer_id and + any customer_support_platform_identifiers you supply. The dialled number is + recorded against that same customer.""" + return start_outbound_phone_conversation( client=self.http_client, params=params, ) diff --git a/tests/test_outbound_conversation_start.py b/tests/test_outbound_conversation_start.py new file mode 100644 index 0000000..510da2e --- /dev/null +++ b/tests/test_outbound_conversation_start.py @@ -0,0 +1,185 @@ +from unittest.mock import MagicMock + +from gradient_labs import ( + Client, + CustomerSupportPlatformIdentifier, + CustomerSupportPlatformIdentifierType, + OutboundSupportPlatform, + StartOutboundChatConversationParams, + StartOutboundConversationResponse, + StartOutboundEmailConversationParams, + StartOutboundPhoneConversationParams, + SupportPlatform, +) + + +def _client_returning(response: dict): + client = Client(api_key="test-key") + post = MagicMock(return_value=response) + client.http_client.post = post + return client, post + + +def test_start_outbound_chat_conversation(): + client, post = _client_returning({"conversation_id": "conv-123"}) + + rsp = client.start_outbound_chat_conversation( + params=StartOutboundChatConversationParams( + customer_id="cust-456", + procedure_id="procedure-789", + support_platform=OutboundSupportPlatform.INTERCOM, + ) + ) + + _, kwargs = post.call_args + body = kwargs["body"] + assert kwargs["path"] == "outbound/conversations/chat" + assert body == { + "customer_id": "cust-456", + "procedure_id": "procedure-789", + "support_platform": "intercom", + } + + assert isinstance(rsp, StartOutboundConversationResponse) + assert rsp.conversation_id == "conv-123" + + +def test_start_outbound_chat_conversation_with_optional_fields(): + client, post = _client_returning({"conversation_id": "conv-123"}) + + client.start_outbound_chat_conversation( + params=StartOutboundChatConversationParams( + customer_id="cust-456", + procedure_id="procedure-789", + support_platform=OutboundSupportPlatform.PUBLIC_API, + customer_support_platform_identifiers=[ + CustomerSupportPlatformIdentifier( + support_platform=SupportPlatform.INTERCOM, + type=CustomerSupportPlatformIdentifierType.INTERCOM_USER, + value="6953e162a988d9ef0f73ef9b", + ), + ], + body="Hi there!", + resources={"customer_profile": {"tier": "premium"}}, + ) + ) + + _, kwargs = post.call_args + body = kwargs["body"] + assert body["support_platform"] == "public-api" + assert body["body"] == "Hi there!" + assert body["resources"] == {"customer_profile": {"tier": "premium"}} + identifiers = body["customer_support_platform_identifiers"] + assert identifiers[0]["support_platform"] == "intercom" + assert identifiers[0]["type"] == "intercom_user" + assert identifiers[0]["value"] == "6953e162a988d9ef0f73ef9b" + + +def test_start_outbound_email_conversation(): + client, post = _client_returning({"conversation_id": "conv-123"}) + + rsp = client.start_outbound_email_conversation( + params=StartOutboundEmailConversationParams( + customer_id="cust-456", + procedure_id="procedure-789", + support_platform=OutboundSupportPlatform.ZENDESK, + customer_support_platform_identifiers=[ + CustomerSupportPlatformIdentifier( + support_platform=SupportPlatform.ZENDESK, + type=CustomerSupportPlatformIdentifierType.ZENDESK_SUPPORT_USER, + value="zd-42", + ), + ], + subject="Your order", + body="It has shipped.", + ) + ) + + _, kwargs = post.call_args + body = kwargs["body"] + assert kwargs["path"] == "outbound/conversations/email" + assert body["customer_id"] == "cust-456" + assert body["procedure_id"] == "procedure-789" + assert body["support_platform"] == "zendesk" + assert body["subject"] == "Your order" + assert body["body"] == "It has shipped." + identifiers = body["customer_support_platform_identifiers"] + assert identifiers[0]["support_platform"] == "zendesk" + assert identifiers[0]["type"] == "zendesk_support_user" + assert identifiers[0]["value"] == "zd-42" + + assert rsp.conversation_id == "conv-123" + + +def test_start_outbound_email_conversation_omits_unset_subject_and_body(): + client, post = _client_returning({"conversation_id": "conv-123"}) + + client.start_outbound_email_conversation( + params=StartOutboundEmailConversationParams( + customer_id="cust-456", + procedure_id="procedure-789", + support_platform=OutboundSupportPlatform.SALESFORCE, + ) + ) + + _, kwargs = post.call_args + body = kwargs["body"] + assert "subject" not in body + assert "body" not in body + assert "customer_support_platform_identifiers" not in body + assert "resources" not in body + + +def test_start_outbound_phone_conversation(): + client, post = _client_returning({"conversation_id": "conv-123"}) + + rsp = client.start_outbound_phone_conversation( + params=StartOutboundPhoneConversationParams( + customer_id="cust-456", + procedure_id="procedure-789", + to_phone_number="+14155551234", + from_phone_number="+14155559876", + ) + ) + + _, kwargs = post.call_args + body = kwargs["body"] + assert kwargs["path"] == "outbound/conversations/phone" + assert body == { + "customer_id": "cust-456", + "procedure_id": "procedure-789", + "to_phone_number": "+14155551234", + "from_phone_number": "+14155559876", + } + assert "support_platform" not in body + + assert rsp.conversation_id == "conv-123" + + +def test_start_outbound_phone_conversation_with_optional_fields(): + client, post = _client_returning({"conversation_id": "conv-123"}) + + client.start_outbound_phone_conversation( + params=StartOutboundPhoneConversationParams( + customer_id="cust-456", + procedure_id="procedure-789", + to_phone_number="+14155551234", + from_phone_number="+14155559876", + customer_support_platform_identifiers=[ + CustomerSupportPlatformIdentifier( + support_platform=SupportPlatform.SALESFORCE, + type=CustomerSupportPlatformIdentifierType.SALESFORCE_CONTACT_ID, + value="003xx000004TmiQAAS", + ), + ], + resources={"order": {"id": "ord-1"}}, + ) + ) + + _, kwargs = post.call_args + body = kwargs["body"] + assert body["resources"] == {"order": {"id": "ord-1"}} + identifiers = body["customer_support_platform_identifiers"] + assert identifiers[0]["support_platform"] == "salesforce" + assert identifiers[0]["type"] == "salesforce_contact_id" + assert identifiers[0]["value"] == "003xx000004TmiQAAS" From 604364a343402a0a07b9439b7d694f639f9a446c Mon Sep 17 00:00:00 2001 From: Arthur Ceccotti Date: Fri, 31 Jul 2026 14:21:23 +0100 Subject: [PATCH 2/2] [release] bump to 0.13.0 --- pyproject.toml | 2 +- src/gradient_labs/_http_client.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3a468d1..8e86cce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "gradient-labs" -version = "0.12.1" +version = "0.13.0" description = "Python bindings for the Gradient Labs API" readme = "README.md" requires-python = ">=3.9,<4.0" diff --git a/src/gradient_labs/_http_client.py b/src/gradient_labs/_http_client.py index f565a75..aaac264 100644 --- a/src/gradient_labs/_http_client.py +++ b/src/gradient_labs/_http_client.py @@ -7,7 +7,10 @@ from .errors import ResponseError API_BASE_URL = "https://api.gradient-labs.ai" -USER_AGENT = "Gradient Labs Python" + +# Keep VERSION in step with `version` in pyproject.toml. +VERSION = "0.13.0" +USER_AGENT = f"Gradient Labs Python/{VERSION}" class HttpClient: