1

I am new to Bot Framework v4 and am trying to write a bot that has several commands. Let's say they are help, orders, and details.

help - retrieves a message containing the commands available.
orders - retrieves a card with a table of data.
details - begins a waterfall dialog retrieving that same table of data, asks for the order #, and then retrieves its details.

The issue I'm facing is that the code in OnMessageActivityAsync runs on the second turn of the dialog. So, the user sends details command and the dialog begins, retuning the list of orders. Then the user sends an order number, but OnMessageActivityAsync runs and the switch statement hits the code in the default block.

How would I solve this? I tried to figure out a way to check if a dialog is in progress, then run the _dialog.RunAsync(... method if it is.

I've looked around for answers but haven't found anything that works. I've followed Microsoft guides to make this bot, like this guide on dialogs. I also found bot samples, which includes a bot with multiple commands, and a bot with a dialog. However, I have not found an effective way to unify them.

My code looks something like so...

TroyBot.cs

public class TroyBot<T> : ActivityHandler where T : Dialog { private readonly ConversationState _conversationState; private readonly UserState _userState; private readonly T _dialog; public TroyBot(ConversationState conversationState, UserState userState, T dialog) { _conversationState = conversationState; _userState = userState; _dialog = dialog; } protected override async Task OnMembersAddedAsync(IList<ChannelAccount> membersAdded, ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken) { foreach (var member in membersAdded) { if (member.Id != turnContext.Activity.Recipient.Id) { await turnContext.SendActivityAsync(MessageFactory.Text( "Hi! I'm Troy. I will help you track and manage orders." + "\n\n" + "Say **`help`** to see what I can do." ), cancellationToken); } } } protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken) { var command = turnContext.Activity.Text.Trim().ToLower(); switch (command) { case "orders": var orders = await GetOrders(); Attachment ordersAttachment = GetOrdersAttachment(orders); await turnContext.SendActivityAsync(MessageFactory.Attachment(ordersAttachment), cancellationToken); break; case "help": var helpText = GetHelpText(); await turnContext.SendActivityAsync(MessageFactory.Text(helpText), cancellationToken); break; case "details": var detailOrders = await GetOrders(); Attachment detailOrdersAttachment = GetOrdersAttachment(detailOrders); await turnContext.SendActivityAsync(MessageFactory.Attachment(detailOrdersAttachment), cancellationToken); await _dialog.RunAsync(turnContext, _conversationState.CreateProperty<DialogState>(nameof(DialogState)), cancellationToken);; break; default: await turnContext.SendActivityAsync(MessageFactory.Text( "Hmmm... I don't know how to help you with that.\n\n" + "Try saying `help` to see what I can do." ), cancellationToken); break; } } public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default) { await base.OnTurnAsync(turnContext, cancellationToken); // Save any state changes that might have occurred during the turn. await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken); await _userState.SaveChangesAsync(turnContext, false, cancellationToken); } ... } 

OrderDetailDialog.cs

public class OrderDetailDialog : ComponentDialog { private readonly IOrderRepo _orderRepo; public OrderDetailDialog(UserState userState, IOrderRepo orderRepo) : base(nameof(OrderDetailDialog)) { _orderRepo = orderRepo; var waterfallSteps = new WaterfallStep[] { OrdersStep, ViewOrderDetailsStep }; AddDialog(new WaterfallDialog(nameof(WaterfallDialog), waterfallSteps)); AddDialog(new NumberPrompt<int>(nameof(NumberPrompt<int>), OrderNumberPromptValidatorAsync)); // The initial child Dialog to run. InitialDialogId = nameof(WaterfallDialog); } private static async Task<DialogTurnResult> OrdersStep(WaterfallStepContext stepContext, CancellationToken cancellationToken) { return await stepContext.PromptAsync(nameof(NumberPrompt<int>), new PromptOptions { Prompt = MessageFactory.Text("Please enter the order number."), RetryPrompt = MessageFactory.Text("The number must be from the list.") }, cancellationToken); } private static async Task<DialogTurnResult> ViewOrderDetailsStep(WaterfallStepContext stepContext, CancellationToken cancellationToken) { stepContext.Values["orderNumber"] = ((FoundChoice)stepContext.Result).Value; return await stepContext.EndDialogAsync(GetOrderDetailsCard(), cancellationToken); } private async Task<bool> OrderNumberPromptValidatorAsync(PromptValidatorContext<int> promptContext, CancellationToken cancellationToken) { var orders = await _orderRepo.GetOrders(); return promptContext.Recognized.Succeeded && orders.Select(x => x.Id).Contains(promptContext.Recognized.Value); } private static Attachment GetOrderDetailsCard() { var card = new AdaptiveCard(new AdaptiveSchemaVersion(1, 0)); card.Body.Add(new AdaptiveTextBlock("order details card attachment")); Attachment attachment = new Attachment() { ContentType = "application/vnd.microsoft.card.adaptive", Content = card }; return attachment; } } 
0

1 Answer 1

1

The problem you are experiencing is due to how you are using the OnMessageActivityAsync activity handler. And, really, the name of the handler says it all: on every message, do something. So, despite the conversation flow entering into a dialog every message sent is going to pass thru this activity handler. Regardless of where the user is in the flow, the default switch case is always going to be triggered if it doesn't match one of the other cases.

A better setup is to make use of the dialog system which can be a simple, but not overly flexible waterfall dialog or a component dialog. In your case, you opted for the component dialog which is good as you won't have to change too much.

For certain, you will need to update the Startup.cs file to include something like this under ConfiguredServices:

// The Dialog that will be run by the bot. services.AddSingleton<OrderDetailDialog >(); // Create the bot as a transient. In this case the ASP Controller is expecting an IBot. services.AddTransient<IBot, DialogBot<OrderDetailDialog >>(); 

And, your 'DialogBot.cs' file (you may have named it differently) would look something like this:

using System.Threading; using System.Threading.Tasks; using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Dialogs; using Microsoft.Bot.Schema; using Microsoft.Extensions.Logging; namespace Microsoft.BotBuilderSamples { public class DialogBot<T> : ActivityHandler where T : Dialog { protected readonly Dialog Dialog; protected readonly BotState ConversationState; protected readonly BotState UserState; protected readonly ILogger Logger; public DialogBot(ConversationState conversationState, UserState userState, T dialog, ILogger<DialogBot<T>> logger) { ConversationState = conversationState; UserState = userState; Dialog = dialog; Logger = logger; } public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default) { await base.OnTurnAsync(turnContext, cancellationToken); // Save any state changes that might have occurred during the turn. await ConversationState.SaveChangesAsync(turnContext, false, cancellationToken); await UserState.SaveChangesAsync(turnContext, false, cancellationToken); } protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken) { Logger.LogInformation("Running dialog with Message Activity."); // Run the Dialog with the new message Activity. await Dialog.RunAsync(turnContext, ConversationState.CreateProperty<DialogState>(nameof(DialogState)), cancellationToken); } } } 

While I do know the Bot Framework SDK fairly well, I'm more JS oriented than C#. So, please refer to this sample to be sure there aren't any other necessary changes to make. For example, implementing dialog state.

Edit:

I forgot to address the switch statement. I'm not entirely sure how you are using the order and help case statements, but it looks like they are only sending an activity. If that is true, they can continue to live in the OnMessageActivityAsync activity handler since, at most, they only perform one action.

For the details dialog, you can create a 'main' dialog that then filters on the activity. When 'details' is entered as text, it then begins the OrderDetailDialog. While it is more complicated, you can refer to 13.core-bot sample to get an idea on how to setup 'MainDialog.cs'. Additionally, if you wish, you could move the entire switch statement to 'MainDialog.cs' (with some modifications). And, if you really wanted to, you could move each of the actions under order and help to their own component dialogs should you want greater uniformity.

Sign up to request clarification or add additional context in comments.

6 Comments

Thanks so much for the reply. This is a bit hard to understand for me, especially the last paragraph, but I am looking through the 13.core-bot code. I do have a question. You are saying it's fine to keep order and help case statements in OnMessageActivityHandler method. But wouldn't the method still run whenever I begin and continue dialogs? If a user sends the message order for a dialog, wouldn't it hit the switch case order?
Well, that is why I mention that I'm not sure how you are using the order and help case statements. If there is a chance that the user might type those two words in as responses while in the details dialog and doing so would break the flow, then don't include them in the OnMessageActivityAsync activity handler. Instead, move them to the main dialog and handle them there.
There isn't much chance they'd use order and help as responses, but even if they did it would be okay to break the flow. I may be beginning to understand what you're saying after realizing some things about the code. Now my question is if I keep order and help case statements in OnMessageActivityHandler method, how would I know when to run the await Dialog.RunAsync(turn....), especially if I got the default: case? And also, would the detail command be in the case statement? Thank you so much for bearing with me.
Apologies, I somehow missed your having responded. You should be able to call the RunAsync() method after the switch statement.
As for the details command specifically, I think my response is the ideal setup (i.e., utilizing component dialogs for as much as you can). As your bot grows, it will be easier to manage the dialogs vs all your commands in a giant switch statement. Make use of the dialog system. It's there for a reason. Now, if you insist on the switch statement option, I think the real issue is the default case. Anything that doesn't match the cases will get that message. It would be better to do nothing there.
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.