This article introduces the Live Assist for Microsoft Dynamics 365 Bot as an Enabler Bot Javascript (NodeJS) SDK, running within the Azure Web App Bot Framework.
The examples contained here do not provide a detailed guide for developing bots within the Microsoft Azure framework. We assume that the developer has experience of Javascript, NodeJS and the Microsoft Azure Framework and that they are able to use this quick start guide as a starting point to become familiar with the Live Assist SDK.
Note: The Microsoft Web App Bot Framework is currently in development. You may need to make some changes in the steps, or the code, for your development effort. We recommend that you use the App Service Editor provided in the Azure web interface.
This bot receives chats initiated with Microsoft Web App Bot Service. The bot echoes any messages that a visitor sends, sending their original message back to them. When the visitor sends a message containing 'help', the Live Assist Bot SDK is invoked to escalate the chat targeted to a Live Assist Skill Group for processing.
The bot, by using the Live Assist SDK, proxies messages between the Azure Web Chat service and the Agent using Live Assist.
See also: BotBuilder samples repository
Step 1—Creating a NodeJS basic Web App bot
- In Azure, create a new NodeJS Web App Bot resource.
- Set the appropriate Storage and Billing requirements for your application.
The significant parts of this process are:- Selecting a name that is unique within Azure
- Selecting the NodeJS (Basic) Application
This process may take Azure a few minutes to build.
Step 2—Developing your Bot
-
In Azure, from your Web App Bot click Build > Open Online Code Editor to launch the Azure App Service Editor and select the app.js file:
-
Obtain your account number in the Live Assist Account Number article.
-
Managing Skills & Users of your Live Assist Agents.
-
Add the Live Assist Chat SDK to the project by installing the @cafex/liveassist-botsdk-js module, via the editor console:
npm install @cafex/liveassist-botsdk-js
Then import the module in the application code, at the top of the app code:
const liveAssist = require('@cafex/liveassist-botsdk-js');
At this point the sample has added a single default function, that handles visitor conversations, to the UniversalBot constructor:var bot = new builder.UniversalBot(connector, function (session) { session.send("You said: %s", session.message.text); });
Modify this so that many specific handlers can be added (whilst for the moment retaining the same functionality):var bot = new builder.UniversalBot(connector); bot.dialog('/', function(session) { session.send("You said: %s", session.message.text); } );
-
For the purposes of this quick guide the state of the conversation (for example whether or not the visitor is chatting to an agent) is retained at the script level. Normally such state will be maintained properly (see the Bot guide at https://docs.microsoft.com/en-us/bot-framework/nodejs/bot-builder-nodejs-concepts, and the Live Assist Chat SDK module readme). This means that only one conversation at a time will be maintained.
const MODE = { BOT: 0, ESC_INITIATED: 1, ESC_WAITING: 2, ESC_CHATTING: 3, }; let chatData; function removeChatData() { chatData = undefined; } function getChatData(session) { if (!chatData) { chatData = { visitorAddress: session.message.address, mode: MODE.BOT }; } return chatData; }
-
Now replace the single dialog handler for ‘/’ with the following:
bot.dialog('/', function(session) { let chatData = getChatData(session); switch (chatData.mode) { case MODE.BOT: if (/^help/i.test(session.message.text)) { session.beginDialog('/escalateQuery'); } else { let visitorText = session.message.text; let botText = 'You said: "' + visitorText + '"'; session.send(botText, session.message.text); } Break; case MODE.ESC_INITIATED: session.send('Please wait, I\'m trying to connect you to an agent'); Break; case MODE.ESC_WAITING: session.send('Please wait, waiting for an agent'); Break; case MODE.ESC_CHATTING: if (/^stop/i.test(session.message.text)) { chatData.escalatedChat.endChat((err) => { if (err) { session.endConversation('A problem has occurred, starting over'); removeChatData(); } else { chatData.mode = MODE.BOT; } }); } else { chatData.escalatedChat.addLine(session.message.text, (err) => { if (err) { session.send('A problem has occurred sending that'); } }); } Break; Default: } } );
This is the ‘steady-state’ of the bot, and is where a conversation will begin. Initially the bot will be in BOT mode, echoing back input. That is, except when the input begins with keyword “help”. At that point the escalation process is started by triggering the ‘/escalateQuery’ dialog (more later).
-
To start the escalation process a further dialog is added to the bot; add the following (replace the ‘YOUR’ placeholders with your actual values):
bot.dialog('/escalateQuery', [ function(session) { session.send('Please wait while I connect you to an agent'); let spec = { skill: '-YOUR SKILL-', }; chatData.escalatedChat = new liveAssist.Chat('-YOUR ACCOUNT ID-'); chatData.escalatedChat.requestChat(spec, (err) => { if (err) { session.send('Sorry, I failed to contact an agent'); chatData.mode = MODE.BOT; } else { chatData.mode = MODE.ESC_INITIATED; pollChat(); } }); session.endDialog(); } ])
This dialog is invoked from within the root dialog to escalate. A chat object is created (and stored in the conversation state) using the SDK Chat constructor, passing in the account id. A chat is then requested using the SDK requestChat operation. Once that operation has completed, we change mode (ESC_INITIATED) and the chat must then be polled for events (more later).
-
To poll for events, add the following:
function pollChat() { chatData.escalatedChat.poll((err, result) => { let endRead = false; if (err) { console.error('Error during poll: %s', err.message); } else { endRead = processEvents(result.events); } if (!endRead) setTimeout(() => pollChat(), 500); }); }
This uses the SDK poll operation to regularly retrieve events related to the chat. Events are processed following each poll. If the end of the chat is detected during processing of events (more later) the polling is stopped.
-
To process the events returned from the poll, add the following:
function processEvents(events) { let endRead = false; events.forEach((event) => { switch (event.type) { case 'state': switch (event.state) { case 'waiting': chatData.mode = MODE.ESC_WAITING; break; case 'chatting': chatData.mode = MODE.ESC_CHATTING; break; case 'ended': endRead = true; bot.beginDialog(chatData.visitorAddress, '*:/endit', chatData); break; default: break; } break; case 'line': if (event.source !== 'visitor') { let msg = event.text; sendProactiveMessage(chatData.visitorAddress, msg); } break; default: break; } }); return endRead; } function sendProactiveMessage(address, text) { var msg = new builder.Message().address(address); msg.text(text); bot.send(msg); } bot.dialog('/endit', [ function(session) { session.endConversation('Agent interaction has ended'); removeChatData(); } ]);
This processes each event (if any) returned from the poll.
The mode is changed as state events dictate. In particular, when an ‘ended’ state event is received the chat has ended (either end might have triggered this) and the ending of the bot conversation is triggered via a new bot dialog (‘/endit’).
Messages submitted by both ends in the chat are indicated with ‘line’ events. Here, we “display” to the visitor any line event that was not submitted by the visitor (as, in our case, the front end Bot Framework Channel Emulator will have already displayed visitor input).
-
That completes the bot. You should now be able re-run it, see the echoing (for lines that do not begin with “help”), connect with an agent (“help”) and chat with the agent.
If a problem occurs, check that the account id you entered is correct, and that the skill you entered is both correct and has been configured in your account.
-
The above illustrates the minimum use of the SDK features. To look at additional features such as bot transcript handling, context data, chat info, and an alternate SDK interface model, see the Bot Escalation SDK JavaScript API .
Complete Code:
/*----------------------------------------------------------------------------- A simple echo bot for the Microsoft Bot Framework. -----------------------------------------------------------------------------*/ var restify = require('restify'); var builder = require('botbuilder'); var botbuilder_azure = require("botbuilder-azure"); var liveAssist = require('@cafex/liveassist-botsdk-js'); // Setup Restify Server var server = restify.createServer(); server.listen(process.env.port || process.env.PORT || 3978, function () { console.log('%s listening to %s', server.name, server.url); }); // Create chat connector for communicating with the Bot Framework Service var connector = new builder.ChatConnector({ appId: process.env.MicrosoftAppId, appPassword: process.env.MicrosoftAppPassword, openIdMetadata: process.env.BotOpenIdMetadata }); // Listen for messages from users server.post('/api/messages', connector.listen()); /*---------------------------------------------------------------------------------------- * Bot Storage: This is a great spot to register the private state storage for your bot. * We provide adapters for Azure Table, CosmosDb, SQL Azure, or you can implement your own! * For samples and documentation, see: https://github.com/Microsoft/BotBuilder-Azure * ---------------------------------------------------------------------------------------- */ var tableName = 'botdata'; var azureTableClient = new botbuilder_azure.AzureTableClient(tableName, process.env['AzureWebJobsStorage']); var tableStorage = new botbuilder_azure.AzureBotStorage({ gzipData: false }, azureTableClient); // Create your bot with a function to receive messages from the user var bot = new builder.UniversalBot(connector); bot.set('storage', tableStorage); var MODE = { BOT: 0, ESC_INITIATED: 1, ESC_WAITING: 2, ESC_CHATTING: 3, }; var chatData; function removeChatData() { chatData = undefined; } function getChatData(session) { if (!chatData) { chatData = { visitorAddress: session.message.address, mode: MODE.BOT }; } return chatData; } bot.dialog('/', function(session) { var chatData = getChatData(session); switch (chatData.mode) { case MODE.BOT: if (/^help/i.test(session.message.text)) { session.beginDialog('/escalateQuery'); } else { var visitorText = session.message.text; var botText = 'You said: "' + visitorText + '"'; session.send(botText, session.message.text); } break; case MODE.ESC_INITIATED: session.send('Please wait, I\'m trying to connect you to an agent'); break; case MODE.ESC_WAITING: session.send('Please wait, waiting for an agent'); break; case MODE.ESC_CHATTING: if (/^stop/i.test(session.message.text)) { chatData.escalatedChat.endChat((err) => { if (err) { session.endConversation('A problem has occurred, starting over'); removeChatData(); } else { chatData.mode = MODE.BOT; } }); } else { chatData.escalatedChat.addLine(session.message.text, (err) => { if (err) { session.send('A problem has occurred sending that'); } }); } break; default: } } ); bot.dialog('/escalateQuery', [ function(session) { session.send('Please wait while I connect you to an agent'); var spec = { skill: '--YOUR--SKILL--', }; chatData.escalatedChat = new liveAssist.Chat('--YOUR-ACCOUNT-ID--'); chatData.escalatedChat.requestChat(spec, (err) => { if (err) { session.send('Sorry, I failed to contact an agent'); chatData.mode = MODE.BOT; } else { chatData.mode = MODE.ESC_INITIATED; pollChat(); } }); session.endDialog(); } ]); function pollChat() { chatData.escalatedChat.poll((err, result) => { var endRead = false; if (err) { console.error('Error during poll: %s', err.message); } else { endRead = processEvents(result.events); } if (!endRead) setTimeout(() => pollChat(), 500); }); } function processEvents(events) { var endRead = false; events.forEach((event) => { switch (event.type) { case 'state': switch (event.state) { case 'waiting': chatData.mode = MODE.ESC_WAITING; break; case 'chatting': chatData.mode = MODE.ESC_CHATTING; break; case 'ended': endRead = true; bot.beginDialog(chatData.visitorAddress, '*:/endit', chatData); break; default: break; } break; case 'line': if (event.source !== 'visitor') { var msg = event.text; sendProactiveMessage(chatData.visitorAddress, msg); } break; default: break; } }); return endRead; } function sendProactiveMessage(address, text) { var msg = new builder.Message().address(address); msg.text(text); bot.send(msg); } bot.dialog('/endit', [ function(session) { session.endConversation('Agent interaction has ended'); removeChatData(); } ]);
Step 3—Testing your bot
- When you have published your bot, you can quickly test the new Chat Bot from Microsoft Azure > Test Web Chat tab.
Important: You need an Agent logged into LiveAssist for Microsoft Dynamics 365, under the correct skill group, to receive the escalated chat.
Step 4—Debugging your bot
There are numerous ways to debug your bot application.
The following Microsoft documents may assist you with this.
- Diagnosing runtime issues the Failed Request Traces option found on the following page provides detailed information:
https://docs.microsoft.com/en-us/azure/app-service/web-sites-enable-diagnostic-log - You can also debug your bot remotely from within Visual Studio with the following Article:
https://docs.microsoft.com/en-us/azure/bot-service/bot-service-debug-bot