Take an hour to build a deeply conversational chatbot for Intercom
Arthur Grishkevich
Posted on August 27, 2021
Co-authored by Aleksandr Aleksashin
Intercom lets businesses communicate with customers or prospective customers in-app or on-website. Intercom is used to initiate support conversations, as well as sales conversations. On the business side, Intercom is generally manned by an employee, although it lets users (businesses) create simple bots.
It’s a great technology. I think it can be made greater yet if one could automate the customer conversations that happen through it at a human level.
How Intercom does bots today
Intercom offers chatbot building functionality out of the box with a drag-and-drop, no code interface. These are rather simplistic command-response/multiple choice chat bots.
Here is what a conversation with an Intercom bot might look like today:
You might ask “what’s wrong with multiple choices”. They are great for taking the cream off the top. However, if a user’s request does not fit within the bounds of the commands served up by the bot, the user will quickly become frustrated and seek to speak with a human agent. The problem with human agents is that they are often unavailable or take a while to respond.
Importantly, according to Intercom’s own research only 15% of customers would prefer talking to a bot over a human agent.
So we got to thinking. What if we can deploy a conversational AI app that can communicate through Intercom and provide a truly human-like experience to the customer? How would customers feel about a thing like that? How would it impact their perception of the brand?
Also, we just think it’s a cool little project. Here is the repository with the source code for your reference.
Dasha AI
Dasha conversational AI platform is a suite of developer tools. Using Dasha Studio, SDK and Cloud Platform, any developer can design, build, train and deploy fully human-like, deeply conversational AI applications. DashaScript is a derivative of TypeScript, the SDK is Node.js. No ML or AI experience is required. It’s a perfect platform to create a human-like chatbot for Intercom.
Challenges to overcome
Here is what we need to make happen for this thing to start talking to customers:
Integrate a Dasha AI app with Intercom
Design and develop the Dasha conversational AI app
Deploy and test the app
In our journey we will create a simple conversational bot for customer support via Dasha platform. In terms of the conversational app functionality - we will keep it exceedingly simple for the purposes of this tutorial, yet will illustrate all the features of Dasha platform that you can use to create fully human-like conversational applications.
Before we go any further, let’s make sure that you are equipped with the tools to use Dasha conversational AI. Firstly, you’ll want to head over to our developer community, where you will get instructions on how to get your API key. Secondly, download and install the Dasha Studio extension for Visual Studio Code. Thirdly, you’ll want to install the Dasha CLI
npm i -g "@dasha.ai/cli"
. For more on our command line interface, read this post.
And finally, you want to clone and open a copy of the source code for the app we are building togay, so that you can follow along the instructions in the tutorial.
Integration
Our Dasha AI app has to be able to do the following things through Intercom:
- Be notified of newly-initiated conversations that have yet to be responded to by a human agent
- Read the content of the customer’s communication.
- Notify the customer that their message(s) have been read.
- Decipher the customer’s intent and select an appropriate response.
- Respond to the customer in the conversation.
To integrate, we will look at intercom API and search for a library that can perform the following actions:
- Search conversation by parameters;
- Reply to a specific conversation;
- Create/open/close/reopen conversations;
- Read messages from any given conversation.
Since clients for Dasha SDK are currently written only in JavaScript (TypeScript), we need a JavaScript library for Intercom. Intercom has a Node.js SDK in their API references (https://github.com/intercom/intercom-node). Unfortunately, this library cannot create new conversations.
Step one. Getting the access token for Intercom
First off we need to get acquainted with the Intercom API.
As we can see, all requests must have an access token. To get your access token, go here and to settings->developers->developer-hub and click “New app” to create a new application.
Once you create your application, head over to App > Configure > Authentication to find your access token.
Equipped with the access token, we can start sending requests to the Intercom rest API. For our HTTP client we will use Axios.
Step two. Apply integration to your code
We are using TypeScript to write our app. Open up main.ts. First, we need to import the intercom integration library.
import { IntercomClient } from "./src/intercomClient";
Then, we need to create an instance of a Dasha application and an instance of an intercom client. On the creation of the Intercom client instance we should pass the access token and url for Intercom as arguments of the function.
We are creating both within the function main
.
const application = await dasha.deploy("./graph", { groupName: "Default" });
const intercom = await IntercomClient.create("https://api.intercom.io", {
accessToken: process.env.INTERCOM_APIKEY!,
});
Next, we instruct the Dasha application how and where to execute the conversation and to let us know all is going well:
try {
application.connectionProvider = async (conv) =>
dasha.chat.connect(await dasha.chat.createConsoleChat());
application.setExternal("send_report_on_manager", (args, conv) => {
console.log({ set_args: args, conversation: conv });
return "ok";
});
Then we need to connect the Intercom client with the Dasha SDK application:
await intercom.simpleConnectToDashaApp(application);
And set a logger to log the actions.
intercom.setLogger(console);
Details of Intercom integration library
Function for connecting Dasha application to Intercom
We use the “simpleConnectToDashaApp” function in our code. Let’s take a look at its implementation. Go to .src/intercomClient.ts, line 412.
Look at the signature of this method. This method has one argument - an instance of platform application.
public async simpleConnectToDashaApp(
application: dasha.Application<
Record<string, unknown>,
Record<string, unknown>
>
): Promise<void>
In the first part of the function below we prepare to interact with the integration:
if (this.isSimpleUsed) {
return;
}
this.isSimpleUsed = true;
const closeTrigger = false;
const conversations = new Map<string, IntercomConversation>();
In the next part of the function, we set up a callback to become active on certain events in the application.
On the "starting" event, it is necessary to check that the incoming conversation has not been started yet. After this you need to execute the function for connecting intercom and application chats, and accept this job. Let’s check out the implementation of this callback:
const dashaChat = await dasha.chat.createChat();
const chat = await this.runChatInConversation(
conversations.get(conv.input.conversationId as string)!.id,
conversations.get(conv.input.conversationId as string)?.open ?? false,
{
closeAfter: {
messageOnClose:
"Conversation was closed because dialog was finished",
},
periodReading: 100,
startOptions: {
skipLastMessage: !conversations.get(
conv.input.conversationId as string
)?.open,
},
}
);
Third step is to connect the intercom client to Dasha application chat.
chat.run(dashaChat).catch(this._logger?.error);
return dasha.chat.connect(dashaChat);
};
application.queue.on("ready", async (id, conv) => {
this._logger?.info(`Got new starting conversation ('${id}').`);
// Checking that the conversation belongs to this intercom client instance
if (!conversations.has(id)) {
this._logger?.info(
`Try starting conversation from not this instance of intercom client new conversation ('${id}'). ` +
`Conversation was been rejected`
);
return;
}
this._logger?.info(`Prepare intercom conversation for chat ('${id}').`);
this._logger?.info(
`Connecting to dasha sdk chat protocol for conversation ('${id}').`
);
conv.input = {
conversationId: id,
continueConversation: conversations.get(id)?.open,
};
Fourth step is to reply to Dasha SDK that the conversation can be started.
conv.on("transcription", async (transcription) => {
if (this.onTransctiption) {
await this.onTransctiption(conversations.get(id)!, transcription);
}
});
this._logger?.info(`Conversation was been accepted ('${id}').`);
The events "failed", "completed", "timeOut" have the same implementation up to the call of the corresponding functions. Look at the implementation for conversation result
event:
const result = await conv.execute();
try {
this._logger?.info(`Conversation finish ('${id}').`);
if (this.onCompletedJob) {
await this.onCompletedJob(conversations.get(id)!, result);
}
} finally {
conversations.delete(id);
}
}
Similarly you will see functionality for rejected, timed out and failed conversations. At line 523 we begin a loop where we search for new conversations.
await this.watchConversations(
async (conv) => {
try {
// Checking that a conversation is already in progress
if (conversations.has(conv.id)) {
return;
}
this._logger?.info(`Got new conversation ('${conv.id}')`);
conversations.set(conv.id, conv);
const date = Date.now();
const notBefore = new Date(date);
const notAfter = new Date(date + 3600 * 1000);
await application.queue.push(conv.id);
this._logger?.info(
`Conversation ('${conv.id}') was enqueued into dasha sdk`
);
} catch {
this._logger?.error(
`Could not enqueue conversation ('${conv.id}') into dasha sdk`
);
}
},
() => {
return closeTrigger;
}
);
this.isSimpleUsed = false;
}
See how we change the state of the instance to start a new simple conversation with this.isSimpleUsed = false;
.
The function that lets us search ouyt a new conversation
We use the “watchConversations” function in our code. Let’s see their implementation. Write simple checking conversations to have a new one or a not replied one.
public async watchConversations(
// We need callback to start work with conversation
callback: (conv: IntercomConversation) => Promise<void>,
// Hook, for end watching conversations
stopTrigger: () => boolean
): Promise<void> {
// Search over all conversation and find only those which have not replied message
while (!stopTrigger()) {
// take first 20 conversation from first page in listing method
let conversation = await this.getConversation(20, 1);
// skip all conversation which was been replied by admin
for (const conv of conversation.conversations.filter(
(x) =>
x.statistics.last_contact_reply_at > x.statistics.last_admin_reply_at
)) {
// send new potential conversation for working
callback(conv);
}
// now we go over all other conversation and do same action like above
while (conversation.pages.total_pages >= conversation.pages.page) {
conversation = await this.getConversation(
20,
conversation.pages.page + 1
);
for (const conv of conversation.conversations.filter(
(x) =>
x.statistics.last_contact_reply_at >
x.statistics.last_admin_reply_at
)) {
callback(conv);
}
}
// timeout in order not to spam requests
await new Promise((resolve, reject) => setTimeout(resolve, 1000));
}
}
Now we can work with applications and conversations, but we cannot start conversations.
Function for start chatting
These methods prepare Intercom for conversation and to start a chat. This function has two logical parts. In part one, we prepare the client for the conversation:
public async runChatInConversation(
conversationId: string,
// is the conversation open at the moment
conversationWasOpen: boolean,
options: {
// if define than conversation after and need close
// else conversation does not closing
closeAfter?: {
messageOnClose: string;
};
// if nod defined than used first available user message in conversation
// else use last available user message in conversation
startOptions?: {
// ignore last message, and awaiting new message
skipLastMessage: boolean;
};
// period of receiving new messages from the api intercom
periodReading: number;
}
): Promise<IIntercomChat> {
// Create client for each conversation
const intercomConversationClient = new IntercomConversationClient(
this,
conversationId,
options.startOptions === undefined
? 0
: await this.getLastTime(
conversationId,
options.startOptions.skipLastMessage
),
options.periodReading
);
In next part we use a hook to start the conversation:
return {
// hook which was triggered when conversation was started on the dasha platform.
run: async (chatChannel: dasha.chat.Chat): Promise<void> => {
try {
// open conversation if it`s needed
if (!conversationWasOpen) {
await this.openConversation(conversationId);
}
// run conversation chat
await intercomConversationClient.run(chatChannel);
} finally {
// close conversation if it`s need
if (options.closeAfter !== undefined) {
try {
await this.closeConversation(
conversationId,
options.closeAfter.messageOnClose
);
} catch (e) {
this._logger?.error(JSON.stringify(e));
}
}
}
},
};
}
Implementing a class to chat within a single conversation.
This is the magic class which lets the Dasha conversational AI chat with Intercom. Go to intercomConversationClient.ts.
Let’s take a look at the construction of this class.
export class IntercomConversationClient implements IIntercomChat {
// Id of conversation on Intercom.
public readonly conversationId: string;
// Used Intercom client.
private readonly client: IIntercomClient;
// Delay for reading new messages from Intercom conversation.
private readonly delayReading: number;
// This property indicate that this client in a close state.
private close = false;
//Creation time of the last message we read from Intercom conversation.
private lastTime = 0;
public constructor(client: IIntercomClient, conversationId: string, lastTime: number, delayReading: number) {
this.client = client;
this.conversationId = conversationId;
this.lastTime = lastTime;
this.delayReading = delayReading;
}
This class should have two functions. The first of them needs to run and control the conversation flow:
public async run(chatChannel: dasha.chat.Chat): Promise<void> {
// listen message from dasha sdk application and write into intercom
chatChannel.on("text",async(text)=>{
await this.client.reply(this.conversationId, text);
});
chatChannel.on("close",()=>{
this.close = true;
});
chatChannel.on("error",(error)=>{
this.close = true;
console.warn("chat error:", error);
});
// listen message from intercom and write into dasha sdk chat channel
await this.readConversation(chatChannel);
}
The next function reads messages from the conversation and sends them to the Dasha application.
private async readConversation(chatChannel: dasha.chat.Chat): Promise<void> {
while (!this.close) {
await new Promise((resolve, _) => setTimeout(resolve, this.delayReading));
// read new messages in intercom conversation
const obj = await this.client.readMessages(this.conversationId, this.lastTime);
// mark conversation as read. This see user in intercom
await this.client.markAsRead(this.conversationId);
this.lastTime = obj.lastTime;
// send new message from intercom into dasha sdk application
for (const msg of obj.messages) {
await chatChannel.sendText(msg);
}
}
}
}
Quite simple.
The Dasha conversational AI app
On to the fun part. Let’s look at the conversational AI app that will interact with our Intercom customer/user.
You will need to open the following files:
- .graph/main.dsl is the DashaScript file which you use to construct the conversational pathway that Dasha is to take. Here you can make your conversation as absolutely complex or simple as you wish. For the purposes of this tutorial we will opt for simple.
- data.json is the file where you store data that is used to train the neural networks to properly understand the customer’s/user’s intent.
-
phrasemap.json is where you store all the phrases to be used by Dasha AI in the context of the conversation. (tip: you can list phrases directly in main.dsl by using the
#sayText()
function. Using the phrase map for it is, however, the better way for scalable apps.)
We will be discussing the following concepts in this part of the tutorial:
- Node is literally just that - a node in the conversation. It’s a point of interaction between the user and the AI. A node must always have another node or digression leading into it.
- Digression is, in essence, a node that does not have to have anything lead into it. A digression can be called up by the user at any point in the conversation. Digressions are used to imitate the way a human might adapt when the person they are talking to suddenly throws them off on a tangent. Read this for a deeper dive on digressions.
- Intent refers to the meaning behind the user/customer’s phrases. When you are speaking to someone you figure out the meaning of their words and phrases and sentences based on context, a variety of clues and previous experience. Right now Dasha AI interprets meaning solely from text (we are working on emotional analysis). Intents refer to the . The Dasha Cloud Platform has neural networks dedicated to training for user intents and to analysing said intents in the course of a conversation. Take a look here for more on intents.
- Named entities are fields referring to names, place names, objects, etc. that you need Dasha AI to identify within the customer/user’s responses. Named entities are also processed by neural networks and you provide training data using data.json in a format similar to intent training data. For more on named entities you can read this piece from the leader of the team which constructed the neural networks.
Let’s look at main.dsl. We start by importing common functions. In the case of Dasha this means common digressions and ways of responding to them.
import "commonReactions/all.dsl";
Then we need to declare some variables for context:
context
{
input conversationId: string;
input continueConversation: boolean;
last_message: string = "";
can_say: boolean = false;
output status: string?;
output serviceStatus: string?;
}
We also need to declare an external function. This is a way for a DashaScript app to call out to an external application to send or receive data. In this case the app is main.ts and we are sending the customer’s phrase data which refers to a manager who led to a dissatisfied customer.
external function send_report_on_manager(report: string): unknown;
Let’s look at the first node - node root
.
start node root
{
do
{
#connectSafe("");
if(!$continueConversation)
{
#say("greeting", repeatMode: "ignore");
#say("how_can_i_help_you");
}
goto hub;
}
transitions
{
hub: goto hub;
}
}
Here we connect to the chat instance if check if our conversation should be continued, as per the code we wrote previously in the integration. If we continue the conversation, we greet the customer and ask how they can be helped.
It is expected that in most cases the customer will state the problem with which they are calling.
Here is where digressions come in. Let’s say the customer says “I have a problem with my phone.” If you look at the intents.json, you will find a "problem_with_phone"
intent on line 98:
"problem_with_phone": {
"includes": [
"I can't seem to make a phone call",
"I have a problem with my phone",
"I'm afraid I broke my phone",
"Phone problem"
],
If you scroll down to line 144 in main.dsl, you will see this digression:
digression problem_dig
{
conditions
{
on #messageHasAnyIntent(["problem_with_phone", "problem_with_manager", "have_problem"]);
}
do
{
digression disable problem_dig;
goto problem;
}
transitions
{
problem: goto problem;
}
}
The digression comes into play when any of the intents "problem_with_phone", "problem_with_manager", "have_problem"
are identified. If they are identified, the conversation goes to node problem
.
There are a variety of exciting funcitons of DashaScript that you should learn about if you want to develop further on it. The best place to go is our documentation. You can also make your way through a detailed tutorial, such as this one.
Launching and testing the Intercom conversation
Now that you’ve got the integration built out and the app is developed as well, let’s launch a test convo.
To do this, type npm i
in the terminal and then ’npm run build` and ’npm run start`. The app should now be deployed.
Now, you need to launch a test conversation with Dasha. It’s a two step process. Step 1 - head over to the Intercom Developer Hub > Your apps. Select your app and click on it. Your address bar will read something of this sort:
You want to copy the number after /apps - this is your Intercom application ID.
Now, copy and paste this line into your address bar https://app.intercom.com/a/apps/yourAppIDgoesHere/test-conversation and replace yourAppIDgoesHere with your application ID. I know, it’s complicated and, yes this is the solution recommended by Intercom.
Now, you will be able to send a message from the chat interface and get a reply from Dasha. In this way you can test any conversation that your users can have with Dasha.
Let me know if this was helpful in the comments or in our developer community .
Posted on August 27, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 30, 2024
November 30, 2024