Search code examples
botframework

Bot Framework v4 - IndexOutOfRangeException when 2 tabs are open


I made a bot with bot framework v4, using C#, and it's on a webpage, https://websitebotv2.azurewebsites.net/, if there's only 1 user it works fine but the moment I open it on a new tab it gives a IndexOutOfRangeException when I start the conversation.

What do I need to do to make it work with multiple tabs open?

When my bot stars it creates a waterfall dialog asking the name and greeting the user:

public dialogBotBot(dialogBotAccessors accessors, LuisRecognizer luis, QnAMaker qna)
    {
        // Set the _accessors 
        _accessors = accessors ?? throw new ArgumentNullException(nameof(accessors));
        // The DialogSet needs a DialogState accessor, it will call it when it has a turn context.
        _dialogs = new DialogSet(accessors.ConversationDialogState);
        
        // This array defines how the Waterfall will execute.
        var waterfallSteps = new WaterfallStep[] {
            NameStepAsync,
            NameConfirmStepAsync,
        };

        // The incoming luis variable is the LUIS Recognizer we added above.
        this.Recognizer = luis ?? throw new System.ArgumentNullException(nameof(luis));

        // The incoming QnA variable is the QnAMaker we added above.
        this.QnA = qna ?? throw new System.ArgumentNullException(nameof(qna));

        // Add named dialogs to the DialogSet. These names are saved in the dialog state.
        _dialogs.Add(new WaterfallDialog("details", waterfallSteps));
        _dialogs.Add(new TextPrompt("name"));

    }

Then I will save his name on UserProfile class, which contains the field Name and Context, the Context has the purpose of saving the conversation.

This works the first time, but if I open a new tab or refresh the current tab for a new conversation the bot will fetch the first conversation data.

The Exception is thrown in Startup.cs in:

services.AddBot<dialogBotBot>(options =>
       {
           options.CredentialProvider = new ConfigurationCredentialProvider(Configuration);

           // Catches any errors that occur during a conversation turn and logs them to currently
           // configured ILogger.
           ILogger logger = _loggerFactory.CreateLogger<dialogBotBot>();

           options.OnTurnError = async (context, exception) =>
           {
               logger.LogError($"Exception caught : {exception}");
               await context.SendActivityAsync(exception + "\nSorry, it looks like something went wrong.\n" + exception.Message);
           };
           

           // Create and add conversation state.
           var conversationState = new ConversationState(dataStore);
           options.State.Add(conversationState);

           // Create and add user state. 
           var userState = new UserState(dataStore);
           options.State.Add(userState);
       });

My onTurnAsync method is:

    public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
    {

        // Handle Message activity type, which is the main activity type for shown within a conversational interface
        // Message activities may contain text, speech, interactive cards, and binary or unknown attachments.
        // see https://aka.ms/about-bot-activity-message to learn more about the message and other activity types
        if (turnContext.Activity.Type == ActivityTypes.Message)
        {
            //Get the current user profile
            userProfile = await _accessors.UserProfile.GetAsync(turnContext, () => new UserProfile(), cancellationToken);

            userProfile.Contexto.Add(turnContext.Activity.Text);

            foreach (string s in userProfile.Contexto)
                await turnContext.SendActivityAsync(s);


            // Get the conversation state from the turn context.
            var state = await _accessors.CounterState.GetAsync(turnContext, () => new CounterState());

            // Bump the turn count for this conversation.
            state.TurnCount++;

            // Check LUIS model
            var recognizerResult = await this.Recognizer.RecognizeAsync(turnContext, cancellationToken);
            var topIntent = recognizerResult?.GetTopScoringIntent();
            // Get the Intent as a string
            string strIntent = (topIntent != null) ? topIntent.Value.intent : "";
            // Get the IntentScore as a double
            double dblIntentScore = (topIntent != null) ? topIntent.Value.score : 0.0;
            // Only proceed with LUIS if there is an Intent 
            // and the score for the Intent is greater than 95
            if (strIntent != "" && (dblIntentScore > 2))
            {
                switch (strIntent)
                {
                    case "None":
                        //add the bot response to contexto
                        await turnContext.SendActivityAsync("Desculpa, não percebi.");
                        break;
                    case "Utilities_Help":
                        //add the bot response to contexto
                        await turnContext.SendActivityAsync("Quero-te ajudar!\nO que precisas?");
                        break;
                    default:
                        // Received an intent we didn't expect, so send its name and score.
                        //add the bot response to contexto
                        await turnContext.SendActivityAsync($"Intent: {topIntent.Value.intent} ({topIntent.Value.score}).");
                        break;
                }
            }
            else
            {
                if (userProfile.Name == null)
                {
                    // Run the DialogSet - let the framework identify the current state of the dialog from the dialog stack and figure out what (if any) is the active dialog.
                    var dialogContext = await _dialogs.CreateContextAsync(turnContext, cancellationToken);
                    var results = await dialogContext.ContinueDialogAsync(cancellationToken);
                    // If the DialogTurnStatus is Empty we should start a new dialog.
                    if (results.Status == DialogTurnStatus.Empty)
                    {
                        await dialogContext.BeginDialogAsync("details", null, cancellationToken);
                    }
                }
                else
                {
                    var answers = await this.QnA.GetAnswersAsync(turnContext);
                    if (answers.Any() && answers[0].Score > 0.7)
                    {
                        // If the service produced one or more answers, send the first one.
                        await turnContext.SendActivityAsync(answers[0].Answer + "\n" + state.TurnCount);
                    }
                    else
                    {
                        var responseMessage = $"Ainda não sei a resposta mas vou averiguar\nPosso-te ajudar com mais alguma coisa?";

                        String connectionString = "Data Source=botdataserverv1.database.windows.net;" +
                                                 "Initial Catalog=botDataBase;" +
                                                 "User [email protected];" +
                                                 "Password=admin_123;";


                        SqlConnection connection = new SqlConnection(connectionString);

                        SqlDataAdapter adapter = new SqlDataAdapter();
                        SqlCommand command;

                        String sms = turnContext.Activity.Text;
                        float result = answers[0].Score;

                        String insertMessage = "insert into Mensagem(texto,contexto,grauCerteza)" +
                                               "values('" + sms + "', 'Falta apurar o contexto' ," + result + ")";

                        connection.Open();

                        command = new SqlCommand(insertMessage, connection);

                        adapter.InsertCommand = new SqlCommand(insertMessage, connection);
                        adapter.InsertCommand.ExecuteNonQuery();

                        command.Dispose();
                        connection.Close();


                        await turnContext.SendActivityAsync(responseMessage);
                    }
                }

                // Save the user profile updates into the user state.
                await _accessors.UserState.SaveChangesAsync(turnContext, false, cancellationToken);

                // Set the property using the accessor.
                await _accessors.CounterState.SetAsync(turnContext, state);

                // Save the new turn count into the conversation state.
                await _accessors.ConversationState.SaveChangesAsync(turnContext);
            }


        }
    }

Solution

  • Your problem is here:

    float result = answers[0].Score;
    

    You have:

    // Check to see if we have any answers
    if (answers.Any() && answers[0].Score > 0.7)
    {
        [...]
        // This is fine
    }
    else // else, WE HAVE NO ANSWERS
    {
        [...]
        // At this point, answers is an empty array, so answers[0] throws an IndexOutOfRangeException
        float result = answers[0].Score;
    

    The reason this happens on refresh is because the new tab's user uses the same User Id. The bot already knows their name, so doesn't show the dialog, and when it calls await this.Recognizer.RecognizeAsync(turnContext, cancellationToken);, the user hasn't entered anything in the new turnContext, so it returns an empty array.

    Sidenote: You can set the userID in WebChat with this:

    window.WebChat.renderWebChat(
          {
              directLine: directLine,
              userID: "USER_ID" // Make is use Math.random() or something if you want it to be random for each refresh
          },
          this.botWindowElement.nativeElement
      );