2
votes

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.

1

1 Answers

2
votes

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.