{"id":4098,"date":"2017-06-30T19:34:12","date_gmt":"2017-06-30T19:34:12","guid":{"rendered":"https:\/\/www.microsoft.com\/reallifecode\/?p=4098"},"modified":"2020-03-14T20:27:30","modified_gmt":"2020-03-15T03:27:30","slug":"bot-to-human-handover-in-node-js","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/ise\/bot-to-human-handover-in-node-js\/","title":{"rendered":"Bot to Human Handoff in Node.js"},"content":{"rendered":"<p>One common request from companies and organizations considering bots is the ability to &#8220;hand off&#8221; a customer from a bot to a human agent as seamlessly as possible. To solve this problem, we implemented an unopinionated e-2-e solution\u00a0called <a href=\"https:\/\/github.com\/palindromed\/Bot-HandOff\/tree\/npm-handoff\"><strong>Handoff<\/strong><\/a>, which enables bot authors to implement a wide variety of scenarios with minimal changes to the actual bot. We recently worked with a partner who wanted to use the <a href=\"https:\/\/github.com\/CatalystCode\/ibex-dashboard\">Ibex dashboard<\/a> to display all the Handoff interactions.\u00a0A scenario we see often is a is\u00a0help\u00a0desk application; although little changes are needed to implement Handoff in your bot, creating your own help centre application that works with Handoff would require a bit of work.<\/p>\n<p>This framework has been developed in TypeScript and Node.js, and a\u00a0similar handoff scenario has also been developed in C# by Tomi Paananen. You can see his <a href=\"http:\/\/tomipaananen.azurewebsites.net\/?p=1851\">write-up of how the C# version works<\/a> and his <a href=\"https:\/\/github.com\/tompaana\/intermediator-bot-sample\">Intermediator-Bot sample on GitHub<\/a>.<\/p>\n<p>This project, <a href=\"https:\/\/github.com\/palindromed\/Bot-HandOff\/tree\/npm-handoff\">Bot-Handoff<\/a>, is open-sourced and available on GitHub. Contributions are always welcome!<!--more--><\/p>\n<h2><a name=\"how-it-works\"><\/a>How It Works<\/h2>\n<p>This framework connects <strong>Customers<\/strong> and <strong>Agents<\/strong> through the <a href=\"https:\/\/dev.botframework.com\">Microsoft Bot Framework<\/a>.<\/p>\n<p>The Handoff depends on a database of conversations, including a transcription of every message sent between the Customer and the bot, or between the Customer and the Agent.<\/p>\n<p>A Handoff conversation consists of:<\/p>\n<ul>\n<li>address information for this conversation with the Customer<\/li>\n<li>the current state of this conversation (Bot, Waiting, or Agent)<\/li>\n<li>the conversational transcript<\/li>\n<li>address information for the Agent, if the Agent is currently connected to this Customer\u00a0(Handoff does not record conversational metadata for the Agent, except when they are connected to a Customer)<\/li>\n<\/ul>\n<p>Essentially, we have some middleware intercepting every message sent to and from the bot. If the message is from the bot, Customer, or an Agent who is speaking to a Customer, it transcribes the message before sending the message through.<\/p>\n<p>If the message is from the Customer and contains the text\u00a0&#8220;help me&#8221; (a command that can be configured by the author of the bot) we update the state of the Customer from &#8216;Bot&#8217; to &#8216;Waiting&#8217;\u00a0meaning the user now waits for an Agent.<\/p>\n<p>Once an Agent picks up the conversation, the state is changed from &#8216;Waiting&#8217; to &#8216;Agent&#8217;. In this state, any message sent from the Customer is intercepted by the middleware and routed to the Agent using the stored address information for the Agent that picked up the conversation, and vice versa.<\/p>\n<p>This code story will go into further detail about key aspects of this framework, including:<\/p>\n<ul>\n<li><a href=\"#middleware\">Middleware Routing<\/a><\/li>\n<li><a href=\"#transcript\">Transcripts and Logging<\/a><\/li>\n<li><a href=\"#commands\">Commands<\/a><\/li>\n<li><a href=\"#mongodb\">Storing State on MongoDB<\/a><\/li>\n<li><a href=\"#app-insights\">Application Insights<\/a><\/li>\n<li><a href=\"#sentiment\">Sentiment Analysis<\/a><\/li>\n<li><a href=\"#force-handoff\">Force Handoff API<\/a><\/li>\n<li><a href=\"#npm\">Using the npm package in your\u00a0bot<\/a><\/li>\n<li><a href=\"#conclusion\">Conclusion and resources<\/a><\/li>\n<\/ul>\n<p><!--more--><\/p>\n<h2><a name=\"middleware\"><\/a>Middleware Routing<\/h2>\n<p>The heart of Handoff is the message router. Using the conversational metadata above, each message from a Customer, Bot, or Agent is routed appropriately. It is implemented as Bot middleware, and can be combined with any other middleware your bot is already using.<\/p>\n<p> <img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/cse\/wp-content\/uploads\/sites\/55\/2017\/06\/drawit-diagram-5-1.png\" alt=\"Image drawit diagram 5 1\" width=\"587\" height=\"826\" class=\"aligncenter size-full wp-image-10906\" srcset=\"https:\/\/devblogs.microsoft.com\/ise\/wp-content\/uploads\/sites\/55\/2017\/06\/drawit-diagram-5-1.png 587w, https:\/\/devblogs.microsoft.com\/ise\/wp-content\/uploads\/sites\/55\/2017\/06\/drawit-diagram-5-1-213x300.png 213w\" sizes=\"(max-width: 587px) 100vw, 587px\" \/><\/p>\n<p>The code snippet below shows the implementation of\u00a0 <span class=\"lang:default decode:true crayon-inline \">bot.use()<\/span>\u00a0 which is how middleware is implemented in the Node.js Microsoft Bot Framework SDK. The middleware performs two key actions: first, it checks to see if what the user enters matches any of our commands. For example, if the user is an Agent and types <em>&#8220;list&#8221;,<\/em> it will list all of the current conversations with the bot. Next, the middleware routes the message:<\/p>\n<pre class=\"lang:js decode:true\">bot.use(\r\n    commandsMiddleware(handoff),\r\n    handoff.routingMiddleware(),\r\n)<\/pre>\n<p>If the activity\u00a0is from the Customer or Agent, it will go through the <span class=\"lang:default decode:true crayon-inline \">botbuilder<\/span>\u00a0 method. We only want to transcribe activities of type <em>&#8220;message&#8221;<\/em> so only in that scenario do we call <span class=\"lang:default decode:true crayon-inline\">routeMessage()<\/span>\u00a0; otherwise, we just pass the activity\u00a0through as usual.\u00a0Activities from the bot go through the send method where we do something similar.<\/p>\n<p>Here we also check to make sure there are no entities in the message activity. If there is an entity, \u00a0it means that the message is from the Agent acting as the bot. We don&#8217;t want the message to send twice as it will already be routed through the <span class=\"lang:default decode:true crayon-inline\">botbuilder<\/span>\u00a0 method. (When the Handoff is active, the Agent is speaking to the bot to speak to the Customer, and that same message is then routed as the bot.)<\/p>\n<pre class=\"lang:js decode:true\">public routingMiddleware() {\r\n    return {\r\n        botbuilder: (session: builder.Session, next: Function) =&gt; {\r\n            \/\/ Pass incoming messages to routing method\r\n            if (session.message.type === 'message') {\r\n                this.routeMessage(session, next);\r\n            } else {\r\n                \/\/ allow messages of non 'message' type through \r\n                next();\r\n            }\r\n        },\r\n        send: async (event: builder.IMessage, next: Function) =&gt; {\r\n            \/\/ Messages sent from the bot do not need to be routed\r\n            \/\/ Not all messages from the bot are type message, we only want to record the actual messages  \r\n            if (event.type === 'message' &amp;&amp; !event.entities) {\r\n                this.transcribeMessageFromBot(event as builder.IMessage, next);\r\n            } else {\r\n                \/\/If not a message (text), just send to user without transcribing\r\n                next();\r\n            }\r\n        }\r\n    }\r\n}<\/pre>\n<p>After getting a message activity from a Customer or Agent, we use <span class=\"lang:default decode:true crayon-inline \">routeMessage()<\/span>\u00a0 to check who the user is.<\/p>\n<pre class=\"lang:js decode:true\">private routeMessage(session: builder.Session, next: Function) {\r\n    if (this.isAgent(session)) {\r\n        this.routeAgentMessage(session)\r\n    } else {\r\n        this.routeCustomerMessage(session, next);\r\n    }\r\n}<\/pre>\n<h4>Agent Recognition<\/h4>\n<p>Customers and Agents are both just users connected to bots, so Handoff needs a way to identify an Agent.\u00a0We use a function of the form\u00a0<span class=\"lang:default decode:true crayon-inline\">isAgent(session: Session) =&gt; boolean<\/span>\u00a0 to determine if the user is an Agent, which can be customized by the author of the bot\u00a0in the initial setup of the Handoff module. For example, in the snippet below, we determine that a user is an Agent if their username starts with <em>&#8220;Agent&#8221;.<\/em><\/p>\n<p><span class=\"lang:js decode:true crayon-inline \">const isAgent = (session) =&gt; session.message.user.name.startsWith(&#8220;Agent&#8221;);<\/span><\/p>\n<p>This information is passed by the bot author\u00a0in the initial setup\u00a0<span class=\"lang:default decode:true crayon-inline\">handoff.setup(bot, app, isAgent, { })<\/span>\u00a0.<\/p>\n<p>There are multiple ways this could be set up:<\/p>\n<ul>\n<li>Create a hardcoded directory of channel-specific user IDs for Agents (e.g., &#8220;Fred Doe on Facebook Messenger is one of our Agents&#8221;)<\/li>\n<li>Create a WebChat-based call center app that specially encodes Agent user IDs (e.g., &#8220;Agent001&#8221;, &#8220;Agent002&#8221;), which is easy with WebChat<\/li>\n<li>Create a WebChat-based call center app that authenticates users and then passes auth tokens to the bot via the WebChat backchannel<\/li>\n<li>Use authbot to identify the user as an Agent via OAuth2 (e.g., &#8220;This authenticated user is marked as an Agent in our employee database&#8221;)<\/li>\n<\/ul>\n<h2><a name=\"transcript\"><\/a>Transcripts and Logging<\/h2>\n<p>Once we know who the user is, several checks are made before sending the message through the bot. In the Agent routing method, we check to see if the Agent is in a conversation; this means checking if they are currently talking to a Customer. If not, we don&#8217;t need further routing and we just pass the message through. Otherwise, if they are talking to a Customer, we route the message through the bot to the Customer they are in the conversation with.<\/p>\n<p> <img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/cse\/wp-content\/uploads\/sites\/55\/2017\/06\/drawit-diagram-6.png\" alt=\"Image drawit diagram 6\" width=\"271\" height=\"336\" class=\"aligncenter size-full wp-image-10907\" srcset=\"https:\/\/devblogs.microsoft.com\/ise\/wp-content\/uploads\/sites\/55\/2017\/06\/drawit-diagram-6.png 271w, https:\/\/devblogs.microsoft.com\/ise\/wp-content\/uploads\/sites\/55\/2017\/06\/drawit-diagram-6-242x300.png 242w\" sizes=\"(max-width: 271px) 100vw, 271px\" \/><\/p>\n<pre class=\"lang:js decode:true\">const conversation = await this.getConversation({ AgentConversationId }, message.address);\r\n\r\n\/\/ send text that Agent typed to the Customer they are in conversation with\r\nthis.bot.send(new builder.Message().address(conversation.Customer).text(message.text).addEntity({ \"Agent\": true }));\r\n<\/pre>\n<p>On the Customer side, we check their state. If they are taking to the bot, we just pass the message through. If they are waiting, we inform them they are being connected to an Agent. And finally, if they are in conversation with an Agent (and the Agent is in the conversation) we route the message through the bot to the Agent.<\/p>\n<p> <img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/cse\/wp-content\/uploads\/sites\/55\/2017\/06\/drawit-diagram-7.png\" alt=\"Image drawit diagram 7\" width=\"350\" height=\"540\" class=\"aligncenter size-full wp-image-10908\" srcset=\"https:\/\/devblogs.microsoft.com\/ise\/wp-content\/uploads\/sites\/55\/2017\/06\/drawit-diagram-7.png 350w, https:\/\/devblogs.microsoft.com\/ise\/wp-content\/uploads\/sites\/55\/2017\/06\/drawit-diagram-7-194x300.png 194w\" sizes=\"(max-width: 350px) 100vw, 350px\" \/><\/p>\n<pre class=\"lang:js decode:true\">\/\/ this method will either return existing conversation or a newly created conversation if this is first time we've heard from Customer\r\nconst conversation = await this.getConversation({ CustomerConversationId: message.address.conversation.id }, message.address);\r\n\r\nthis.bot.send(new builder.Message().address(conversation.Agent).text(message.text));<\/pre>\n<p>For both scenarios, we update the conversation&#8217;s transcript array, using the conversation ID, so we have all the messages that have gone between the Customer and the bot, and the Customer and the Agent.<\/p>\n<pre class=\"lang:js decode:true\">conversation.transcript.push({\r\n    timestamp: datetime,\r\n    from: from,\r\n    sentimentScore: sentimentScore,\r\n    state: conversation.state,\r\n    text\r\n});<\/pre>\n<p>The datetime variable used is of type\u00a0<span class=\"lang:default decode:true crayon-inline\">new Date().toISOString()<\/span>\u00a0so that it matches the format returned from <a href=\"https:\/\/docs.botframework.com\/en-us\/node\/builder\/chat-reference\/classes\/_botbuilder_d_.message.html#localtimestamp\"><span class=\"lang:default decode:true crayon-inline\">localTimestamp<\/span><\/a>\u00a0.\u00a0Something worth noting is that <span class=\"lang:default decode:true crayon-inline \">localTimestamp<\/span>\u00a0\u00a0(a\u00a0method\u00a0returned with a message activity) is not provided when the message is coming from WebChat. The SDK recommends not using the <a href=\"https:\/\/docs.botframework.com\/en-us\/node\/builder\/chat-reference\/classes\/_botbuilder_d_.message.html#timestamp\"><span class=\"lang:default decode:true crayon-inline\">timestamp<\/span><\/a>\u00a0\u00a0value, \u00a0so we put a check in place to use <span class=\"lang:default decode:true crayon-inline \">localTimestamp<\/span>\u00a0 where possible.<\/p>\n<div>\n<pre class=\"lang:default decode:true\">datetime = message.localTimestamp ? message.localTimestamp : message.timestamp<\/pre>\n<\/div>\n<h2><a name=\"commands\"><\/a>Commands<\/h2>\n<p>As mentioned earlier, the middleware of the bot looks for commands in all the messages sent to the bot. There are several commands made available through the <a href=\"https:\/\/github.com\/palindromed\/Bot-HandOff\/blob\/npm-handoff\/src\/commands.ts\"><span class=\"lang:default decode:true crayon-inline\">commands.ts<\/span><\/a>\u00a0 middleware feature.\u00a0After checking to make sure the activity is of the message type, we check if this command is coming from\u00a0the Agent or from the Customer in a similar fashion as <span class=\"lang:default decode:true crayon-inline\">routeMessage()<\/span>\u00a0using <span class=\"lang:default decode:true crayon-inline\">isAgent<\/span>\u00a0.<\/p>\n<p>On the <strong>Agent<\/strong> side, there are four\u00a0commands available:<\/p>\n<table>\n<tbody>\n<tr>\n<th>Command<\/th>\n<th>Description<\/th>\n<\/tr>\n<tr>\n<td>options<\/td>\n<td>Displays all of the commands available to the Agent.<\/td>\n<\/tr>\n<tr>\n<td>list<\/td>\n<td>Lists all of the conversations with the bot, using the conversation data in MongoDB<\/td>\n<\/tr>\n<tr>\n<td>connect<\/td>\n<td>This is the main command that activates the Handoff, taking the Customer from Waiting (1) state to Agent (2) state. After this state change, all messages the Customer sends to the bot, and all messages the Agent sends to the bot, are routed using the address information sorted for that conversation.<\/td>\n<\/tr>\n<tr>\n<td>disconnect<\/td>\n<td>This command disconnects\u00a0the Agent from the Customer, resetting the Customer&#8217;s state to Bot (0). If the bot author chooses, they can keep all of the conversation data by providing a <span class=\"lang:default decode:true crayon-inline \">retainData<\/span>\u00a0 option\/environment variable. Otherwise, if the user has spoken to an Agent and the Agent has disconnected, the conversation is deleted from the database.<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>On the <strong>Customer<\/strong> side, there is only one command. The bot author can provide a custom keyword or phrase that a Customer can use to trigger the Handoff (the default is set to &#8220;<em>help<\/em>&#8220;) through an environment variable or the Handoff module initial setup options.<\/p>\n<p>Regex is used to make sure the text is an exact match:<\/p>\n<pre class=\"lang:js decode:true\">const customerStartHandoffCommandRegex = new RegExp(\"^\" + indexExports._customerStartHandoffCommand + \"$\", \"gi\");<\/pre>\n<p>If the text matches exactly, the user&#8217;s message is transcribed, and they are queued to speak to an agent, changing their state from 0 (Bot) to 1 (Waiting).<\/p>\n<h2><a name=\"mongodb\"><\/a>Storing State on MongoDB<\/h2>\n<p>The\u00a0conversation data, which consists of the items listed below, are all stored in a Mongo database:<\/p>\n<ul>\n<li>address information for this conversation with the Customer<\/li>\n<li>the current state of this conversation (Bot, Waiting, or Agent)<\/li>\n<li>the conversational transcript<\/li>\n<li>address information for the Agent, if the Agent is currently connected to this Customer\u00a0(Handoff does not record conversational metadata for the Agent, except when they are connected to a Customer)<\/li>\n<\/ul>\n<p>The <a href=\"https:\/\/www.npmjs.com\/package\/mongoose\">Mongoose npm module<\/a> was used to enable this feature; the bot author simply has to provide the\u00a0MongoDB connection string as an option or environment variable. Another option is to use <a href=\"https:\/\/azure.microsoft.com\/en-us\/services\/cosmos-db\/\">Azure Cosmos DB<\/a>\u00a0which\u00a0automatically indexes all data and allows you to use the\u00a0MongoDB API. You can learn more from this blog post on\u00a0<a href=\"https:\/\/docs.microsoft.com\/en-us\/azure\/cosmos-db\/connect-mongodb-account\">how to connect a MongoDB application to CosmosDB<\/a>.<\/p>\n<p><a href=\"https:\/\/github.com\/palindromed\/Bot-HandOff\/blob\/npm-handoff\/src\/mongoose-provider.ts\"><span class=\"lang:default decode:true crayon-inline\">mongoose-provider.ts<\/span><\/a>\u00a0 handles all of the database interactions, and is where all of the schemas are described and, through a series of promises, the conversation is created, deleted, and updated. \u00a0For example:<\/p>\n<pre class=\"lang:js decode:true\">export interface ConversationDocument extends Conversation, mongoose.Document { }\r\n\r\nexport const ConversationModel = mongoose.model&lt;ConversationDocument&gt;('Conversation', ConversationSchema)\r\n\r\nprivate async updateConversation(conversation: Conversation): Promise&lt;boolean&gt; {\r\n    return new Promise&lt;boolean&gt;((resolve, reject) =&gt; {\r\n        ConversationModel.findByIdAndUpdate((conversation as any)._id, conversation).then((error) =&gt; {\r\n            resolve(true)\r\n        }).catch((error) =&gt; {\r\n            console.log('Failed to update conversation');\r\n            console.log(conversation as any);\r\n            resolve(false);\r\n        });\r\n    });\r\n}<\/pre>\n<h2><a name=\"app-insights\"><\/a>Application Insights<\/h2>\n<p>The need to record all the conversation details in <a href=\"https:\/\/azure.microsoft.com\/en-us\/services\/application-insights\/\">Azure Application Insights<\/a> came from a hackfest we did with a partner. They wanted to use the<a href=\"https:\/\/github.com\/CatalystCode\/ibex-dashboard\"> Ibex dashboard<\/a> to display all the Handoff interactions.<\/p>\n<p>We implemented Application Insights as an optional feature by only logging the data to Application Insights if they provide the Application Insights instrumentation key in the initial setup of the handoff module, or through an environment variable.<\/p>\n<p>We used the <a href=\"https:\/\/www.npmjs.com\/package\/applicationinsights\">Application Insights npm module<\/a> to set up and start\u00a0an Application Insights client. This client is then used to track an event under the title <em>&#8220;Transcript&#8221;<\/em>.<\/p>\n<pre class=\"lang:js decode:true\">let appInsights = require('applicationinsights');\r\n\r\nappInsights.setup(_appInsightsInstrumentationKey).start();\r\nexports._appInsights = appInsights;<\/pre>\n<p>The Application Insights logging happens in the <span class=\"lang:default decode:true crayon-inline\">addToTranscript()<\/span>\u00a0\u00a0method. Two things worth noting here:<\/p>\n<ol>\n<li>You can&#8217;t log embedded JSON\u00a0objects in Application Insights, so we flatten the object to one item.<\/li>\n<li>We had to first stringify the object we got from MongoDB before parsing it so that functions from MongoDB don&#8217;t get logged\u00a0<span class=\"lang:default decode:true crayon-inline\">JSON.parse(JSON.stringify(OBJECT))<\/span>.<\/li>\n<\/ol>\n<pre class=\"lang:js decode:true\">if (indexExports._appInsights) {   \r\n    let latestTranscriptItem = conversation.transcript.length-1;\r\n    let x = JSON.parse(JSON.stringify(conversation.transcript[latestTranscriptItem]));\r\n    x['botId'] = conversation.Customer.bot.id;\r\n    x['CustomerId'] = conversation.Customer.user.id;\r\n    x['CustomerName'] = conversation.Customer.user.name;\r\n    x['CustomerChannelId'] = conversation.Customer.channelId;\r\n    x['CustomerConversationId'] = conversation.Customer.conversation.id;\r\n    if (conversation.Agent) {\r\n        x['AgentId'] = conversation.Agent.user.id;\r\n        x['AgentName'] = conversation.Agent.user.name;\r\n        x['AgentChannelId'] = conversation.Agent.channelId;\r\n        x['AgentConversationId'] = conversation.Agent.conversation.id;\r\n    }\r\n    indexExports._appInsights.client.trackEvent(\"Transcript\", x);    \r\n}<\/pre>\n<h2><a name=\"sentiment\"><\/a>Sentiment Analysis<\/h2>\n<p>We wanted to give bot authors the option to also log the sentiment score of messages from the Customer using <a href=\"https:\/\/azure.microsoft.com\/en-us\/services\/cognitive-services\/text-analytics\/\">Microsoft Cognitive Services Text Analytics<\/a>; this API returns a value between 0 and 1 where 1 is a very positive sentiment and 0 is a very negative sentiment. If the user is not the Customer, or they do not provided an API key, the default sentiment score is set to -1.<\/p>\n<pre class=\"lang:js decode:true\">let sentimentScore = -1;\r\n\r\n...\r\n\r\nif (from == \"Customer\") {\r\n    if (indexExports._textAnalyticsKey) { \r\n        sentimentScore = await this.collectSentiment(text); \r\n    }\r\n}\r\n\r\n...\r\n\r\nconversation.transcript.push({\r\n    timestamp: datetime,\r\n    from: from,\r\n    sentimentScore: sentimentScore,\r\n    state: conversation.state,\r\n    text\r\n});<\/pre>\n<p>We get the sentiment score by sending the user&#8217;s text to the Text Analytics API.<\/p>\n<pre class=\"lang:js decode:true\">private async collectSentiment(text: string): Promise&lt;number&gt; {\r\n    if (text == null || text == '') return;\r\n    let _sentimentUrl = 'https:\/\/westus.api.cognitive.microsoft.com\/text\/analytics\/v2.0\/sentiment';\r\n    let _sentimentId = 'bot-analytics';\r\n    let _sentimentKey = indexExports._textAnalyticsKey;\r\n\r\n    let options = {\r\n        url: _sentimentUrl,\r\n        method: 'POST',\r\n        headers: {\r\n            'Content-Type': 'application\/json',\r\n            'Ocp-Apim-Subscription-Key': _sentimentKey\r\n        },\r\n        json: true,\r\n        body: {\r\n            \"documents\": [\r\n                {\r\n                    \"language\": \"en\",\r\n                    \"id\": _sentimentId,\r\n                    \"text\": text\r\n                }\r\n            ]\r\n        }\r\n    };\r\n\r\n    return new Promise&lt;number&gt;(function (resolve, reject) {\r\n        request(options, (error, response, body) =&gt; {\r\n            if (error) { reject(error); }\r\n            let result: any = _.find(body.documents, { id: _sentimentId }) || {};\r\n            let score = result.score || null;\r\n            resolve(score);\r\n        });\r\n    });\r\n}<\/pre>\n<h2><a name=\"force-handoff\"><\/a>Force Handoff via API<\/h2>\n<p>To provide additional means of controlling and customizing the Handoff experience, an endpoint (<span class=\"lang:default decode:true crayon-inline\">POST<\/span>) is available to queue a Customer to speak to an Agent using the Customer&#8217;s conversation ID. \u00a0This API useful for a partner who wanted to use their own system to view all current conversations happening with a bot, and trigger the handoff manually with a button click, for any given conversation.<\/p>\n<pre class=\"lang:js decode:true\">app.post('\/api\/conversations', async (req, res) =&gt; {\r\n    const authHeader = req.headers['authorization'];\r\n    if (authHeader) {\r\n        if (authHeader === 'Bearer ' + _directLineSecret) {\r\n            if (await handoff.queueCustomerForAgent({ customerConversationId: req.body.conversationId })) {\r\n                res.status(200).send({ \"code\": 200, \"message\": \"OK\" });\r\n            } else {\r\n                res.status(400).send({ \"code\": 400, \"message\": \"Can't find conversation ID\" });\r\n            }\r\n        }\r\n    } else {\r\n        res.status(401).send({ \"code\": 401, \"message\": \"Not Authorized\" });\r\n    }\r\n});<\/pre>\n<p>You can use the <span class=\"lang:default decode:true crayon-inline \">GET<\/span>\u00a0endpoint to retrieve a list of all current conversations:<\/p>\n<pre class=\"lang:js decode:true\">app.get('\/api\/conversations', async (req, res) =&gt; {\r\n    const authHeader = req.headers['authorization'];\r\n    console.log(authHeader);\r\n    console.log(req.headers);\r\n    if (authHeader) {\r\n        if (authHeader === 'Bearer ' + _directLineSecret) {\r\n            let conversations = await mongooseProvider.getCurrentConversations()\r\n            res.status(200).send(conversations);\r\n        }\r\n    }\r\n    res.status(401).send('Not Authorized');\r\n});<\/pre>\n<p>The images below show a third party application took advantage of the Application Insights logs and this API:<\/p>\n<p> <img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/cse\/wp-content\/uploads\/sites\/55\/2017\/06\/handoff-page.png\" alt=\"Image handoff page\" width=\"1356\" height=\"822\" class=\"aligncenter size-full wp-image-10910\" srcset=\"https:\/\/devblogs.microsoft.com\/ise\/wp-content\/uploads\/sites\/55\/2017\/06\/handoff-page.png 1356w, https:\/\/devblogs.microsoft.com\/ise\/wp-content\/uploads\/sites\/55\/2017\/06\/handoff-page-300x182.png 300w, https:\/\/devblogs.microsoft.com\/ise\/wp-content\/uploads\/sites\/55\/2017\/06\/handoff-page-1024x621.png 1024w, https:\/\/devblogs.microsoft.com\/ise\/wp-content\/uploads\/sites\/55\/2017\/06\/handoff-page-768x466.png 768w\" sizes=\"(max-width: 1356px) 100vw, 1356px\" \/><\/p>\n<h2><a name=\"npm\"><\/a>Using the npm package<\/h2>\n<p>To make this framework easy to use, we packaged it up and made it available through npm under the name <a href=\"https:\/\/www.npmjs.com\/package\/botbuilder-handoff\">botbuilder-handoff<\/a>. \u00a0Below is an example of how you can use the framework in a normal bot:<\/p>\n<pre class=\"lang:js decode:true\">import * as builder from 'botbuilder';\r\nimport * as handoff from 'botbuilder-handoff';\r\n\r\n\/\/=========================================================\r\n\/\/ Handoff Setup\r\n\/\/=========================================================\r\n\r\n\/\/ Replace this function with custom login\/verification for agents\r\nconst isAgent = (session: builder.Session) =&gt; session.message.user.name.startsWith(\"Agent\");\r\n\r\n\/**\r\n    bot: builder.UniversalBot\r\n    app: express ( e.g. const app = express(); )\r\n    isAgent: function to determine when agent is talking to the bot\r\n    options: { }\r\n**\/\r\nhandoff.setup(bot, app, isAgent, {\r\n    mongodbProvider: process.env.MONGODB_PROVIDER,\r\n    directlineSecret: process.env.MICROSOFT_DIRECTLINE_SECRET,\r\n    textAnalyticsKey: process.env.CG_SENTIMENT_KEY,\r\n    appInsightsInstrumentationKey: process.env.APPINSIGHTS_INSTRUMENTATIONKEY,\r\n    retainData: process.env.RETAIN_DATA,\r\n    customerStartHandoffCommand: process.env.CUSTOMER_START_HANDOFF_COMMAND\r\n});\r\n<\/pre>\n<p>The <a href=\"https:\/\/www.npmjs.com\/package\/botbuilder-handoff\">README<\/a> for this project explains every\u00a0option in detail.<\/p>\n<h2><a name=\"conclusion\"><\/a> Conclusion<\/h2>\n<p>This framework enables anyone developing a bot using the Node.js Bot Framework SDK to integrate\u00a0bot to human handoff feature. The added functionality of Application Insights and the sentiment score provide bot authors with more data about interactions with their bot. This data can then be used to customize the bot-to-human handoff experience and analyze the overall customer experience with their bot and agents.<\/p>\n<p>This project is open sourced and available on <a href=\"https:\/\/github.com\/palindromed\/Bot-HandOff\/tree\/npm-handoff\">Github:Bot-Handoff<\/a>.\u00a0We continue to add major new features, but it is in a usable state now.\u00a0Contributions to this project are always welcome!<\/p>\n<h3>Resources<\/h3>\n<ul>\n<li><a href=\"https:\/\/github.com\/palindromed\/Bot-HandOff\/tree\/npm-handoff\">Bot-Handoff source code<\/a><\/li>\n<li><a href=\"https:\/\/www.npmjs.com\/package\/botbuilder-handoff\">botbuilder-handoff npm\u00a0module<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/CatalystCode\/ibex-dashboard\">Ibex Dashboard<\/a><\/li>\n<li><a href=\"https:\/\/dev.botframework.com\">Microsoft Bot Framework<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>When considering bots, it\u2019s often important for organizations to have the ability to \u201chand off\u201d a customer from a bot to a human agent seamlessly. We implemented an unopinionated e-2-e solution called Handoff for bot authors to implement a variety of scenarios, using the Microsoft Bot Framework Node.js SDK.<\/p>\n","protected":false},"author":21393,"featured_media":10909,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[13],"tags":[64,109,110,249,322],"class_list":["post-4098","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-bots","tag-azure-application-insights","tag-bot-framework-sdk","tag-bots","tag-microsoft-bot-framework-mbf","tag-sentiment-analysis"],"acf":[],"blog_post_summary":"<p>When considering bots, it\u2019s often important for organizations to have the ability to \u201chand off\u201d a customer from a bot to a human agent seamlessly. We implemented an unopinionated e-2-e solution called Handoff for bot authors to implement a variety of scenarios, using the Microsoft Bot Framework Node.js SDK.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts\/4098","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/users\/21393"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/comments?post=4098"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts\/4098\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/media\/10909"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/media?parent=4098"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/categories?post=4098"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/tags?post=4098"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}