FormFlow and suggested action - c#

I am using FormFlow with enums to render some questions but it seems that formflow is rendering them as a HeroCard with buttons, I'd like the prompts to render as suggested actions so the show as quick replies in FB, what would be the best way of doing this?
For now, I implemented a custom prompter as follows but like to know if there's a better way of doing this with attributes so I don't need to write custom code.
private static async Task<FormPrompt> Prompter(IDialogContext context, FormPrompt prompt, JObject state, IField<JObject> field)
{
IMessageActivity promptMessage;
// Handle buttons as quick replies when possible (FB only renders 11 quick replies)
if (prompt.Buttons.Count > 0 && prompt.Buttons.Count <= 11)
{
// Build a standard prompt with suggested actions.
promptMessage = context.MakeMessage();
promptMessage.Text = prompt.Prompt;
var actions = prompt.Buttons.Select(button => new CardAction
{
Type = ActionTypes.ImBack,
Title = button.Description,
Value = button.Description
})
.ToList();
promptMessage.SuggestedActions = new SuggestedActions(actions: actions);
}
else
{
promptMessage = await Extensions.GetDefaultPrompter(context, prompt);
}
await context.PostAsync(promptMessage);
return prompt;
}

If you want this functionality than you will have to stick to your implementation. Formflow tries to be as abstract as possible and presenting the enum options as a Herocard with buttons is just a result of that. It does this because almost all channels support Herocards and only facebook supports quick replies.

Related

How to call a QNA bot with in a waterfall dialog?

Say example when we have 3 menu( choices ) within a waterfall dialog in an initial step, when user selected third choice, the system should take over to qna bot form that , is that something possible ?
Nodejs here instead of C# but hopefully this will point you in the right direction. You can do this, you just need to create a separate QnA Dialog and call it from within your waterfall via await step.beginDialog(YOUR_QNA_DIALOG_NAME). You would want that dialog to prompt for the question in the first step, provide the answer in the second step, and (if desired) prompt if they want to ask another question so you can loop via replaceDialog. If there are steps after the initial menu selection, you may wish to cancellAllDialogs instead of endDialog when they exit, otherwise the bot will pick up where it left off with the first dialog.
There might be a more elegant way to do this without using a waterfall dialog but I haven't found it yet.
I have provided a sample QnA Dialog below, but note that in this case I am using it as a default action if no intent is recognized, so it's using the activity.text from the ActivityHandler instead of explicitly prompting the user, and it does not loop. Still, I though this might be helpful.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
const { Dialog, MessageFactory } = require('botbuilder');
const { QnAServiceHelper } = require('../helpers/qnAServiceHelper');
const { CardHelper} = require('../helpers/cardHelper');
class QnADialog {
constructor() {
}
async processAsync(oldState, activity){
const defaultAnswer = `I'm sorry, I don't know how to help with that. Try asking a different question or type "Help" for options.`;
var MINIMUM_SCORE = 50;
var newState = null;
var query = activity.text;
var qnaResult = await QnAServiceHelper.queryQnAService(query, oldState);
var qnaAnswer = qnaResult[0].answer;
var prompts = null;
if(qnaResult[0].context != null){
prompts = qnaResult[0].context.prompts;
}
var outputActivity = null;
if (prompts == null || prompts.length < 1) {
if (qnaResult[0].score > MINIMUM_SCORE) {
outputActivity = MessageFactory.text(qnaAnswer);
} else {
outputActivity = MessageFactory.text(defaultAnswer);
}
}
else {
var newState = {
PreviousQnaId: qnaResult[0].id,
PreviousUserQuery: query
}
outputActivity = CardHelper.GetHeroCard('', qnaAnswer, prompts);
}
return [newState, outputActivity , null];
}
}
module.exports.QnADialog = QnADialog;

How to add more than 10 buttons for ChoicePrompt Bot framework V4 c#

I am using ChoicePrompt in WaterfallStep for displaying choices. The issue is if the choice list is greater than 10, I am not getting buttons, That's displaying as text. Please help me on how to fix this error.
var waterfallSteps = new WaterfallStep[]
{
InitializeStateStepAsync,
PromptShowChoiceStepAsync,
DisplaySuccessStepAsync,
};
AddDialog(new WaterfallDialog("waterfallDialog", waterfallSteps));
AddDialog(new ChoicePrompt("ShowChoicePrompt", ValidateShowChoice));
private async Task<DialogTurnResult> ValidateShowChoice(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
return await stepContext.PromptAsync("ShowChoicePrompt", new PromptOptions
{
Prompt = MessageFactory.Text("Please select from choices"),
RetryPrompt = MessageFactory.Text("Sorry, Please the valid choice"),
Choices = ChoiceFactory.ToChoices(choicesList),
}, cancellationToken);
}
}
If choicesList count is greater than 10, I am getting the buttons like this. Which is not an issue in Bot framework V3.
Please select from choices
1. Choice-1
2. Choice-2
3. Choice-3
4. Choice-4
5. Choice-5
6. Choice-6
7. Choice-7
8. Choice-8
9. Choice-9
10. Choice-10
11. Choice-11
12. Choice-12
13. Choice-13
Please help me how to resolve this error.
Choice prompts in v3 used hero cards by default. You can force your prompts to use hero cards with the new HeroCard option in the ListStyle enum. The list style can be applied directly to the prompt when you add it to your dialog set:
AddDialog(new ChoicePrompt("ShowChoicePrompt", ValidateShowChoice) { Style = ListStyle.HeroCard });
You also now have the ability to specify a list style in the prompt options:
return await stepContext.PromptAsync("ShowChoicePrompt", new PromptOptions
{
Prompt = MessageFactory.Text("Please select from choices"),
RetryPrompt = MessageFactory.Text("Sorry, Please the valid choice"),
Choices = ChoiceFactory.ToChoices(choicesList),
Style = ListStyle.HeroCard,
}, cancellationToken);
While it's true that it's bad practice to include too many buttons in a message and most channels enforce this convention by not allowing a lot of buttons on a card, the connectors for Facebook and Skype will automatically generate multiple cards if you try to put too many buttons on one card.
In Facebook Messenger it will look like this:
In Skype it will look like this:
These display choices are made due to limitations / guidelines on the channels used.
If you have a look to Facebook Messenger developer's page about Quick replies here, it states:
Quick replies provide a way to present a set of up to 11 buttons
in-conversation that contain a title and optional image, and appear
prominently above the composer. You can also use quick replies to
request a person's location, email address, and phone number.
As a consequence, in the code of the BotBuilder available on Github, you will have a method to Determine if a number of Suggested Actions are supported by a Channel here:
/// <summary>
/// Determine if a number of Suggested Actions are supported by a Channel.
/// </summary>
/// <param name="channelId">The Channel to check the if Suggested Actions are supported in.</param>
/// <param name="buttonCnt">(Optional) The number of Suggested Actions to check for the Channel.</param>
/// <returns>True if the Channel supports the buttonCnt total Suggested Actions, False if the Channel does not support that number of Suggested Actions.</returns>
public static bool SupportsSuggestedActions(string channelId, int buttonCnt = 100)
{
switch (channelId)
{
// https://developers.facebook.com/docs/messenger-platform/send-messages/quick-replies
case Connector.Channels.Facebook:
case Connector.Channels.Skype:
return buttonCnt <= 10;
// ...
}
}
Then, in the ChoiceFactory that you used, the display is selected (see code here):
public static IMessageActivity ForChannel(string channelId, IList<Choice> list, string text = null, string speak = null, ChoiceFactoryOptions options = null)
{
channelId = channelId ?? string.Empty;
list = list ?? new List<Choice>();
// Find maximum title length
var maxTitleLength = 0;
foreach (var choice in list)
{
var l = choice.Action != null && !string.IsNullOrEmpty(choice.Action.Title) ? choice.Action.Title.Length : choice.Value.Length;
if (l > maxTitleLength)
{
maxTitleLength = l;
}
}
// Determine list style
var supportsSuggestedActions = Channel.SupportsSuggestedActions(channelId, list.Count);
var supportsCardActions = Channel.SupportsCardActions(channelId, list.Count);
var maxActionTitleLength = Channel.MaxActionTitleLength(channelId);
var hasMessageFeed = Channel.HasMessageFeed(channelId);
var longTitles = maxTitleLength > maxActionTitleLength;
if (!longTitles && !supportsSuggestedActions && supportsCardActions)
{
// SuggestedActions is the preferred approach, but for channels that don't
// support them (e.g. Teams, Cortana) we should use a HeroCard with CardActions
return HeroCard(list, text, speak);
}
else if (!longTitles && supportsSuggestedActions)
{
// We always prefer showing choices using suggested actions. If the titles are too long, however,
// we'll have to show them as a text list.
return SuggestedAction(list, text, speak);
}
else if (!longTitles && list.Count <= 3)
{
// If the titles are short and there are 3 or less choices we'll use an inline list.
return Inline(list, text, speak, options);
}
else
{
// Show a numbered list.
return List(list, text, speak, options);
}
}
This is why you got the list if you provide more than 10 items.
Generally, it's a best practice to limit the number of buttons, more than 10 is huge. You may adapt your behaviour (grouping items / adding an additional level of selection by group for example)

Clarify the usage of GenerateMessages() method

Using the Prompter method to send custom cards in the FormFlow. Looking at the code saw that there is a GenerateMessages() method, which is always returning false for the below code. Can someone clarify why / when to use this method?
https://docs.botframework.com/en-us/csharp/builder/sdkreference/d7/d6d/class_microsoft_1_1_bot_1_1_builder_1_1_form_flow_1_1_advanced_1_1_extensions.html#abff216af1ae24937c78767e621477935
.Prompter(async (context, prompt, state, field) => {
var preamble = context.MakeMessage();
var promptMessage = context.MakeMessage();
if (prompt.GenerateMessages(preamble, promptMessage))
{
await context.PostAsync(preamble);
}
else
{
promptMessage.Text = prompt.Prompt;
var attachment = Helper.GetAttachment();
promptMessage.Attachments.Add(attachment);
await context.PostAsync(promptMessage);
}
The code for .GenerateMessages can be found here: https://github.com/Microsoft/BotBuilder/blob/497252e8d9949be20baa2cebaa6ce56de04461cf/CSharp/Library/Microsoft.Bot.Builder/FormFlow/IPrompt.cs#L248
It appears that false will be returned unless there are:
buttons or an image in the description AND
one or more Environment.NewLine characters in the Prompt
I haven't personally used it, but it seems this method would be useful when defining a multi-line prompt message using FormFlow. Since markdown is not supported in all channels, this method provides somewhat of a workaround: enabling multi-line messages.

Issue with List of buttons in Microsoft Bot Builder for .NET - Channel: Facebook Messenger

I am trying to add a list of buttons as inside a herocard. It works fine in Bot Emulator but doesn't work in Messenger Channel. Here's my code.
public static IList<Attachment> ToAttachmentList(this List<string> items)
{
var attachments = new List<Attachment>();
var actions = new List<CardAction>();
foreach (var item in items)
{
actions.Add(new CardAction(ActionTypes.ImBack, title: item, value: item));
}
var heroCard = new HeroCard
{
Buttons = actions
};
attachments.Add(heroCard.ToAttachment());
return attachments;
}
private async Task ShowOptions(IDialogContext context)
{
var reply = context.MakeMessage();
reply.Text = $"Here's what you can do.";
reply.AttachmentLayout = AttachmentLayoutTypes.List;
reply.Attachments = Messages.OrderingOptions.ToAttachmentList();
await context.PostAsync(reply);
}
In Messenger, the last button gets added as a Carousel, all the button text is truncated.
Please help me fix this.
Per the documentation, the button title has a 20 character limit.
Also, if you have more that 3 buttons, Facebook will split them as the Button template expects from 1-3 buttons.
You will need to limit the characters to 20, so instead of "I want to order pizza", you might want to use "Order pizza" for example. To add more buttons; you might want to explore Quick Replies since the limit is 11 "buttons" (but you still have the 20 chars limit on the title). You can check this other post to understand more about Quick Replies.

Terminate all dialogs and exit conversation in MS Bot Framework when the user types "exit", "quit" etc

I can't figure out how to do the a very simple thing in MS Bot Framework: allow the user to break out of any conversation, leave the current dialogs and return to the main menu by typing "quit", "exit" or "start over".
Here's the way my main conversation is set up:
public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
try
{
if (activity.Type == ActivityTypes.Message)
{
UserActivityLogger.LogUserBehaviour(activity);
if (activity.Text.ToLower() == "start over")
{
//Do something here, but I don't have the IDialogContext here!
}
BotUtils.SendTyping(activity); //send "typing" indicator upon each message received
await Conversation.SendAsync(activity, () => new RootDialog());
}
else
{
HandleSystemMessage(activity);
}
}
I know how to terminate a dialog with context.Done<DialogType>(this);, but in this method, I do not have access to the IDialogContext object, so I cannot call .Done().
Is there any other way to terminate the whole dialog stack when the user types a certain message, other than adding a check for that in each step of all dialogs?
Posted bounty:
I need a way to terminate all IDialogs without using the outrageous hack that I've posted here (which deletes all user data, which I need, e.g. user settings and preferences).
Basically, when the user types "quit" or "exit", I need to exit whatever IDialog is currently in progress and return to the fresh state, as if the user has just initiated a conversation.
I need to be able to do this from MessageController.cs, where I still do not have access to IDialogContext. The only useful data I seem to have there is the Activity object. I will be happy if someone points out to other ways to do that.
Another way to approach this is find some other way to check for the "exit" and "quit" keywords at some other place of the bot, rather than in the Post method.
But it shouldn't be a check that is done at every single step of the IDialog, because that's too much code and not even always possible (when using PromptDialog, I have no access to the text that the user typed).
Two possible ways that I didn't explore:
Instead of terminating all current IDialogs, start a new conversation
with the user (new ConversationId)
Obtain the IDialogStack object and do something with it to manage the dialog stack.
The Microsoft docs are silent on this object so I have no idea how to get it. I do not use the Chain object that allows .Switch() anywhere in the bot, but if you think it can be rewritten to use it, it can be one of the ways to solve this too. However, I haven't found how to do branching between various types of dialogs (FormFlow and the ordinary IDialog) which in turn call their own child dialogs etc.
PROBLEM BREAKDOWN
From my understanding of your question, what you want to achieve is to reset the dialog stack without completely destroy the bot state.
FACTS (from what I read from github repository)
How the framework save the dialog stack is as below:
BotDataStore > BotData > DialogStack
BotFramework is using AutoFac as an DI container
DialogModule is their Autofac module for dialog components
HOW TO DO
Knowing FACTS from above, my solution will be
Register the dependencies so we can use in our controller:
// in Global.asax.cs
var builder = new ContainerBuilder();
builder.RegisterModule(new DialogModule());
builder.RegisterModule(new ReflectionSurrogateModule());
builder.RegisterModule(new DialogModule_MakeRoot());
var config = GlobalConfiguration.Configuration;
builder.RegisterApiControllers(Assembly.GetExecutingAssembly());
builder.RegisterWebApiFilterProvider(config);
var container = builder.Build();
config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
Get the Autofac Container (feel free to put anywhere in your code that you're comfortable with)
private static ILifetimeScope Container
{
get
{
var config = GlobalConfiguration.Configuration;
var resolver = (AutofacWebApiDependencyResolver)config.DependencyResolver;
return resolver.Container;
}
}
Load the BotData in the scope
Load the DialogStack
Reset the DialogStack
Push the new BotData back to BotDataStore
using (var scope = DialogModule.BeginLifetimeScope(Container, activity))
{
var botData = scope.Resolve<IBotData>();
await botData.LoadAsync(default(CancellationToken));
var stack = scope.Resolve<IDialogStack>();
stack.Reset();
await botData.FlushAsync(default(CancellationToken));
}
Hope it helps.
UPDATE 1 (27/08/2016)
Thanks to #ejadib to point out, Container is already being exposed in conversation class.
We can remove step 2 in the answer above, in the end the code will look like
using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, activity))
{
var botData = scope.Resolve<IBotData>();
await botData.LoadAsync(default(CancellationToken));
var stack = scope.Resolve<IDialogStack>();
stack.Reset();
await botData.FlushAsync(default(CancellationToken));
}
Here is a horribly ugly hack that works. It basically deletes all user data (which you might actually need) and this causes the conversation to restart.
If someone knows a better way, without deleting user data, please please share.
public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
try
{
if (activity.Type == ActivityTypes.Message)
{
//if the user types certain messages, quit all dialogs and start over
string msg = activity.Text.ToLower().Trim();
if (msg == "start over" || msg == "exit" || msg == "quit" || msg == "done" || msg =="start again" || msg == "restart" || msg == "leave" || msg == "reset")
{
//This is where the conversation gets reset!
activity.GetStateClient().BotState.DeleteStateForUser(activity.ChannelId, activity.From.Id);
}
//and even if we reset everything, show the welcome message again
BotUtils.SendTyping(activity); //send "typing" indicator upon each message received
await Conversation.SendAsync(activity, () => new RootDialog());
}
else
{
HandleSystemMessage(activity);
}
}
I know this is a little old, but I had the same problem and the posted solutions are no longer the best approaches.
I'm not sure since what version this is available, but on 3.8.1 you can register IScorable services that can be triggered anywhere in the dialog.
There is a sample code that shows how it works, and it does have a "cancel" global command handler:
https://github.com/Microsoft/BotBuilder-Samples/tree/master/CSharp/core-GlobalMessageHandlers
A part of the code will look like this:
protected override async Task PostAsync(IActivity item, string state, CancellationToken token)
{
this.task.Reset();
}
Additional code that worked for someone else:
private async Task _reset(Activity activity)
{
await activity.GetStateClient().BotState
.DeleteStateForUserWithHttpMessagesAsync(activity.ChannelId, activity.From.Id);
var client = new ConnectorClient(new Uri(activity.ServiceUrl));
var clearMsg = activity.CreateReply();
clearMsg.Text = $"Reseting everything for conversation: {activity.Conversation.Id}";
await client.Conversations.SendToConversationAsync(clearMsg);
}
This code is posted by user mmulhearn here: https://github.com/Microsoft/BotBuilder/issues/101#issuecomment-316170517
I know this is an old question, but I got here so... It seems like the 'recommended' way to handle interruptions is to use a Main dialog as the entry point to your bot and then make that main dialog inherit some kind of Cancel/Help dialog whose OnContinueDialogAsync function gets called first. There you can call CancelAllDialosAsync from the DialogContext provided in the parameter.
Read more here https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-howto-handle-user-interrupt?view=azure-bot-service-4.0&tabs=csharp

Categories