Microsoft Bot : How to capture Too Many Attempts i

2019-09-16 01:14发布

问题:

I have developed one BOT application, where i have used form flow to ask set of questions to User. Now i have requirement to capture event when user enters invalid option more than 3 times. I have found TooManyAttemptsException class for PromptDialog, but could not find the same for FormDialog. Is there any way to capture the too many attempts and stop user further attempt in a FormDialog?

//Sample code

{
    var enrollmentForm = new FormDialog<AppointmentQuery>(new Models.AppointmentQuery(), AppointmentForm.BuildForm, FormOptions.PromptInStart);
                        context.Call<AppointmentQuery>(enrollmentForm, this.ResumeAfterOptionDialog);

    private async Task ResumeAfterOptionDialog(IDialogContext context, IAwaitable<AppointmentQuery> result)
    {
        try
        {
            var message = await result;
        }
        catch (FormCanceledException e)
        {
            string reply = string.Empty;
            await this.StartOverAsync(context, reply);
        }
        catch (TooManyAttemptsException et)
        {
            await context.PostAsync($"Ooops! Too many attemps :(. But don't worry, I'm handling that exception and you can try again!");
            await this.StartOverAsync(context, "");
        }

        catch (Exception ex)
        {
            await context.PostAsync($"Failed with message: {ex.Message}");
        }
        finally
        {
            context.Wait(this.MessageReceivedAsync);
        }
    }
}

回答1:

FormFlow doesn't throw TooManyAttemptsException. However, a workaround is to perform your own field validation like this:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Microsoft.Bot.Builder.FormFlow;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.FormFlow.Advanced;
using System.Threading.Tasks;

namespace BugFormFlowBot2
{
    [Serializable]
    [Template(TemplateUsage.EnumSelectOne, "Which {&} were you working with? {||}")]
    [Template(TemplateUsage.EnumSelectMany, "Which {&} were you working with? {||}", ChoiceStyle = ChoiceStyleOptions.PerLine)]
    public class BugReport
    {
        public string Product { get; set; }

        public string Version { get; set; }

        public List<PlatformOptions> Platform { get; set; }

        [Prompt("What is the {&}")]
        [Describe("Description of the Problem")]
        public string ProblemDescription { get; set; }

        [Numeric(1, 3)]
        public int Priority { get; set; }

        private static int versionAttempts = 0;

        public static IForm<BugReport> BuildForm()
        {
            return new FormBuilder<BugReport>()
                    .Message("Welcome to Bug Report bot!")
                    .Field(new FieldReflector<BugReport>(nameof(Product))
                            .SetType(null)
                            .SetDefine((state, field) =>
                            {
                                foreach (var prod in GetProducts())
                                    field
                                        .AddDescription(prod, prod)
                                        .AddTerms(prod, prod);

                                return Task.FromResult(true);
                            }))
                    .Field(nameof(Version),
                        validate: async (state, response) =>
                        {
                            var result = new ValidateResult { IsValid = true, Value = response };

                            foreach (var segment in (response as string ?? "").Split('.'))
                            {
                                int digit;
                                if (!int.TryParse(segment, out digit))
                                {
                                    result.Feedback =
                                        "Version number must be numeric segments, optionally separated by dots. e.g. 7.2, 10, or 3.56";
                                    result.IsValid = false;

                                    if (++versionAttempts > 2)
                                        throw new TooManyAttemptsException("Too many attempts at the version number.");

                                    break;
                                }
                            }

                            return await Task.FromResult(result);
                        })
                    .Field(nameof(Platform))
                    .AddRemainingFields()
                    .Confirm(async (bugReport) =>
                     {
                         var response = new PromptAttribute(
                             $"You entered {bugReport.Product}, {bugReport.Version}, {bugReport.Platform}" +
                             $"{bugReport.ProblemDescription}, {bugReport.Priority}. Is this Correct?");
                         return await Task.FromResult(response);
                     })
                    .OnCompletion(async (context, bugReport) => 
                     {
                        await context.PostAsync("Thanks for the report!");
                     })
                    .Build();
        }

        static List<string> GetProducts()
        {
            return new List<string>
            {
                "Office",
                "SQL Server",
                "Visual Studio"
            };
        }
    }
}

Notice how the versionAttempts field is private static. Then look at the validation for the Version field and how it throws TooManyAttemptsException.

That doesn't completely solve the problem because FormFlow wraps all exceptions, in a FormCanceledException. The following code shows how to handle that too.

using System;
using System.Threading.Tasks;
using System.Web.Http;
using Microsoft.Bot.Connector;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.FormFlow;
using System.Net.Http;
using System.Net;

namespace BugFormFlowBot2
{
    [BotAuthentication]
    public class MessagesController : ApiController
    {
        internal static IDialog<BugReport> MakeRootDialog()
        {
            return Chain.From(() => FormDialog.FromForm(BugReport.BuildForm))
                        .Loop();
        }

        public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
        {
            if (activity?.Type == ActivityTypes.Message)
                try
                {
                    await Conversation.SendAsync(activity, MakeRootDialog);
                }
                catch (FormCanceledException fcEx) when(fcEx.InnerException is TooManyAttemptsException)
                {
                    ConnectorClient connector = new ConnectorClient(new Uri(activity.ServiceUrl));

                    Activity reply = activity.CreateReply(
                        $"Too Many Attempts at {fcEx.Last}. " +
                        $"Completed Steps: {string.Join(", ", fcEx.Completed)}");

                    await connector.Conversations.ReplyToActivityAsync(reply);
                }
                catch (FormCanceledException fcEx)
                {
                    ConnectorClient connector = new ConnectorClient(new Uri(activity.ServiceUrl));

                    Activity reply = activity.CreateReply(
                        $"Form cancelled at {fcEx.Last}. " +
                        $"Completed Steps: {string.Join(", ", fcEx.Completed)}");

                    await connector.Conversations.ReplyToActivityAsync(reply);
                }

            return Request.CreateResponse(HttpStatusCode.OK);
        }
    }
}

Notice the FormCanceledException handler in Post, using the when(fcEx.InnerException is TooManyAttemptsException), allowing you to know when the TooManyAttemptsException occurred. Interestingly, the FormCanceledException provides more information on the fields that have been completed, giving you more information on the state of the form at the time of the exception.

You can find this code in the BugFormFlowBot2 project in my BotDemos GitHub repository.