How to stop QnAMaker dialog from returning to parent dialog? - node.js

Trying to understand MS Bot Framework and build my first bot.
The default dialog is a Prompts.choice. First choice passes to new dialog that asks for Prompts.text.
User enters text which then passes that to my QnAMaker, which looks for the answer and returns it. This works as expected.
After answer is returned, my bot closes the QnAMaker dialog and then returns to the default dialog where the bot starts over from the beginning. I don't want this to happen.
How do I keep the user in the QnAMaker dialog so that they can continually ask QNAMaker questions until they are done and want to do something else in the bot?
Screenshot of terminal output. Code below. Thanks!
bot.dialog('rentalHelp', [
function(session) {
builder.Prompts.text(session, "In which state do you live? (please spell out)");
},
function(session, results) {
// start the QnA bot dialog
session.beginDialog('QnAMaker');
}
]);

I think I figured out the reason it was doing this from Bot SDK chat reference library
Excerpt:
note: Waterfalls have a hidden last step which will automatically end the current dialog if if you call a prompt or dialog from the last step. This is useful where you have a deep stack of dialogs and want a call to session.endDialog() from the last child on the stack to end the entire stack. The close of the last child will trigger all of its parents to move to this hidden step which will cascade the close all the way up the stack. This is typically a desired behavior but if you want to avoid it or stop it somewhere in the middle you'll need to add a step to the end of your waterfall that either does nothing or calls something like session.send() which isn't going to advance the waterfall forward.
It would be great if someone could confirm this is the problem or help me to improve my code.

Related

Discord.js collector collects button interaction 2 times when I run the same command twice?

I am currently working on a /inventory command, the way it works is that the user does /inventory and I saved that user.id with the page they are on { user.id: page } and based on that generate the page. The way you move from page to page is with buttons and collectors, but my problem is that when the same user does /inventory twice so that there is 2 inventory embeds both with the exact same buttons whenever the user presses one button, the code checks if I am the user who did the /inventory commands (so true) and what customId the button has (both embeds have the same button customId). Due to this both inv embeds are updated and I get an error "Interaction has already been acknowledged."
Is there anyway to differentiate which button has been pressed to update the corresponding embed correctly?
Since there is no actual bug with the code I am not posting the code here, if you need the code just ask. I just want to know what I should do to avoid this.
I encountered a similar issue right now. You should create your collector on a message instead of a channel. You can use fetchReply: true on your reply() method to fetch a message and then use it to call createMessageComponentCollector() method from it.
I know this question is 4 months old, but maybe it'll help someone anyway.
The best way to do this is to create your collector on the message you want to send.
// For example:
const MSG = await message.channel.send({embeds: [yourEmbed]});
const collector = await MSG.createMessageComponentCollector({ filter, idle: 20000 });
Another way to solve this is to create a random ID for your buttons. Like this:
const randomID = Math.floor(100000 + Math.random() * 900000);
const button = new MessageButton()
.setCustomId(`button${randomID}`)
.setLabel('Just a button')
.setStyle('PRIMARY')
There is still a small possibility of getting the same ID on two interactions, but it's really low. You can also create longer IDs to lower the chances even further.
And lastly, another way would be to prevent the user from using the command again while your inventory command is active. This would be achievable by creating a
Map and when the user triggers the command you add user's ID and the command's name.
After the collector ends or the command is stopped you can clear the Map.

I want to restart my watterfall dialog at it's beginning when it ends, using C#

I'm using the Bot Framework .Net SDK4.
I start my Dialog at MainDialog.
I'm trying to restart my dialog when the watterfall dialog conversation ends. I have multiple watterfall that redirect to other watterfall dialogs, unti they reach the final one.
When I'm using stepContext.EndDialogAsync(null, cancellationToken) or stepContext.CancellAllDialogsAsync(cancellationToken), the dialog just returns to the previous parent dialog.
I also can't just use BeginDialogAsync(nameof(MainDialog), null, cancellationToke) because of circular dependency issues.
Is there anything I can do to restart my dialog at MainDialog, where it reruns tehe dialog again.
use
return await sc.ReplaceDialogAsync(nameof(NoUnderstandDialog), cancellationToken);
To restart the waterfall dialog you are currently in.
ReplaceDialogAsync :
Starts a new dialog and replaces on the stack the currently active dialog with
the new one. This is particularly useful for creating loops or redirecting to
another dialog.
You can use this for multiple reasons, validation for example if the user inputs a wrong value, you can restart the dialog to prompt again. Be careful though your waterfall dialog should always "ends" meaning it should have a EndDialogAsync so you don't get stuck in an endless loop

Is there an easy way to get the currently active dialog on the stack?

My bot is based on core-bot sample and has an interrupt function which can be invoked by certain intents during dialogs. If I'm in a dialog, and then the interrupt starts a dialog, they are both invoked via dc.beginDialog and there are on a single level in the dialog stack. For example, it would look like this
[ { id: 'viewOrderDialog', state: { dialogs: [Object] } },
{ id: 'interruptDialog', state: { dialogs: [Object] } } ]
So I can somewhat easily get the active dialog by getting the ID of the last element in the array. However, in my process I can start additional dialogs from, in this case, interruptDialog. Those are started from within a waterfall via step.beginDialog. In that case, they are no longer at the same level as the other dialogs (started from dc instead of step). I have to get into state.dialogs.dialogStack to find the id, which then can become nested again if that dialog calls another. Here is an example of what dc.activeDialog can end up looking like:
{"id":"interruptDialog","state":{"dialogs":{"dialogStack":[{"id":"waterfallDialog","state":{"options":"expediteOrder","values":{"instanceId":"d61d748e-af45-cea0-9188-63904de21dfc"},"stepIndex":0}},{"id":"escalationDialog","state":{"dialogs":{"dialogStack":[{"id":"waterfallDialog","state":{"options":{},"values":{"instanceId":"6e755278-d636-dd76-3b47-eb43e3eda1c7"},"stepIndex":2}},{"id":"emailDialog","state":{"dialogs":{"dialogStack":[{"id":"waterfallDialog","state":{"options":{},"values":{"instanceId":"87f08019-ff59-ce03-ccab-7914fb0b553b"},"stepIndex":1}},{"id":"emailPrompt","state":{"options":{"prompt":"Which email address do you want us to reply to?"},"state":{}}}]}}}]}}}]}}}
I could get down to the lowest level, which in this case is emailPrompt, but it seems it would take an inordinate amount of overhead to check and see if each level of dialogs/dialogStack was an array. (And yes, I should probably name my waterfall dialogs something other than waterfallDialog). I was hoping there would be an easy way to just get the most recent dialog off the stack, but I couldn't find anything to give me that information.
In a less general sense, I'm specifically trying to add a condition to the interrupt to prevent it from being invoked within certain dialogs. I have a step where user can write an email body, and if they write something about expediting an order, the interrupt is activating. In this specific case I decided to solve it by converting dc.activeDialog to a string and then checking to see if it includes 'emailDialog'. Then I add a condition for !activeDialog.includes('emailDialog'). Works fine for this case, but I asked the more general question because this may not be a good solution in other cases where I need to know which dialog I am in.
I can provide code snippets if needed, but the code itself isn't really important. I'm just trying to determine the best way to get the id of the currently active dialog from the dialog context.
The reason you're seeing nested dialog stacks is because you're using component dialogs.
If your interruptions are always performed on the root dialog context and that's where your interrupt dialogs get added, then there should be no need to dig into the nested dialog stacks. Because the interrupt dialog will always be in the root dialog stack you can just check your root dialog context to see if the active dialog is an interrupt dialog.
I don't know of any builtin way to determine the innermost active dialog, but if that's really what you want to do then it shouldn't be hard to create a recursive function to do it:
getInnermostActiveDialog(dc) {
var child = dc.child;
return child ? this.getInnermostActiveDialog(child) : dc.activeDialog;
}
It should be noted that the Core Bot sample makes only specific dialogs interruptible by having them extend a common base dialog class and then handling interruptions from within the dialog instead of from the bot class. You might want to follow that example by having dialogs "opt in" to interruptibility rather than having the interrupt dialog "opt out."

How to handle clicks in buttons left on screen but no longer valid, in Microsoft Bot Framework?

I'm a bit confused about something. We are building a bot with Microsoft Bot Framework (v3 -- I know, I know, it's old, but that's what they want us to do) and at startup we offer the user th choice of two paths to follow, using buttons. It works fine, but the buttons remain on the screen after the user has clicked, as is normal. Should we attempt to handle a user click in the buttons after we have passed from their actual point of use? That is, let's say we've passed through all our waterfalls and are back at the root dialog? How we detect these clicks and intercept them and give suitable feedback (e.g. "That's not a valid choice"?).
At present, it seems there is some routing going on before the user click even gets to our root dialog --
> ChatConnector: message received. UniversalBot("*") routing <null> from
> "emulator" Library("BotBuilder").findRoutes() explanation:
> ActiveDialog(0.1) ...BotBuilder:prompt-text - WARN: Prompt - no
> intent handler found for null ...BotBuilder:prompt-text -
> Session.send() ...BotBuilder:prompt-text - Session.sendBatch() sending
> 1 message(s)
A generic "I didn't understand. Please try again." reply is sent automatically, which seems to come from botbuilder/systemResources.js. Is this, then, the correct and expect behaviour?
I'm a bit confused, as I thought the message would always get to the root dialog and we would have a chance to evaluate it there. I've put a breakpoint in the root dialog, but it never reaches it when I click on an 'already used' button.
Thanks for any insights!
I've faced a similar scenario. I have several forms as adaptive cards and also a carousel of hero cards. What I do is make sure some of the data returned on buttons clicks (or taps) is unique for every form. Then when I'm expecting a form I check if it's the right one, otherwise anything else (including messages with no luis intents) is rejected and I ask the user to answer the current question or cancel while looping back a step.
So for example, in the data for this button
"actions": [
{
"type": "Action.Submit",
"title": "Submit",
"data": {
"x": 13
}
}
...
]
I would add "form" or maybe "dialog" with unique values that you can check for when you expect a specific form/card. In other places where I just expect text then I just ignore forms and re-ask the question.
I'm not how it works for NodeJs but in C#, buttons (and CardActions with MessageBack/PostBack) come under Context.Activity.Value whereas text responses are in Context.Activity.Text

Bot Framework BeginDialog or ReplaceDialog during conversation, which should I use?

I'm currently working on a bot using Microsoft's Bot Framework. This bot has multiple Component dialogs build up from WaterfallDialogs. When my users go through the conversations I split up pieces of this conversation in separate waterfall dialogs, but I've noticed that there are two ways to make this work.
BeginDialog() Creates a new instance of the dialog and pushes it onto the stack.
ReplaceDialog() Ends the active dialog and starts a new dialog in its place.
Currently I've not noticed any difference in using these two ways of switching waterfallDialogs. What are the main differences between the two and which should I be using to switch between waterfallDialogs within a single Component Dialog?
beginDialog does not end the current dialog. So if that dialog is not complete, it will continue running after the new dialog or dialogs are popped off the stack. If you are running this as the last step of a waterfall dialog, I think it will technically work but is not considered a best practice. If you have no intention of returning to the currently running dialog, you should replaceDialog since it combines ending of the current dialog with the beginning of the new one.

Resources