Bot Framework: Loop through Prompts - c#

I recently got into Microsoft's Bot Framework, and this will be one of my first exposure to asynchronous programming in C#. I am creating a prompt that is designed as a selection tree. Using an XML document, I designed a hierarchy of topics the user can select -- I then abstracted the parsing of the XML using a HelpTopicSelector class.
The flow is as follows:
User types "help"
Context forwards to HelpDialog
Help Dialog creates prompt with list of options provided by the HelpTopicSelector
When user selects a prompt option, HelpTopicSelector "selects" the choise and updates a new list of choices from the subtree
Create another prompt with updated topics
Repeat until the last selected topic is the last node - call Context.Done
The help dialog is called from a basic dialog as follows:
private async Task ActivityRecievedAsync(IDialogContext context, IAwaitable<object> result)
{
Activity activity = await result as Activity;
if (activity.Text == "test")
{
await context.PostAsync("works");
}
else if(activity.Text == "help")
{
await context.Forward(new HelpDialog(), this.ResumeAfterHelp, activity.AsMessageActivity(), System.Threading.CancellationToken.None);
await context.PostAsync("Done Selection!");
}
context.Wait(ActivityRecievedAsync);
}
I am almost certain the problem in my code lies in the "loop" nature of my HelpDialog, but I genuinely have no idea WHY it fails.
class HelpDialog : IDialog
{
public async Task StartAsync(IDialogContext context)
{
await context.PostAsync("Reached Help Dialog!");
context.Wait(ActivityRecievedAsync);
}
private async Task ActivityRecievedAsync(IDialogContext context, IAwaitable<object> result)
{
var message = await result;
await context.PostAsync("HelpDialog: Activity Received");
await HandleTopicSelection(context);
context.Wait(ActivityRecievedAsync);
}
private async Task HandleTopicSelection(IDialogContext context)
{
List<string> topics = HelpTopicSelector.Instance.Topics;
PromptDialog.Choice<string>(context, TopicSelectedAsync, topics, "Select A Topic:");
// Unecessary?
context.Wait(ActivityRecievedAsync);
}
private async Task TopicSelectedAsync(IDialogContext context, IAwaitable<string> result)
{
string selection = await result;
if (HelpTopicSelector.Instance.IsQuestionNode(selection))
{
await context.PostAsync($"You asked: {selection}");
HelpTopicSelector.Instance.Reset();
context.Done<string>(selection);
}
else
{
HelpTopicSelector.Instance.SelectElement(selection);
await HandleTopicSelection(context);
}
// Unecessary?
context.Wait(ActivityRecievedAsync);
}
}
What I Expect:
I believe the await keyword should hold a Task's execution until the awaited Task is done.
Similarily, I believe Context.Wait is called in the end of Tasks to loop back to the AcitivtyReceived method, which effectively makes the bot wait for a user input.
Assuming that logic is true, the help dialog enters in the StartAsync method and hands control to the ActivityReceivedAsync which responds to the "message" passed by Context.Forward of the parent dialog. Then, it awaits the HandleTopic method which is responsible for the prompt. The prompt continues execution in the TopicSelectedAsync as indicated by the ResumeAfter argument.
The TopicSelectedAsync method checks if the selected topic is at the end of the XML tree, and if so, ends the Dialog by calling Context.Done. Otherwise, it awaits another HandleTopic method, which recursively creates another prompt - effectively creating a loop until the dialog ends.
Given how hacky this looks, I wasn't surprised to face an error. The bot emulator throws a "Stack is Empty" exception
.
After attempting to debug with break points, I notice the HelpDialog abruptly ends and exits when it enters TopicSelectedAsync method (specifically when it awaits the result). Visual Studio throws the following exception:
invalid need: Expected Call, have Poll.
EXTRA NOTE:
I tried coding this logic inside my BasicDialog class initially without forwarding to any other dialog. To my surprise, it almost worked flawlessly.

This survey dialog sample is similar to your scenerio: https://github.com/Microsoft/BotBuilder-Samples/blob/45d0f8767d6b71b3a11b060c893521d5150ede7f/CSharp/core-proactiveMessages/startNewDialogWithPrompt/SurveyDialog.cs
Modifying it to be a help dialog:
[Serializable]
public class HelpDialog : IDialog
{
public async Task StartAsync(IDialogContext context)
{
PromptDialog.Choice<string>(context, TopicSelectedAsync, HelpTopicSelector.Instance.Topics, "Select A Topic:", attempts: 3, retry: "Please select a Topic");
}
private async Task TopicSelectedAsync(IDialogContext context, IAwaitable<object> result)
{
try
{
string selection = await result as string;
if (HelpTopicSelector.Instance.IsQuestionNode(selection))
{
await context.PostAsync($"You asked: {selection}");
HelpTopicSelector.Instance.Reset();
context.Done<string>(selection);
}
else
{
await this.StartAsync(context);
}
}
catch (TooManyAttemptsException)
{
await this.StartAsync(context);
}
}
}
Calling it from a parent dialog like this (using context.Call() instead of .Forward()):
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
{
Activity activity = await result as Activity;
if (activity.Text == "test")
{
await context.PostAsync("works");
context.Wait(MessageReceivedAsync);
}
else if (activity.Text == "help")
{
context.Call(new HelpDialog(), ResumeAfterHelp);
await context.PostAsync("Called help dialog!");
}
}
private async Task ResumeAfterHelp(IDialogContext context, IAwaitable<object> result)
{
var selection = await result as string;
context.Wait(MessageReceivedAsync);
}
When supplying a method for Context.Wait(), you are actually supplying a continuation delegate. The next message received from the user will be sent to the method last .Wait() 'ed on. If you are forwarding, or calling a separate dialog, the parent should not then also call .Wait(). Also, when calling context.Done(), there should not also be a .Wait() afterwards in the same dialog.

Related

Calling CarouselCardsDialog on Luis

Hi guys im trying to call a carouseldialog on my luis intent.
Luis Code:
[LuisIntent("Help")]
public async Task Help(IDialogContext context, LuisResult result)
{
context.Call(new CarouselCardsDialog(), DialogsCompleted);
}
private async Task DialogsCompleted(IDialogContext context, IAwaitable<object> result)
{
await context.PostAsync("Please choose from one of the topics above or Type in a new message.");
context.Wait(MessageReceived);
}
What happens is I have to type the message HELP 2x because on the first one, it appears as if nothing has happened, and it will only come out on the second attempt. Is there a way to call it like an attachment instead? I also notice that after loading the carousel any other message you enter after that will only return the carousel, I want it to retain the Luis dialog and simply call the carousel as an attachment.
There are 2 problems in your implementation as you noticed:
start of your CarouselCards dialog which is currently "not automatic"
end of this dialog as it's continuously displaynig the cards
Problem 1 - Start of Carousel Dialog
For the 1st problem it comes from the way you are sending the user to the dialog:
[LuisIntent("Help")]
public async Task Help(IDialogContext context, LuisResult result)
{
context.Call(new CarouselCardsDialog(), DialogsCompleted);
}
Here you are doing a Call so the child dialog will be started and... that's all. In fact, it's only waiting for your next input due to the implementation of the StartAsync in your child dialog:
public async Task StartAsync(IDialogContext context)
{
context.Wait(this.MessageReceivedAsync);
}
There are 2 solutions, both are working (and are doing more or less the same things), choose one (but not both, or you will loop!):
Solution #1 - Change the StartAsync content of your child dialog
It will look like this:
public async Task StartAsync(IDialogContext context)
{
// Force the call to MessageReceivedAsync instead of waiting
await MessageReceivedAsync(context, new AwaitableFromItem<IMessageActivity>(context.Activity.AsMessageActivity()));
}
Solution #2 - Change the way you send to your child: Forward a message instead of Call
Here you forward the last message (context.Activity) to the dialog.
[LuisIntent("Help")]
public async Task Help(IDialogContext context, LuisResult result)
{
//context.Call(new CarouselCardsDialog(), DialogsCompleted);
await context.Forward(new CarouselCardsDialog(), DialogsCompleted, context.Activity.AsMessageActivity(), CancellationToken.None);
}
Problem 2 - End of the dialog to stop displaying cards
As you mentioned the use of the sample here, you may not have ended the Dialog as you can see in the last line here:
public virtual async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> result)
{
var reply = context.MakeMessage();
reply.AttachmentLayout = AttachmentLayoutTypes.Carousel;
reply.Attachments = GetCardsAttachments();
await context.PostAsync(reply);
context.Wait(this.MessageReceivedAsync);
}
So change context.Wait(this.MessageReceivedAsync); to context.Done<object>(null); and you will end the dialog after displaying the cards.

Microsoft bot error - Exception: invalid need: expected Call, have Poll

I keep getting this error and I don't know how to fix it: Exception: invalid need: expected Call, have Poll
PromptDialog.Text(context, setEmail, "What is the contact's email? ");
PromptDialog.Text(context, setPhone, "What is the contact's phone number? ");
private async Task setPhone(IDialogContext context, IAwaitable<string> result)
{
this.contact1.Phone = await result;
ReturnContact(context, contact1);
}
private async Task setEmail(IDialogContext context, IAwaitable<string> result)
{
this.contact1.Email = await result;
ReturnContact(context, contact1);
}
the prompt dialogs are part of a different method. How do I prompt the user twice in a row without getting this error?
PromptDialog.Text was not designed to be called twice, because you need two different answers from a user, so in terms of botframework it is like two separate "transactions".
Rather than making a double call you need to create a cascade of calls, where you initiate the Phone question from the Email question handler:
[Serializable]
public class SomeDialog : IDialog<object>
{
public async Task StartAsync(IDialogContext context)
{
context.Wait(MessageReceivedAsync);
}
private async Task OnPhoneSet(IDialogContext context, IAwaitable<string> result)
{
var res = await result;
}
private async Task OnEmailSet(IDialogContext context, IAwaitable<string> result)
{
var res = await result;
PromptDialog.Text(context, OnPhoneSet, "What is the contact's phone number? ");
}
public virtual async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> argument)
{
var message = await argument;
PromptDialog.Text(context, OnEmailSet, "What is the contact's email? ");
}
}
The workflow is like the following:
User initiates a dialog for the first time. Callstack: StartAsync -> MessageReceivedAsync -> PromptDialog.Text(context, OnEmailSet). Now dialog is waiting for Email posted
User posts Email. Callstack: OnEmailSet -> PromptDialog.Text(context, OnPhoneSet. Now dialog is waiting for Phone posted
User posts Phone. Callstack: OnPhoneSet. On OnPhoneSet you'll do further actions, for example you can close dialog using Context.Done or something.

Leave my secondDialog and continue with RootDialog

I have a RootDialog, who contains 3 paths BTCDialog, LTCDialog and ETHDialog.
When I go through the first dialog like BTCDialog i execute my code, and I need to get out when my dialog is finish (for example because I need to launch LTCDialog)
I go through BTCDialog with
await context.Forward(new BTCDialog(), this.ResumeAfterDialog, activity, CancellationToken.None);
When I finish I get out to BTCDialog with
context.Done(argument);
So I executed my function in RootDialog
private async Task ResumeAfterDialog(IDialogContext context, IAwaitable<object> result)
{
var activity = await result as Activity;
context.Wait(MessageReceivedAsync);
}
My problem is when my method ResumeAfterDialog is executed I have an information (result) who contains a message I could use in my RootDialog
How write my method ResumeAfterDialog to continue the dialog with my user ? I would like execute directly the method MessageReceivedAsync
Assuming that your result is of type IMessageActivity, then you can just call the MessageReceivedAsync method instead of waiting for the next message.
private async Task ResumeAfterDialog(IDialogContext context, IAwaitable<object> result)
{
var activity = await result as Activity;
await MessageReceivedAsync(context, Awaitable.FromItem(activity));
}
As a side note, if your BTCDialog dialog will be always returning an IMessageActivity, you should update your dialog to be IDialog and in that way you can update your ResumeAfterDialog method to receive an IAwaitable<IMessageActivity> instead of IAwaitable<object> saving you for doing the cast.

Bot Framework, promptchoice a list of object

I want to ask a user which widget it wants to use after searching for widgets, which results in a list of widgets. I want to be able to click on the name of a widget, and then get the URL of the widget. When I run the following code, I get invalid need: expected Call, have Poll.
public async Task SelectAfterSearch(IDialogContext context, List<Widget> widgetlist)
{
PromptDialog.Choice(context, this.OnWidgetSelected, GetListOfWidgets("list"), "Which one do you want more information about?", "Not a valid option", 3);
}
public async Task OnWidgetSelected(IDialogContext context, IAwaitable<Widget> result)
{
var chosen = await result;
await context.PostAsync($"You have chosen {chosen.Name}: {chosen.Url}");
}
You are missing a context.Wait at the end of your OnWidgetSelected method.
public async Task OnWidgetSelected(IDialogContext context, IAwaitable<Widget> result)
{
var chosen = await result;
await context.PostAsync($"You have chosen {chosen.Name}: {chosen.Url}");
context.Wait(...) // => usually you Wait on the MessageReceived method.
}

Prompt the user for a string in child dialog in a bot

I am currently playing around with Bots and LUIS.
So I have a running Bot. In my RootDialog, I handle all the intents that I get from LUIS. Now I want to check if an Entity is missing for an intent.
if (result.Entities.Count == 0) {
var ct = new CancellationToken();
await context.Forward(new ParameterDialog(), ResumeAfterParameterDialog, message, ct);
If there is no Entity I'm creating a new child dialog.
public class ParameterDialog : IDialog<object> {
public async Task StartAsync(IDialogContext context) {
context.Wait(MessageReceivedAsync);
}
public async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> argument) {
argument = new PromptDialog.PromptString("Please enter a parameter", "please try again", 2);
var prompt = await argument;
await context.PostAsync($"Your Parameter is: {prompt}");
context.Done(prompt);
}
}
If I could get user input I would then pass it back to my parent dialog.
Now I don't really know how I can stop the Bot and let it wait for user input.
Can someone please explain how I can accomplish that?
Thank you!
You are missing a context.Call of the PromptString dialog you are creating.
The context.Call method expects a dialog and a 'callback' method (ResumeAfter) that will be called once the dialog completes (in this case, when PromptString completes).
In your scenario your code should look like:
public async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> argument)
{
var dialog = new PromptDialog.PromptString("Please enter a parameter", "please try again", 2);
context.Call(dialog, ResumeAfterPrompt)
}
private Task ResumeAfterPrompt(IDialogContext context, IAwaitable<string> result)
{
var parameter = await result;
context.Done(parameter);
}

Categories

Resources