Search code examples
c#.net-corebotframework

How to create and run Dialog from Middleware with Microsoft BotFramework


Question

How can I conditionally create and run a Dialog from middleware without breaking the bot?

Context

I'm using the dotnetcore/13.core-bot sample.

I have a setup to run a custom spellchecking Middleware. I am trying to create a dialog from Middleware so that after the user types some misspelled input and ONLY when two or more spellcheck suggestions are found, the user gets the possible sentence interpretations and chooses from a HeroCard or similar.

From my middleware SpellcheckMiddleware.cs, myDialog.RunAsync(...) runs a dialog, however, after the middleware exits onTurnAsync(), I get a fatal error: "An item with the same key has already been added". That error occurs when the bot tries to continue MainDialog from MainDialog.cs which is the dialog that was setup in Startup.cs.

Bot emulator visual

enter image description here

Error capture within Visual Studio

("An item with the same key has already been added")

enter image description here

---

My code

The only thing I have changed from the sample code is creating these two files, one defines the dialog that resolves a spellcheck with multiple suggestions, and one that is middleware that should run the spellcheck dialog.

SpellcheckMiddleware.cs:

public class SpellCheckMiddleware : IMiddleware
{
    private readonly ConversationState conversationState;

    public SpellCheckMiddleware(
        IConfiguration configuration, 
        ConversationState conversationState)
    {
        this.conversationState = conversationState;
    }

    public async Task OnTurnAsync(
        ITurnContext turnContext, 
        NextDelegate next,
        CancellationToken cancellationToken = new CancellationToken())
    {            
        
        # Fake suggestions
        List<List<String>> suggestions = new List<List<String>>{ 
            new List<String>{'Info', 'Olympics'},
            new List<String>{'Info', 'Olympia'},
        };

        SpellcheckSuggestionsDialog myDialog = new SpellcheckSuggestionsDialog(suggestions);            

        await myDialog.RunAsync(
            turnContext, 
            conversationState.CreateProperty<DialogState>(nameof(DialogState)), 
            cancellationToken);
            
        await next(cancellationToken);
    }
}

SpellcheckSuggestionsDialog.cs:

class SpellcheckSuggestionsDialog : ComponentDialog
{
    // Create a prompt that uses the default choice recognizer which allows exact matching, or number matching.
    public ChoicePrompt SpellcheckPrompt { get; set; }
    public WaterfallDialog WaterfallDialog { get; set; }
    List<string> Choices { get; set; }

    internal SpellcheckSuggestionsDialog(
        IEnumerable<IEnumerable<string>> correctedSentenceParts)
    {
        SpellcheckPrompt = new ChoicePrompt(
            nameof(ChoicePrompt), 
            validator: null, 
            defaultLocale: null);
        WaterfallDialog = new WaterfallDialog(
            nameof(WaterfallDialog), 
            new WaterfallStep[]{
                SpellingSuggestionsCartesianChoiceAsync,
                EndSpellingDialogAsync
            });

        AddDialog(SpellcheckPrompt);
        AddDialog(WaterfallDialog);
        InitialDialogId = nameof(WaterfallDialog);

        // Get all possible combinations of the elements in the list of list. Works as expected.
        var possibleUtterances = correctedSentenceParts.CartesianProduct();

        // Generate a choices array with the flattened list
        Choices = new();
        foreach (var item in possibleUtterances) {
            System.Console.WriteLine(item.JoinStrings(" "));
            Choices.Add(item.JoinStrings(" "));                        
        }
    }

    private async Task<DialogTurnResult> SpellingSuggestionsCartesianChoiceAsync(
        WaterfallStepContext stepContext, 
        CancellationToken cancellationToken)
    {
        return await stepContext.PromptAsync(
            SpellcheckPrompt.Id, 
            new PromptOptions()
            {
                Choices = ChoiceFactory.ToChoices(Choices),
                RetryPrompt = MessageFactory.Text("Did you mean...?"),
                Prompt = MessageFactory.Text("Did you mean...?"),
                Style = ListStyle.HeroCard
            });
    }

    private async Task<DialogTurnResult> EndSpellingDialogAsync(
        WaterfallStepContext stepContext, 
        CancellationToken cancellationToken)
    {
        // Overriding text sent using the choosen correction.
        stepContext.Context.TurnState.Add("CorrectionChoice", stepContext.Result);
        var choosen_correction = stepContext.Context.TurnState.Get<string>("CorrectionChoice");
        stepContext.Context.Activity.Text = choosen_correction;

        return await stepContext.EndDialogAsync(null, cancellationToken);
    }
}

Solution

  • As of 2022, there is no sample from Microsoft showing dialogs being spawned from middleware, so that is likely not the intended way to use the framework. It may be possible, but then you're in a sense going against the framework, which is never advisable.

    In order to have a dialog that provides spellcheck suggestions when the user types with a typo, I suggest you make that dialog part of logic specified in the WaterFall Dialog steps in MainDialog.cs.

    AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[]
    {
        IntroStepAsync,
        SpellcheckStep, //new step to force user to choose a spellcheck suggestions
        ActStepAsync,
        FinalStepAsync,
    }));
    

    This comes with the drawback that if you need to spellcheck multiple user inputs in the conversation, then you will need to add multiple spellcheck steps, each customized to handle the input expected at the matching point in the conversation steps.