How to combine ChoicePrompt and TextPrompt in the same step - node.js

I am using Microsoft Bot Framework V4 in node.js. In a step of a Dialog we need to combine buttons using the ChoicePrompt object but also the TextPrompt. In case the user clicks the buttons the suggested actions will be triggered, and if the user writes plain text, we handle the action using LUIS and certain intents. The problem is combining both actions.
I have tried to avoid re-prompting when using the ChoicePrompt, but I couldn't manage. Also I look for other prompts that directly could combine buttons and text but it seems there's not any.
First I declare the objects I am using in the prompt:
class ExampleDialog extends LogoutDialog {
constructor(userState, logger) {
super(EXAMPLE_DIALOG);
this.addDialog(new TextPrompt(TEXT_PROMPT));
this.addDialog(new ChoicePrompt(CHOICE_PROMPT));
Second, in the steps, I use the prompts declared before:
async firstStep(step) {
const promptOptions = {
prompt: 'Text to prompt',
retryPrompt: 'Retry text prompt',
choices: ChoiceFactory.toChoices(['option1', 'option2', 'option3'])
};
const promptAction = await step.prompt(A_PROMPT_ID, promptOptions);
return promptAction;
}
async secondStep(step) {
const thePreviousStepResult = step.result.values
}

Text prompts are the way to go when you want to accept any string. Remember that you can include any activity as the prompt property of your prompt options, and that activity can contain attachments, suggested actions, etc. You can see in the source code that a choice prompt just calls Prompt.appendChoices which uses ChoiceFactory to generate the buttons for its activities. You can do the same thing:
const promptOptions = {
prompt: ChoiceFactory.forChannel(step.context, ['option1', 'option2', 'option3'], 'Text to prompt')
// You can also include a retry prompt if you like,
// but there's no need to include the choices property in a text prompt
};
const promptAction = await step.prompt(TEXT_PROMPT, promptOptions);
return promptAction;

Related

How to pass one more parameter other than option from the intent showing the carousel to the intent handling the carousel?

I am using an intent to first present the carousel to the user.
When the user clicks on one of the options in the carousel, in the handler intent I get the key of the carousel item that the user selected.
Example of carousel intent,
app.intent('search', async (conv,params) => {
conv.ask(`Choose one item`,new Carousel({
title :`Search results`,
items : carouselItems,
}));
});
Example of the handler intent,
app.intent('handle_carousel', async (conv,params,option) => {
const key = parseInt(option);
});
However, along with the key of the option selected I also want to pass another integer from the carousel intent to the handler intent.
This other integer is different for each option. You can think of the other integer as an ID, it's unique for each option.
How can I achieve that?
You have a few approaches for passing additional data that should be associated with each key.
The first is, as you note in your answer, storing that mapping in a table that is stored as part of session data (either using conv.data or a Dialogflow context).
Another is to encode that data as part of the key that you include with each option, and then decode the key when you get it back.
So, for example, you could make the key a result of an encode function like
function encodeOptionKey( key, otherValue ){
return `${key}:${otherValue}`
}
and then decode it with a function such as
function decodeOptionKey( option ){
const [key,otherValue] = option.split(':');
return {
key,
otherValue
}
}
and call this from your handler with something like
app.intent('handle_carousel', async (conv,params,option) => {
const {key, otherValue} = decodeOptionKey( option );
// ...
});
I created a map of the keys of various carousel options and the corresponding parameter I wanted to pass, saved that map in conv.data.store, which is the conversation storage provided by actions-on-google. Then I used that map to get the parameter from the carousel key that was being passed to the handler intent.
For example in the carousel intent :
let map = {
keyofcarousel : option,
other_parameter : otherparam,
};
conv.data.store = map;
Then call conv.data.store in the handler intent.

dynamic prompt choices in bot-framework v4 (node.js)

I've got a prompt for an SMS bot in which the user can make multiple choices. I'm looking for a pattern for a ChoicePrompt that allows me to do this:
show multiple selections
then after the user selects and answer, re-prompt them to answer again
Remove their previous choice(s) and add an "exit" option to move on
Automatically end the step if they've selected everything.
I'd like to avoid creating a new prompt w/switch cases for each answer tier, as this pattern needs to be implemented in a lot of places...
Example:
bot: User, what do you do to relax?
Exercise
Read a book
Nothing
user: Exercise
bot: Exercise, cool. What else?
Read a book
Nothing else
user: Read a book
bot: OK, you've done everything so we're moving on!
The botframework don't have a ListPrompt that I can see, at least for v4. They do however, have Suggested Actions you can use for this!!! The Botbuilder-Samples repo has a Suggested Action sample that shows a list of three colors:
async onTurn(turnContext) {
// See https://aka.ms/about-bot-activity-message to learn more about the message and other activity types.
if (turnContext.activity.type === ActivityTypes.Message) {
const text = turnContext.activity.text;
// Create an array with the valid color options.
const validColors = ['Red', 'Blue', 'Yellow'];
// If the `text` is in the Array, a valid color was selected and send agreement.
if (validColors.includes(text)) {
await turnContext.sendActivity(`I agree, ${ text } is the best color.`);
} else {
await turnContext.sendActivity('Please select a color.');
}
// After the bot has responded send the suggested actions.
await this.sendSuggestedActions(turnContext);
} else if (turnContext.activity.type === ActivityTypes.ConversationUpdate) {
await this.sendWelcomeMessage(turnContext);
} else {
await turnContext.sendActivity(`[${ turnContext.activity.type } event detected.]`);
}
}
An option would be to programatically create the array (in the example above, it's "const validColors") and if the reply is in the list of colors, recreate the array however you want without the chosen option.

botframework choice invalid response typed

I am using nodejs SDK for creating my bot with MSFT botframework.
The code snippet is as follows:
function(session, args, next){
builder.Prompts.choice(session, "Please select one of the options:", ['AAA', 'BBB','CCC'], {retryPrompt: "Invalid choice, Please pick below listed choices",
listStyle: builder.ListStyle.button,
maxRetries: 1
});
},
function(session,results){
if (results.response) {
//Do something
}
}
I have 2 questions:
I would like to navigate to a different dialog Flow in case the user types anything other then the options("AAA","BBB","CCC"). Is that possible?
I would like to change the retryPrompt everytime lets say pick the utterances from a list. Is that possible?
I would like to navigate to a different dialog Flow in case the user types anything other then the options("AAA","BBB","CCC"). Is that possible?
Yes, it's possible. You can define several dialogs with required waterfall steps related to the choices. Like:
bot.dialog('AAA',[...])
And leverage replaceDialog to redirect your user to new dialog flows:
(session,result)=>{
//result.response.entity should be one of string `AAA`,`BBB`,`CCC`
session.replaceDialog(result.response.entity);
}
I would like to change the retryPrompt everytime lets say pick the utterances from a list. Is that possible?
Yes, it's possible. According the choice definition, we can set options extends IPromptChoiceOptions which extends [IPromptOptions][2], we can find that retryPrompt?: TextOrMessageType;, dig into the source code for the definition of TextOrMessageType:
/**
* Flexible range of possible prompts that can be sent to a user.
* * _{string}_ - A simple message to send the user.
* * _{string[]}_ - Array of possible messages to send the user. One will be chosen at random.
...
...
*/
export type TextOrMessageType = string|string[]|IMessage|IIsMessage;
So we can set a string list for retryPrompt, bot builder will pick one randomly. Please try following:
builder.Prompts.choice(session, "Please select one of the options:", ['AAA', 'BBB', 'CCC'], {
listStyle: builder.ListStyle.button,
maxRetries: 1,
retryPrompt:['utterance 1','utterance 2','utterance 3','utterance 4']
});
As you can call a function in retryPrompt you can do both of them:
builder.Prompts.choice(
session,
'This is just a question?',
'Yes|No',
{ retryPrompt: particularRetry() }
);
In the above, you can do what you want in particularRetry function.
For example for the second question, you can call the new propmt with new choices.
You can see more details in this post.

Multiple buttons in HeroCard

I'd like to have multiple buttons on HeroCard
and be able to press all buttons one after another
but when I press click button program jumps to next function in waterfall
and expects next action instead of button action again
what should I do in this case?
bot.dialog("/showCards", [
(session) => {
const msg = new Message(session)
.textFormat(TextFormat.xml)
.attachmentLayout(AttachmentLayout.carousel)
.attachments([{
title: "title",
url: "https://www.wikipedia.org/portal/wikipedia.org/assets/img/Wikipedia-logo-v2.png"
}].map(obj =>
new HeroCard(session)
.title(obj.title)
.images([
CardImage.create(session, obj.url)
.tap(CardAction.showImage(session, obj.url)),
])
.buttons([
CardAction.openUrl(session, obj.url),
CardAction.imBack(session, `click`, "Click"),
CardAction.imBack(session, `clack`, "Clack")
])
));
Prompts.choice(session, msg, ["click", "clack"]);
},
(session, results) => {
// todo use results.response.entity
}
]);
You could also use CardAction.dialogAction and link every button to a beginDialogAction.
let card = new builder.HeroCard(session)
.title(title)
.subtitle(subtitle)
.buttons([builder.CardAction.dialogAction(session, 'dialogAAction', 'dataYouNeedInDialogA', 'ButtonTitleA'), builder.CardAction.dialogAction(session, 'dialogBAction', 'dataYouNeedInDialogA', 'ButtonTitleB')]);
let msg = new builder.Message(session)
.attachments([card])
session.endDialog(msg);
// use one of these two to either end the dialog and start a new one or to stay in the current dialog and wait for user input
session.send(msg);
// don't forget to add the dialogs to your bot / library later in your code (outside your current dialog)
bot.dialog('dialogA', dialogA); // initialized somewhere in your code
bot.dialog('dialogB', dialogB);
bot.beginDialogAction('dialogAAction', 'dialogA');
bot.beginDialogAction('dialogBAction', 'dialogB', {
onSelectAction: (session, args, next) => {
// you might want to clear the dialogStack if the button is pressed. Otherwise, if the button is pressed multiple times, instances of dialogB are pilled up on the dialog stack.
session.clearDialogStack();
next();
}
});
In my opinion, this is the best way to achieve the behaviour you described so far. All buttons work whenever the user presses them, even if they scroll back in the conversation and press the same button again. The only trade-off is that you have to pass data to the new dialog and can not use dialogData throughout the whole flow. Nevertheless, I think it's worth it because ensures consistent UX throughout the usage of the bot.
Hope this helps. You can build click and clack dialogs, link them to actions and pass the data that you need. The user would be able to press click, clack, click and the bot would still work. :)
Use a switch-case in the ResumeAfter function, in the default case send the user to the previous function.

BotFramework: Start a Form from a Dialog using Intents

Regarding the Microsoft Bot Framework, we all know the samples given by Microsoft. Those samples, however, normally have "one single purpose", that is, the Pizzabot is only for ordering Pizzas, and so on.
Thing is, I was hoping on creating a more complex Bot that actually answers a series of things. For this I am creating a "lobby" dialog where all the messages go, using this MessageController:
return await Conversation.SendAsync(message, () => new LobbyDialog());
On that "Lobby" dialog I have a series of LUIS intents for different things, and since it picks the Task based on the intent, it works nicely.
However, for more complex operations, I was hoping on using the FormFlow mechanism so I can have forms like in the PizzaBot sample. The problem is that all of the "form bots" that are sampled always use this message controller type:
return Chain.From(() => new PizzaOrderDialog(BuildForm)
And in the same MessagesController stablishes the builder flow, like this:
var builder = new FormBuilder<PizzaOrder>();
ActiveDelegate<PizzaOrder> isBYO = (pizza) => pizza.Kind == PizzaOptions.BYOPizza;
ActiveDelegate<PizzaOrder> isSignature = (pizza) => pizza.Kind == PizzaOptions.SignaturePizza;
ActiveDelegate<PizzaOrder> isGourmet = (pizza) => pizza.Kind == PizzaOptions.GourmetDelitePizza;
ActiveDelegate<PizzaOrder> isStuffed = (pizza) => pizza.Kind == PizzaOptions.StuffedPizza;
return builder
// .Field(nameof(PizzaOrder.Choice))
.Field(nameof(PizzaOrder.Size))
.Field(nameof(PizzaOrder.Kind))
.Field("BYO.Crust", isBYO)
.Field("BYO.Sauce", isBYO)
.Field("BYO.Toppings", isBYO)
.Field(nameof(PizzaOrder.GourmetDelite), isGourmet)
.Field(nameof(PizzaOrder.Signature), isSignature)
.Field(nameof(PizzaOrder.Stuffed), isStuffed)
.AddRemainingFields()
.Confirm("Would you like a {Size}, {BYO.Crust} crust, {BYO.Sauce}, {BYO.Toppings} pizza?", isBYO)
.Confirm("Would you like a {Size}, {&Signature} {Signature} pizza?", isSignature, dependencies: new string[] { "Size", "Kind", "Signature" })
.Confirm("Would you like a {Size}, {&GourmetDelite} {GourmetDelite} pizza?", isGourmet)
.Confirm("Would you like a {Size}, {&Stuffed} {Stuffed} pizza?", isStuffed)
.Build()
;
My big question here is, is it possible to start the conversation with the MessagesController that I used and then in the LobbyDialog, use an Intent that fires a Form and returns it? That is, start a flow from a dialog? Or is better to use DialogChains for that?
Because, from what I tried, it appears that I can ONLY do forms if they are called from teh MessagesController class with the methods I described, that is, how Microsoft sampled it in the Pizzabot.
I appreciate any help or input on the matter. Thanks for your time.
Sure you can! Instantiating a form from a dialog is a pretty common scenario. To accomplish that you can do the following inside the LUIS intent method:
var form = new FormDialog<YourFormModel>(
<ExistingModel>,
<TheMethodThatBuildTheForm>,
FormOptions.PromptInStart,
result.Entities);
context.Call(form, <ResumeAfterCallback>);
using the PizzaBot sample, it should looks like:
var form = new FormDialog<PizzaOrder>(
null,
BuildForm,
FormOptions.PromptInStart,
result.Entities);
context.Call(form, <ResumeAfterCallback>);
In the ResumeAfterCallback you will usually get the result of the form, catch exceptions and perform a context.Wait so the dialog can keep receiving messages. Below a quick example:
private async Task ResumeAfterCallback(IDialogContext context,
IAwaitable<PizzaOrder> result)
{
try
{
var pizzaOrder = await result;
// do something with the pizzaOrder
context.Wait(this.MessageReceived);
}
catch (FormCanceledException<PizzaOrder> e)
{
string reply;
if (e.InnerException == null)
{
reply = "You have canceled the operation. What would you like to do next?";
}
else
{
reply = $"Oops! Something went wrong :(. Technical Details: {e.InnerException.Message}";
}
await context.PostAsync(reply);
context.Wait(this.MessageReceived);
}
}

Resources