-1

I have been trying to use Microsoft Cognitive and AI toolkit with QnAMaker API, in order to create a simplistic chat bot.

While my normal qnaMakerAi chat bot works fine, there is an issue while I was trying to enhance it's feature and include the bot feedback within the response.

I have been following the exact code sample as is referred here.

The issue I'm having is:

Exception: Object reference not set to an instance of an object.
[File of type 'text/plain']. 

The debugger is giving error in the code section - (in the file WebApiConfig.cs)

    JsonConvert.DefaultSettings = () => new JsonSerializerSettings()
    {
        ContractResolver = new CamelCasePropertyNamesContractResolver(),
        Formatting = Newtonsoft.Json.Formatting.Indented,
        NullValueHandling = NullValueHandling.Ignore,
    };

I have also raised a detailed description of the issue in - https://github.com/Microsoft/BotBuilder/issues/4267.

Please check and suggest.

Based on the user comments, here is the code for MessagesController -

using System;
using System.Threading.Tasks;
using System.Web.Http;

using Microsoft.Bot.Connector;
using Microsoft.Bot.Builder.Dialogs;
using System.Web.Http.Description;
using System.Net.Http;
using QnABot.Dialogs;

namespace Microsoft.Bot.Sample.QnABot
{
    [BotAuthentication]
    public class MessagesController : ApiController
    {
        /// <summary>
        /// POST: api/Messages
        /// receive a message from a user and send replies
        /// </summary>
        /// <param name="activity"></param>
        [ResponseType(typeof(void))]
        public virtual async Task<HttpResponseMessage> Post([FromBody] Activity activity)
        {
            // check if activity is of type message
            if (activity.GetActivityType() == ActivityTypes.Message)
            {
                //await Conversation.SendAsync(activity, () => new RootDialog());
                await Conversation.SendAsync(activity, () => new QnaDialog());
            }
            else
            {
                HandleSystemMessage(activity);
            }
            return new HttpResponseMessage(System.Net.HttpStatusCode.Accepted);
        }

        private Activity HandleSystemMessage(Activity message)
        {
            if (message.Type == ActivityTypes.DeleteUserData)
            {
                // Implement user deletion here
                // If we handle user deletion, return a real message
            }
            else if (message.Type == ActivityTypes.ConversationUpdate)
            {
                // Handle conversation state changes, like members being added and removed
                // Use Activity.MembersAdded and Activity.MembersRemoved and Activity.Action for info
                // Not available in all channels
            }
            else if (message.Type == ActivityTypes.ContactRelationUpdate)
            {
                // Handle add/remove from contact lists
                // Activity.From + Activity.Action represent what happened
            }
            else if (message.Type == ActivityTypes.Typing)
            {
                // Handle knowing tha the user is typing
            }
            else if (message.Type == ActivityTypes.Ping)
            {

            }

            return null;
        }
    }
}

For QnADialog -

using Microsoft.Bot.Builder.Azure;
using Microsoft.Bot.Builder.CognitiveServices.QnAMaker;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Connector;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Threading.Tasks;
using System.Web;

namespace QnABot.Dialogs
{
    [Serializable]
    public class QnaDialog : QnAMakerDialog
    {
        public QnaDialog() : base(new QnAMakerService(new QnAMakerAttribute("b372e477-0a2f-4a5a-88d5-3a664d16a4c3", "4ee02ead3xxxxxx", "Sorry, I couldn't find an answer for that", 0.5)))
        {
        }

        protected override async Task RespondFromQnAMakerResultAsync(IDialogContext context, IMessageActivity message, QnAMakerResults result)
        {
            // answer is a string
            var answer = result.Answers.First().Answer;

            Activity reply = ((Activity)context.Activity).CreateReply();

            string[] qnaAnswerData = answer.Split(';');
            int dataSize = qnaAnswerData.Length;

            string title = qnaAnswerData[0];
            string description = qnaAnswerData[1];
            string url = qnaAnswerData[2];
            string imageURL = qnaAnswerData[3];

            HeroCard card = new HeroCard
            {
                Title = title,
                Subtitle = description,
            };

            card.Buttons = new List<CardAction>
            {
                new CardAction(ActionTypes.OpenUrl, "Learn More", value: url)
            };

            card.Images = new List<CardImage>
            {
                new CardImage( url = imageURL)
            };

            reply.Attachments.Add(card.ToAttachment());


            await context.PostAsync(reply);
        }

        protected override async Task DefaultWaitNextMessageAsync(IDialogContext context, IMessageActivity message, QnAMakerResults result)
        {
            // get the URL
            var answer = result.Answers.First().Answer;
            string[] qnaAnswerData = answer.Split(';');
            string qnaURL = qnaAnswerData[2];

            // pass user's question
            var userQuestion = (context.Activity as Activity).Text;

            context.Call(new FeedbackDialog(qnaURL, userQuestion), ResumeAfterFeedback);

        }

        private async Task ResumeAfterFeedback(IDialogContext context, IAwaitable<IMessageActivity> result)
        {
            if (await result != null)
            {
                await MessageReceivedAsync(context, result);
            }
            else
            {
                context.Done<IMessageActivity>(null);
            }
        }

    }
}

For FeedBackDialog -

using Microsoft.ApplicationInsights;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Connector;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;

namespace QnABot.Dialogs
{
    [Serializable]
    public class FeedbackDialog : IDialog<IMessageActivity>
    {
        private string qnaURL;
        private string userQuestion;

        public FeedbackDialog(string url, string question)
        {
            // keep track of data associated with feedback
            qnaURL = url;
            userQuestion = question;
        }

        public async Task StartAsync(IDialogContext context)
        {
            var feedback = ((Activity)context.Activity).CreateReply("Did you find what you need?");

            feedback.SuggestedActions = new SuggestedActions()
            {
                Actions = new List<CardAction>()
                {
                    new CardAction(){ Title = "", Type=ActionTypes.PostBack, Value=$"yes-positive-feedback" },
                    new CardAction(){ Title = "", Type=ActionTypes.PostBack, Value=$"no-negative-feedback" }
                }
            };

            await context.PostAsync(feedback);

            context.Wait(this.MessageReceivedAsync);
        }

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

            if (userFeedback.Text.Contains("yes-positive-feedback") || userFeedback.Text.Contains("no-negative-feedback"))
            {
                // create telemetry client to post to Application Insights 
                TelemetryClient telemetry = new TelemetryClient();

                if (userFeedback.Text.Contains("yes-positive-feedback"))
                {
                    // post feedback to App Insights
                    var properties = new Dictionary<string, string>
                    {
                        {"Question", userQuestion },
                        {"URL", qnaURL },
                        {"Vote", "Yes" }
                        // add properties relevant to your bot 
                    };

                    telemetry.TrackEvent("Yes-Vote", properties);
                }
                else if (userFeedback.Text.Contains("no-negative-feedback"))
                {
                    // post feedback to App Insights
                }

                await context.PostAsync("Thanks for your feedback!");

                context.Done<IMessageActivity>(null);
            }
            else
            {
                // no feedback, return to QnA dialog
                context.Done<IMessageActivity>(userFeedback);
            }
        }
    }
}
Nicolas R
  • 13,812
  • 2
  • 28
  • 57
Rakesh Kumar
  • 307
  • 1
  • 2
  • 10
  • If the code isn't **here**, in a [mcve], it is unlikely to be read. – mjwills Mar 07 '18 at 10:47
  • JsonConvert.DefaultSettings = () => new JsonSerializerSettings() { ContractResolver = new CamelCasePropertyNamesContractResolver(), Formatting = Newtonsoft.Json.Formatting.Indented, NullValueHandling = NullValueHandling.Ignore, }; The error is in the above section of the code. – Rakesh Kumar Mar 07 '18 at 10:49
  • What error is it giving? – mjwills Mar 07 '18 at 11:04
  • The error is - Exception: Object reference not set to an instance of an object. [File of type 'text/plain']. Also , a more detailed description of the issue is here - https://github.com/Microsoft/BotBuilder/issues/4267 – Rakesh Kumar Mar 07 '18 at 11:12
  • The block of code your are mentioning is the one where the process is passing for every exception. With only those details, we can't help you more – Nicolas R Mar 07 '18 at 12:58
  • One more thing: what does your answer in your QnA knowledge base look like? Because in the sample they are using strings with `;` and doing a split on those strings – Nicolas R Mar 07 '18 at 13:02
  • @nicolas R - that's right, however, the debugger has not been hit the break point at RespondFromQnAMakerResultAsync or DefaultWaitNextMessageAsync method in the QnADialog.cs file. It goes into the Post method in the Messages controller and then to the QnaDialog() constructor and then back into WebApiConfig.cs file in the JsonConvert.DefaultSettings Section. – Rakesh Kumar Mar 08 '18 at 01:05
  • Sorry but you will not have a valid answer while you don't provide your code of your Controller and dialog. We can't guess where your problem is – Nicolas R Mar 08 '18 at 07:34
  • @NicolasR - I have added the code for the MessagesController, QnADialog and FeedbackDialog, as part of the main question. Please check and let me know. – Rakesh Kumar Mar 08 '18 at 09:26

1 Answers1

2

1st, bad config

Ok, the 1st problem is the fact that you inverted 2 parameters in your QnaDialog declaration:

public QnaDialog() : base(new QnAMakerService(new QnAMakerAttribute("b372e477-0a2f-4a5a-88d5-3a664d16a4c3", "4ee02ead3xxxxxx", "Sorry, I couldn't find an answer for that", 0.5)))

The syntax is: Qn

public QnAMakerAttribute(string subscriptionKey, string knowledgebaseId,  ...

Here you inverted your Key and your knowledgebaseId. The Guid should be in 2nd position, not 1st.

Note that I modified your subscription key in the question and reply, you should note share them like that.

Code improvement

The sample that you used seems to be not valid:

  • in the case there is no match
  • when your answer is not made of a string with ; separator (like when you type "hi", the reply is "hello"

I added some security to avoid errors in those cases:

protected override async Task RespondFromQnAMakerResultAsync(IDialogContext context, IMessageActivity message, QnAMakerResults result)
{
    // answer is a string
    var answer = result.Answers.First().Answer;

    Activity reply = ((Activity)context.Activity).CreateReply();

    var qnaAnswerData = answer.Split(';');
    var dataSize = qnaAnswerData.Length;

    if (dataSize == 3)
    {
        var title = qnaAnswerData[0];
        var description = qnaAnswerData[1];
        var url = qnaAnswerData[2];
        var imageUrl = qnaAnswerData[3];

        var card = new HeroCard
        {
            Title = title,
            Subtitle = description,
            Buttons = new List<CardAction>
            {
                new CardAction(ActionTypes.OpenUrl, "Learn More", value: url)
            },
            Images = new List<CardImage>
            {
                new CardImage(url = imageUrl)
            },
        };

        reply.Attachments.Add(card.ToAttachment());
    }
    else
    {
        reply.Text = answer;
    }

    await context.PostAsync(reply);
}

protected override async Task DefaultWaitNextMessageAsync(IDialogContext context, IMessageActivity message, QnAMakerResults result)
{
    if (result.Answers.Count > 0)
    {
        // get the URL
        var answer = result.Answers.First().Answer;
        var qnaAnswerData = answer.Split(';');

        var dataSize = qnaAnswerData.Length;

        if (dataSize == 3)
        {
            var qnaUrl = qnaAnswerData[2];

            // pass user's question
            var userQuestion = (context.Activity as Activity).Text;

            context.Call(new FeedbackDialog(qnaUrl, userQuestion), ResumeAfterFeedback);
        }
        else
        {
            await ResumeAfterFeedback(context, new AwaitableFromItem<IMessageActivity>(null));
        }
    }
    else
    {
        await ResumeAfterFeedback(context, new AwaitableFromItem<IMessageActivity>(null));
    }
}

private async Task ResumeAfterFeedback(IDialogContext context, IAwaitable<IMessageActivity> result)
{
    if (await result != null)
    {
        await MessageReceivedAsync(context, result);
    }
    else
    {
        context.Done<IMessageActivity>(null);
    }
}
Nicolas R
  • 13,812
  • 2
  • 28
  • 57