ChatBot back to previous dialog - node.js

I've created a chatbot using Node.js and the flow of dialogs works fine til the endDialog. Im having issues implementing a back option so it can jump back only to previous dialog. Can anyone please guide me how to solve that?
.reloadAction(
"restartBenefits", "Ok. Let's start over.",
{
matches: /^start over$|^restart$/i
}
)
.cancelAction(
"cancelRequest", "Thank you for reaching out, Good bye!",
{
matches: /^nevermind$|^cancel$|^cancel.*request/i,
confirmPrompt: "This will cancel your request. Are you sure?"
}
);

Use a customAction
In this case you can listen for whatever you want to be a keyword for "back" and then simply route the user back to that dialog using replaceDialog
bot.customAction({
matches: /back|last/gi, //whatever prompts you want to match.
onSelectAction: (session, args, next) => {
session.replaceDialog(PreviousDialog); //variable with the last dialog in it, set that somewhere, such as the end of your previous dialog
}
})

I think that at the final step of the dialog waterfall you need to add the last lines in this sample step:
/**
* This is the final step in the main waterfall dialog.
* It wraps up the sample "book a flight" interaction with a simple confirmation.
*/
async finalStep(stepContext) {
// If the child dialog ("bookingDialog") was cancelled or the user failed to confirm, the Result here will be null.
if (stepContext.result) {
const result = stepContext.result;
// Now we have all the booking details.
// This is where calls to the booking AOU service or database would go.
// If the call to the booking service was successful tell the user.
const timeProperty = new TimexProperty(result.travelDate);
const travelDateMsg = timeProperty.toNaturalLanguage(new Date(Date.now()));
await stepContext.context.sendActivity(ActivityFactory.fromObject(this.lgTemplates.evaluate('BookingConfirmation', {
Destination: result.destination,
Origin: result.origin,
DateMessage: travelDateMsg
})));
}
// =====> HERE: Restart the main dialog with a different message the second time around
return await stepContext.replaceDialog(this.initialDialogId, { restartMsg: 'What else can I do for you?' });
}
Just change the this.initialDialogId accordingly.

Related

How to have delay between two steps in Bot Framework v4?

I want to have dialog flow like this
step1 -> delay -> step2
async step1(stepContext) {
await stepContext.context.sendActivity('Give user some task to do');
return await stepContext.next();
}
async delay(stepContext) {
await stepContext.context.sendActivity({type: 'delay', value:5000});
return await stepContext.next();
}
async step2(stepContext) {}
The above code is not working as wanted. When I run the bot it waits for 5 seconds and then executes step1 and step2. I want the delay to be after step1.
Actually, I want the delay to be of 2 minutes. I was wondering that won't the bot go on sleep or something. I am sorry I am new to this.
I think the best way to accomplish this while using waterfall dialogs is going to be via the use of setTimeout and Proactive Messages. I'm going to make some assumptions here that you are familiar with things like Conversation State, Bot Framework Adapter, and Turn Handlers, but if you need further guidance on anything let me know.
First, foundationally you are going to need to capture the conversation reference in order to send the proactive message. I find this easiest to just do in your onTurn function and save it to conversation state every turn as follows:
const conversationData = await this.dialogState.get(context, {});
conversationData.conversationReference = TurnContext.getConversationReference(context.activity);
await this.conversationState.saveChanges(context);
Now, you can set up your dialog as follows. You could probably do this several different ways. I typically try to make each step a prompt, but you could probably switch these actions around and still make it work.
async step1(stepContext) {
// First give the user the task
await stepContext.context.sendActivity('Give user some task to do');
// Next set up a timeout interval. The action here is to send a proactive message.
const conversationData = await this.dialogState.get(stepContext.context, {});
this.inactivityTimer = setTimeout(async function(conversationReference) {
try {
const adapter = new BotFrameworkAdapter({
appId: process.env.microsoftAppID,
appPassword: process.env.microsoftAppPassword
});
await adapter.continueConversation(conversationReference, async turnContext => {
await turnContext.sendActivity('Were you able to successfully complete the task?');
});
} catch (error) {
//console.log('Bad Request. Please ensure your message contains the conversation reference and message text.');
console.log(error);
}
}, 120000, conversationData.conversationReference);
// Give the user a confirmation prompt
return await stepContext.prompt('choicePrompt', 'Please confirm the results of the task when you are finished',['Passed','Failed']);
}
async step2(stepContext) {
// Clear timeout to prevent the proactive message if user has already responded
clearTimeout(this.inactivityTimer);
/* REST OF YOUR DIALOG HERE */
return await stepContext.endDialog();
}
As mentioned, make sure you are importing your Bot Framework Adapter, setting up your prompt (I used choice but obviously it can be whatever), importing conversation state, etc. Also, one thing to be aware of is that if the user responds very close to the timer (2 minutes as I've set it up here), it is possible that the proactive message will be invoked before the bot reaches the clearTimeout statement. I think this would be an annoyance at most, but you would need to decide if that UX is ok based on how frequently someone would complete the task in near exactly 2 minutes.
One final note, you could probably put the proactive message invocation in a separate helper function, especially if you are going to use this in many different dialogs. That way you don't have to create multiple instances of the Adapter, in addition to making your code easier to update. That said, if you only need it in only place like I did, I find it much easier just to insert it into the dialog since it's not that much code.

Is there a way to make bots aware of what page they are on?

I have a chatbot that will eventually be deployed on multiple websites, and there are a number or variables that need to change based on the site (e.g. language, QnA Database, Dialog, etc.). I'd like to do this with a single bot, and just pass a variable so that it knows which page it is being rendered on (for a simple example, let's assume country pages: us, fr, de, etc.). I have been unsuccessful in passing this information to the bot.
Ideally this would be before the welcome message fires, but I can't even get it to send at all. I have a custom store set up:
const store = window.WebChat.createStore({}, function(dispatch) { return function(next) { return function(action) {
if (action.type === 'WEB_CHAT/SEND_MESSAGE') {
// Message sent by the user
PageTitleNotification.Off();
clearTimeout(interval);
} else if (action.type === 'DIRECT_LINE/INCOMING_ACTIVITY' && action.payload.activity.name !== "inactive") {
// Message sent by the bot
clearInterval(interval);
interval = setTimeout(function() {
// Change title to flash the page
PageTitleNotification.On('Are you still there?');
// Notify bot the user has been inactive
dispatch.dispatch({
type: 'WEB_CHAT/SEND_EVENT',
payload: {
name: 'inactive',
value: ''
}
});
}, 300000)
}
return next(action);
}}});
But for my use case I don't think what's in there actually matters, only that it is defined. The functions here just 1) clear an interval when the user sends a message and 2) set a new interval and send an inactivity message to the bot.
I also have a send message activity that is on a button click for a transcript. It looks like this:
document.querySelector('#transcriptButton').addEventListener('click', function() {
return store.dispatch({
type: 'WEB_CHAT/SEND_MESSAGE',
payload: { text: 'Email me a transcript' }
});
/*return store.dispatch({
type: 'WEB_CHAT/SEND_EVENT',
payload: {
name: 'siteContext',
value: 'eatonchatbot indexBackup.html'
}
});*/
});
This sends a "front channel" message (that I can see in the bot) to request a transcript, which kicks off a dialog. That works. The commented out section alludes to what I'm trying to do. I have a separate dispatch statement as shown below, which has the exact same SEND_EVENT code as is commented out above. The SEND_EVENT does work as expected when it keys off the button click.
Here is the additional code I added. This is the piece that is NOT working. What I want is, when the bot has been rendered (but ideally before the welcome message), send this siteContext event to the bot so that I know where the bot is being rendered. I do not get any activity in the bot with this code. I also tried replacing it with SEND_MESSAGE instead of SEND_EVENT in a sort of reverse test from above, but that didn't work either.
// Test setting site context
store.dispatch({
type: 'WEB_CHAT/SEND_EVENT',
payload: {
name: 'siteContext',
value: 'eatonchatbot indexBackup.html'
}
});
/*store.dispatch({
type: 'WEB_CHAT/SEND_MESSAGE',
payload: {
text: 'eatonchatbot indexBackup.html'
}
});*/
It just occurred to me that this statement is probably running before the bot is rendered. So I put it in an interval and this DOES work. However, it does not fire the message until after the welcome message has been sent.
setTimeout(function() {
store.dispatch({
type: 'WEB_CHAT/SEND_EVENT',
payload: {
name: 'siteContext',
value: 'eatonchatbot indexBackup.html'
}
});
}, 5000);
So this kind of works, but if this siteContext value was needed to determine the language of the welcome message, this would obviously fail. So my main ask here is, is there a better way to try to pass in a siteContext value like this, or is there some way to ensure that the context is received and can be used by the bot before the welcome message fires? I do see that there is a locale setting in the renderWebChat method, but I can't figure out if and how I could access that in the bot, and besides it may not be granular enough depending on the business need. But it seems if I could send some sort of value in that renderWebChat object, that might avoid all of the other crazy stuff I'm trying to do.
With some help from #Hessel and this issue I found on GitHub, I was able to come up with a solution. Just setting the values being passed in via onEvent (which I am now using in place of onTurn to reduce an if statement) isn't good enough if you need to alter content in the welcome message (e.g. language, user name, or an altogether different message). The onMembersAdded still fires before the values can be set, at least if you're setting them in userState. The key is to set up separate welcome messages in onEvent for directline and onMembersAdded for all other channels (I didn't include webchat as in the example as I'm not sending any event for that channel).
Here is the onEvent function I used:
this.onEvent(async (context, next) => {
// Check for inactivity
if (context.activity.name && context.activity.name === 'inactive') {
await context.sendActivity({
text: 'Are you still there? Is there anything else I can help you with?',
name: 'inactive'
});
}
// Check for webchat/join event (directline conversation initiation)
if (context.activity.name && context.activity.name === 'webchat/join') {
const userData = await this.userDialogStateAccessor.get(context, {});
userData.siteContext = context.activity.value;
// Debug
console.log(`The current language is: ${userData.siteContext.language}`);
console.log(`The current page is: ${userData.siteContext.page}`);
//await context.sendActivity(`The current language is: ${userData.siteContext.language}`);
//await context.sendActivity(`The current page is: ${userData.siteContext.page}`);
if (!userData.accountNumber) {
const dc = await this.dialogs.createContext(context);
await dc.beginDialog(AUTH_DIALOG);
await this.conversationState.saveChanges(context);
await this.userState.saveChanges(context);
} else {
if (context.activity.channelId == 'msteams') {
var welcomeCard = CardHelper.GetMenuCardTeams(welcomeMessage,'Y','Y');
} else {
var welcomeCard = CardHelper.GetMenuCard(welcomeMessage,'Y','Y');
}
await context.sendActivity(welcomeCard);
this.appInsightsClient.trackEvent({name:'conversationStart', properties:{accountNumber:userData.accountNumber}});
}
await this.userState.saveChanges(context);
}
// By calling next() you ensure that the next BotHandler is run.
await next();
});
The event I used in the custom store is pretty much the same as above, except I updated it to pull in the most preferred language and current url (was hard coded above).
store = window.WebChat.createStore({}, function (dispatch) {
return function (next) {
return function (action) {
if (action.type === 'DIRECT_LINE/CONNECT_FULFILLED') {
dispatch.dispatch({
type: 'WEB_CHAT/SEND_EVENT',
payload: {
name: 'webchat/join',
value: {
language: navigator.languages[0],
page: window.location.href
}
}
});
}
return next(action);
};
};
});
If you have your renderWebChat method inside a function that you can call so that your bot doesn't automatically start (I have a floating icon that causes the bot to load onclick) this should go outside that function.

Microsoft Teams Bot-Framework Skill Dialog: endDialog() does not work

I am implementing a dialogRootBot that will call a skillDialogBot.
My dialogRootBot is able to invoke the skillDialogBot. The conversation can progress until the point where the skillDialogBot is supposed to return the results to the dialogRootBot.
The skillDialogBot has the following setup
this.addDialog(new TextPrompt(TEXT_PROMPT))
.addDialog(new ConfirmPrompt(CONFIRM_PROMPT))
.addDialog(new WaterfallDialog(WATERFALL_DIALOG, [
this.processStep.bind(this)
]));
The processStep is laid out like this
async processStep(stepContext) {
const details = stepContext.options;
details.result = {
status: 'success'
};
return await stepContext.endDialog(stepContext.options);
}
I was expecting the dialogRootBot to get the result from the skillDialogBot after processStep has called endDialog, but that never happens. Instead, the user is stuck with the skillDialogBot until the user manually types in "abort", which is the command the dialogRootBot is monitoring to cancel all dialogs in its onContinueDialog() implementation
Here is how the onContinueDialog() looks like
async onContinueDialog(innerDc) {
const activeSkill = await this.activeSkillProperty.get(innerDc.context, () => null);
const activity = innerDc.context.activity;
if (activeSkill != null && activity.type === ActivityTypes.Message && activity.text) {
if (activity.text.toLocaleLowerCase() === 'abort') {
// Cancel all dialogs when the user says abort.
// The SkillDialog automatically sends an EndOfConversation message to the skill to let the
// skill know that it needs to end its current dialogs, too.
await innerDc.cancelAllDialogs();
return await innerDc.replaceDialog(this.initialDialogId, { text: 'Request canceled!' });
}
}
return await super.onContinueDialog(innerDc);
}
I modeled this after the botbuilder-samples\samples\javascript_nodejs\81.skills-skilldialog sample. If I were to change the skillDialogBot and have it do a ConfirmPrompt() before the finalStep()'s endDialog(), then the conversation ends correctly with the skillDialogBot() posting the dialog's results to the rootDialogBot.
For the sake of clarity, this is how the bookingDialog in the skills-skilldialog sample looks like
/**
* Confirm the information the user has provided.
*/
async confirmStep(stepContext) {
const bookingDetails = stepContext.options;
// Capture the results of the previous step.
bookingDetails.travelDate = stepContext.result;
const messageText = `Please confirm, I have you traveling to: ${ bookingDetails.destination } from: ${ bookingDetails.origin } on: ${ bookingDetails.travelDate }. Is this correct?`;
const msg = MessageFactory.text(messageText, messageText, InputHints.ExpectingInput);
// Offer a YES/NO prompt.
return await stepContext.prompt(CONFIRM_PROMPT, { prompt: msg });
}
/**
* Complete the interaction and end the dialog.
*/
async finalStep(stepContext) {
if (stepContext.result === true) {
const bookingDetails = stepContext.options;
return await stepContext.endDialog(bookingDetails);
}
return await stepContext.endDialog();
}
Is it not possible to endDialog() without a prompt? Why is my skillDialogBot unable to endDialog and pass the results to the rootDialogBot?
Thank You
For the sake of reference, when using stepContext.beginDialog(), it appears that we cannot reliably endDialog() if the waterfall dialog does not have a prompt step added. If there is a use case where we want to use a skillDialogBot and call a specific dialog in the skillDialogBot via stepContext.beginDialog(), and the called dialog is only doing processing (eg. call REST APIs) without the need to prompt users for further information, at the end of the processing, we can opt to end the conversation instead.
Hence, assuming we have finalStep() bound to the waterfall dialog as the last step, just use the following in finalStep()
return await stepContext.context.sendActivity({
type: ActivityTypes.EndOfConversation,
code: EndOfConversationCodes.CompletedSuccessfully,
value: stepContext.options
});
At least for my use case, I was able to terminate the dialog without needing a user prompt and got the results back to the rootDialog.

Starting a dialog from conversationUpdate event in Node.js BotBuilder

I want to show the message and call a dialog when chatbot initialize. The below code shows the message. But, can not call a dialog.
bot.on('conversationUpdate', function (activity) {
// when user joins conversation, send welcome message
if (activity.membersAdded) {
activity.membersAdded.forEach(function (identity) {
if (identity.id === activity.address.bot.id) {
var reply = new builder.Message()
.address(activity.address)
.text("Hi, Welcome ");
bot.send(reply);
// bot.beginDialog("initialize", '/');
// session.beginDialog("initialize");
}
});
}});bot.dialog('/', intents);
Below is the code for dialog. I need to call below dialog when chatbot begins
bot.dialog('initialize', [
function (session, args, next) {
builder.Prompts.choice(session, "Do you have account?", "Yes|No", { listStyle: builder.ListStyle.button });
}, function (session, args, next) {
if (args.response.entity.toLowerCase() === 'yes') {
//session.beginDialog("lousyspeed");
session.send("No pressed");
} else if (args.response.entity.toLowerCase() === 'no') {
session.send("Yes pressed");
session.endConversation();
}
}]).endConversationAction("stop",
"",
{
matches: /^cancel$|^goodbye$|^exit|^stop|^close/i
// confirmPrompt: "This will cancel your order. Are you sure?"
});
I tried below methods. But it is not working
1. bot.beginDialog("initialize", '/');
2. session.beginDialog("initialize");
You are hitting this error because, although they have the same method name, the method signatures differ between session.beginDialog() and <UniversalBot>bot.beginDialog().
This can be a bit confusing since the first argument to session.beginDialog() is the dialogId, but when using bot.beginDialog() the first argument is the address, and the second param is the dialogId.
To solve this, call bot.beginDialog() with the correct input parameters as described in the SDK reference documentation - Eg. bot.beginDialog(activity.address, dialogId);
https://docs.botframework.com/en-us/node/builder/chat-reference/classes/_botbuilder_d_.universalbot.html#begindialog
You can also see the full method signature in the botbuilder.d TypeScript definition file here:
/**
* Proactively starts a new dialog with the user. Any current conversation between the bot and user will be replaced with a new dialog stack.
* #param address Address of the user to start a new conversation with. This should be saved during a previous conversation with the user. Any existing conversation or dialog will be immediately terminated.
* #param dialogId ID of the dialog to begin.
* #param dialogArgs (Optional) arguments to pass to dialog.
* #param done (Optional) function to invoke once the operation is completed.
*/
beginDialog(address: IAddress, dialogId: string, dialogArgs?: any, done?: (err: Error) => void): void;
I fixed my problem by using single line code
bot.beginDialog(activity.address, 'initialize');

Iterating the click on elements through wedriver.io and nodejs implementation of selenium

When I log in to my application, I am being shown a number of broadcast messages which may or may not be shown and whose number is not under my control. I need to click on a checkbox and next button to dismiss one message and move to the next. So, I need to write a loop on basis of a common element present on the messages with webdriver.io and node.js
I am new to selenium with node.js and webdriver.io and trying to write webdriver.io fucntion inside while and if loop, correct me if that is not possible
The code which I have used is below :
//Code to loop the click on elements depending upon presence of an Element
//Gherkin- I should see if Elementxyz exist then I click on Elementabc and Elementdef
//Author : Rohit
this.Then(/^I should see if "([^"]*)" exist then I click on "([^"]*)" and "([^"]*)"$/, function (selectorelement, selectorcheckbox, selectornext) {
// Element whose presence is checked
selectorelement = this.getSelector(selectorelement);
//Checkbox which needs to be ticked
selectorcheckbox = this.getSelector(selectorcheckbox);
//next button which needs to be clicked
selectornext = this.getSelector(selectornext);
return this.client
.waitForElemReady(selectornext, this.TIMEOUT_CONST)
.then(function(){
if(this.client.isExisting(selectorelement))
{
while(this.client.isExisting(selectorelement))
{
this.client
.click(selectorcheckbox)
.click(selectornext)
.pause(12000)
}
}
else{
console.log("you got lucky there are no messages this time :)")
}
}.bind(this)); });
Please help me as i am new to node.js and wedriver.io world
Hi t33n ,
i have tried the below code and loop is working fine now only thing is that my script is not waiting as .pause() is not working. Could you please help in that.
When I log in to my application, I am being shown a number of broadcast messages which may or may not be shown and whose number is not under my control. I need to click on a checkbox and next button to dismiss one message and move to the next. So, I need to write a loop on basis of a element present on the messages with webdriver.io and node.js I am new to selenium with node.js and webdriver.io and trying to write webdriver.io function for loop.
The code which i am trying to use is working and looping, only thing is giving me problem is pause which is not working as i need to pause a bit till the next message appears.
this.Then(/^I should see if "([^"])" exist then I click on "([^"])" and "([^"]*)"$/, function (selectorelement, selectorcheckbox, selectornext) {
selectorelement = this.getSelector(selectorelement);
// Checking presence of selectorcheckbox
selectorcheckbox = this.getSelector(selectorcheckbox);
selectornext = this.getSelector(selectornext);
var flag1 = false;
// function used for loop
function runNext(flag1,selectorcheckbox, selectornext,browser) {
setTimeout(function (){
browser
.isExisting(selectorcheckbox)
.then(function (isExisting) {
flag1 = isExisting;
if (flag1) {
flag1 = false;
browser.click(selectorcheckbox)
.pause(1000)
.click(selectornext)
.pause(5000); // Pause statements not working
runNext(flag1, selectorcheckbox, selectornext, browser);
}
else {
console.log("no messages left or no messages this time");
}
}, 50000)
}
.bind(this));
}
var loop= runNext(flag1,selectorcheckbox, selectornext,this.client);
});
.pause statements are only working when i am doing return this.client.pause but this stops the execution wherever i write this line.
Please help me with some solution.
maybe this working for you. If not you should see how loops can work and you can create your own loop with this.
function loop() {
client
.isVisible(selectornext).then(function(isVisible) {
if (isVisible) {
client
.isVisible(selectorelement).then(function(isVisible2) {
if (isVisible2) {
client
.click(selectorcheckbox)
.pause(1000)
.click(selectornext)
.pause(12000)
loop()
} // if (isVisible2) {
}) // .isVisible(selectorelement).then(function(isVisible2) {
} // if (isVisible) {
else{
// cancel loop here?
}
}) // .isVisible(selectornext).then(function(isVisible) {
} // function loop() {

Resources