1
votes

I am developing a chatbot using Microsoft Bot Framework (for .NET), QnA Maker & QnAMakerDialog (https://github.com/garypretty/botframework/tree/master/QnAMakerDialog). The bot and the web project hosting the chatbox control are deployed in Azure. I am using Direct Line as a channel.

The conversation flow is very simple. The user starts on a main branch. Based on user input, the conversation continues with a QnAMakerDialog or a custom dialog used for feedback.

The problem is as it follows:

The user starts in the main branch. As long as the user does not type 'end', I forward the conversation to the QnA dialog and try to give an answer to his question. At some point, the user types 'end'. So, I start the Feedback dialog. The user types the feedback. And now, he is supposed to be thanked for that feedback and sent back to the QnA dialog. Instead of this, he is being replied that no good answer was found in the QnA database of knowledge. This means that, somehow, he finds himself on the wrong branch! The bot thinks he is on the QnA branch, but in fact he should be on the feedback branch...

This error cannot be reproduced all the time, following the same steps. It happens randomly, without a pattern. Even more - it only happens in some environments. It never happens on my development machine, it very rarely happens on one environment and it happens very often on a third environment. (The two environments are configured almost identical and the problem cannot arise from there). Also, the problem cannot come from QnAMakerDialog – I made a test with a custom QnADialog which always returns a static message instead of an answer from QnAMaker and the problem is still present.

Here's the code. Any ideas are very much welcomed.

[BotAuthentication]
public class MessagesController : ApiController
{
    private readonly ILog log;
    public MessagesController(ILog log)
    {
        this.log = log;
    }

    internal static IDialog<object> MakeRoot()
    {
        return Chain.From(() => new HomeDialog());
    }

    public virtual async Task<HttpResponseMessage> Post([FromBody] Activity activity)
    {
        var client = new ConnectorClient(new Uri(activity.ServiceUrl));
        try
        {
            switch (activity.GetActivityType())
            {
                case ActivityTypes.Message:
                    var typingReply = activity.CreateReply();
                    typingReply.Type = ActivityTypes.Typing;
                    await client.Conversations.ReplyToActivityAsync(typingReply);
                    await Conversation.SendAsync(activity, MakeRoot);
                    break;

                default:
                    HandleSystemMessage(activity);
                    break;
            }
        }
        catch (Exception ex)
        {
            var errorReply = activity.CreateReply();
            errorReply.Type = ActivityTypes.Message;
            errorReply.Text ="I'm sorry, I'm having issues understanding you. Let's try again.";

            await client.Conversations.ReplyToActivityAsync(errorReply);

            log.Error("Issue in the bot.", ex);
        }

        return new HttpResponseMessage(System.Net.HttpStatusCode.Accepted);
    }

    private Activity HandleSystemMessage(Activity message)
    {

        if (message.Type == ActivityTypes.DeleteUserData)
        {
        }
        else if (message.Type == ActivityTypes.ConversationUpdate)
        {
        }
        else if (message.Type == ActivityTypes.ContactRelationUpdate)
        {
        }
        else if (message.Type == ActivityTypes.Typing)
        {
        }
        else if (message.Type == ActivityTypes.Ping)
        {
        }
        return null;
    }
}

[Serializable]
public class HomeDialog : IDialog<object>
{
    public async Task StartAsync(IDialogContext context)
    {
            context.Wait(MessageReceivedAsync);
    }

    private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> result)
    {
            await RedirectToQnaDialog(context);
    }

    private async Task RedirectToQnaDialog(IDialogContext context)
    {
            await context.Forward(new QnaDialog(), QnaDialogResumeAfter, context.Activity, CancellationToken.None);
    }

    private async Task QnaDialogResumeAfter(IDialogContext context, IAwaitable<object> result)
    {
            var message = await result;

            PromptDialog.Text(context,
                ResumeAfterQuestionTyped,
                "Type your question or 'end' to end this conversation.",
                "Please retry", 3);
    }

    private async Task ResumeAfterQuestionTyped(IDialogContext context, IAwaitable<string> inputFromUser)
    {
            var question = await inputFromUser;

            if (question.ToLower().Equals("end"))
            {
                await context.PostAsync("You would really help me out by giving feedback. " +
                                        "What subjects should we include to provide answers for your questions?");
                context.Call(new FeedbackDialog(), FeedbackDialogResumeAfter);
            }
            else
            {
                await context.Forward(new QnaDialog(), QnaDialogResumeAfter, context.Activity, CancellationToken.None);
            }
    }

    private async Task FeedbackDialogResumeAfter(IDialogContext context, IAwaitable<object> result)
    {
            await context.PostAsync("Thank you for your feedback. You can now continue to ask me more questions.");

            context.Wait(MessageReceivedAsync);
    }


[Serializable]
public class QnaDialog : QnAMakerDialog
{
    public QnaDialog() : base(new QnAMakerService
    (new QnAMakerAttribute(ConfigurationManager.AppSettings["QnaSubscriptionKey"],
        ConfigurationManager.AppSettings["QnaKnownledgeBaseKey"],
        ConfigurationManager.AppSettings["QnaNotFoundReply"],
        Convert.ToDouble(ConfigurationManager.AppSettings["QnaPrecentageMatch"]), 5)))
    {
    }

    protected override async Task RespondFromQnAMakerResultAsync(IDialogContext context, IMessageActivity message,
        QnAMakerResults results)
    {
            if (results.Answers.Count > 0)
            {
                var response = results.Answers.First().Answer;

                await context.PostAsync(response);
            }
    }

    protected override async Task DefaultWaitNextMessageAsync(IDialogContext context, IMessageActivity message,
        QnAMakerResults result)
    {
            context.Done<IMessageActivity>(null);
    }


[Serializable]
public class FeedbackDialog : IDialog<object>
{
    public async Task StartAsync(IDialogContext context)
    {
            context.Wait(MessageReceivedAsync);
    }

    private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> result)
    {
            var message = await result;

            context.Done(message);
    }
  }

}

2
Are you able to debug the code when the issue is occuring? Context.Done() method might not be hit in your QnADialogAnita George

2 Answers

1
votes

I am posting the answer, because this might help others in the future:

The problem is that I was using the in-memory bot state while this is clearly documented in the Microsoft documentation that this should only be used for testing purposes.

 var store = new InMemoryDataStore(); // volatile in-memory store

 builder.Register(c => store)
     .Keyed<IBotDataStore<BotData>>(AzureModule.Key_DataStore)
     .AsSelf()
     .SingleInstance();

The built-in Azure Load Balancer forwards DirectLine’s requests randomly to one of the instances which is why the API is completely lost because each instance of the API has its own “state” in memory.

So, basically the fix would be to implement a state management for the bot and not use the default in-memory state.

0
votes

Since MessageReceivedAsync in HomeDialog is just showing a PromptDialog, FeedbackDialogResumeAfter should also just show a PromptDialog.

I think the following code will produce the desired behavior:

[Serializable]
public class HomeDialog : IDialog<object>
{
    public async Task StartAsync(IDialogContext context)
    {
        context.Wait(MessageReceivedAsync);
    }

    private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> result)
    {
        await QnaDialogResumeAfter(context, result);
    }

    private async Task QnaDialogResumeAfter(IDialogContext context, IAwaitable<object> result)
    {
        var message = await result;

        PromptDialog.Text(context,
            ResumeAfterQuestionTyped,
            "Type your question or 'end' to end this conversation.",
            "Please retry", 3);
    }

    private async Task ResumeAfterQuestionTyped(IDialogContext context, IAwaitable<string> inputFromUser)
    {
        var question = await inputFromUser;

        if (question.ToLower().Equals("end"))
        {
            await context.PostAsync("You would really help me out by giving feedback. " +
                                    "What subjects should we include to provide answers for your questions?");
            context.Call(new QnaDialog.FeedbackDialog(), FeedbackDialogResumeAfter);
        }
        else
        {
            await context.Forward(new QnaDialog(), QnaDialogResumeAfter, context.Activity, CancellationToken.None);
        }
    }

    private async Task FeedbackDialogResumeAfter(IDialogContext context, IAwaitable<object> result)
    {
        PromptDialog.Text(context,
            ResumeAfterQuestionTyped,
            "Thank you for your feedback. You can now continue to ask me more questions.",
            "Please retry", 3);
    }


    [Serializable]
    public class QnaDialog : QnAMakerDialog
    {
        public QnaDialog() : base(new QnAMakerService
        (new QnAMakerAttribute(ConfigurationManager.AppSettings["QnaSubscriptionKey"],
            ConfigurationManager.AppSettings["QnaKnownledgeBaseKey"],
            ConfigurationManager.AppSettings["QnaNotFoundReply"],
            Convert.ToDouble(ConfigurationManager.AppSettings["QnaPrecentageMatch"]), 5)))
        {
        }

        protected override async Task RespondFromQnAMakerResultAsync(IDialogContext context, IMessageActivity message,
            QnAMakerResults results)
        {
            if (results.Answers.Count > 0)
            {
                var response = results.Answers.First().Answer;

                await context.PostAsync(response);
            }
        }

        protected override async Task DefaultWaitNextMessageAsync(IDialogContext context, IMessageActivity message, QnAMakerResults result)
        {
            context.Done<IMessageActivity>(null);
        }


        [Serializable]
        public class FeedbackDialog : IDialog<object>
        {
            public async Task StartAsync(IDialogContext context)
            {
                context.Wait(MessageReceivedAsync);
            }

            private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> result)
            {
                var message = await result;

                context.Done(message);
            }
        }
    }
}