microsoft / botframework-sdk

Bot Framework provides the most comprehensive experience for building conversation applications.
MIT License
7.5k stars 2.44k forks source link

LUIS with FormFlow: How to open up a Form one after another #4300

Closed FabianTan93 closed 6 years ago

FabianTan93 commented 6 years ago

My bot is supposed to help delete appointment.

1) A prompt for user's nric will be done (in RetrieveAppt.cs) 2) Subsequently, if there is such user in my database, it should go on to prompt user to enter the apptId which he/she wants to delete (as there may be multiple appointments made by same person) (in DeleteAppt.cs)

Issue Description

Exception thrown: 'Microsoft.Bot.Builder.Internals.Fibers.InvalidNeedException' in Microsoft.Bot.Builder.dll

Code Example

RetrieveAppt.cs

using Microsoft.Bot.Builder.FormFlow;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;

namespace Bot.Models
{
    [Serializable]
    public class RetrieveAppt
    {
        [Prompt("Please provide your NRIC:")]
        public string Nric { get; set; }

        public override string ToString()
        {
            var builder = new StringBuilder();
            builder.AppendFormat(Nric);
            return builder.ToString();
        }

    }

}

DeleteAppt.cs


using Microsoft.Bot.Builder.FormFlow;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;

namespace Bot.Models
{
    [Serializable]
    public class DeleteAppt
    {
        [Prompt("Please enter the appointment id that you wish to delete/cancel :")]
        public string apptId { get; set; }

        public override string ToString()
        {
            var builder = new StringBuilder();
            builder.AppendFormat(apptId);
            return builder.ToString();
        }
    }
}

ApptLuisDialog.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.Luis;
using Microsoft.Bot.Builder.Luis.Models;
using System.Threading.Tasks;
using Microsoft.Bot.Builder.FormFlow;
using Microsoft.Bot.Connector;
using Bot.Models;
using System.Data.SqlClient;
using System.Globalization;

namespace Bot.Dialogs
{
    [LuisModel("I have my own key", "I have my own key")]
    [Serializable]
    class ApptLuisDialog : LuisDialog<ApptLuisDialog>
    {
        String sql = @"Data Source=(localdb)\MSSQLLocalDB; Initial Catalog=Temp.DB; User Id = (insert your username here); Password = (insert your password here); Integrated Security=true;MultipleActiveResultSets = true";

        private static IForm<RetrieveAppt> BuildRetrieveForm()
        {
            var builder = new FormBuilder<RetrieveAppt>();
            return builder.AddRemainingFields().Build();
        }

        private static IForm<DeleteAppt> BuildDeleteForm()
        {
            var builder = new FormBuilder<DeleteAppt>();
            return builder.AddRemainingFields().Build();
        }

        [LuisIntent("")]
        [LuisIntent("None")]
        public async Task None(IDialogContext context, LuisResult result)
        {
            System.Diagnostics.Debug.WriteLine("Entered here: B");
            await context.PostAsync("I'm sorry I don't understand you. However, I can help you to: \n\n" + "1) Retrieve Appointment \n\n" + "2) Create Appointment \n\n" + "3) Delete Appointment \n\n" + "4) Edit Appointment");
            context.Wait(MessageReceived);
        }

        [LuisIntent("RetrieveAppointment")]
        public async Task RetrieveAppointment(IDialogContext context, LuisResult result)
        {
            System.Diagnostics.Debug.WriteLine("Entered here: C");
            var form = new RetrieveAppt();
            var entities = new List<EntityRecommendation>(result.Entities);
            var retrieveAppt = new FormDialog<RetrieveAppt>(form, BuildRetrieveForm, FormOptions.PromptInStart);
            context.Call(retrieveAppt, RetrieveComplete);
        }

        private async Task RetrieveComplete(IDialogContext context, IAwaitable<RetrieveAppt> result)
        {
            RetrieveAppt appt = null;
            try
            {
                appt = await result;
            }
            catch (OperationCanceledException)
            {
                await context.PostAsync("You cancelled the form!");
                return;
            }

            if (appt != null)
            {
                //getting user's input value
                String nric = appt.Nric.ToString();
                List<string> apptInfo = new List<string>();
                //Create connection
                SqlConnection con = new SqlConnection(sql);
                //SQL Command
                SqlCommand cmd = new SqlCommand("SELECT * FROM Appointment a WHERE a.Nric ='" + nric + "'", con);
                //Open sql connection
                con.Open();

                SqlDataReader dr = cmd.ExecuteReader();
                while (dr.Read())
                {
                    String date = dr["AptDate"].ToString();
                    String[] temp = date.Split(null);

                    apptInfo.Add("Appointment ID: " + dr["ApptId"].ToString() + "\n\n"
                        + "Nric: " + dr["Nric"].ToString() + "\n\n"
                        + "Date: " + temp[0] + "\n\n"
                        + "Time: " + dr["AptStartTime"].ToString() + "\n\n"
                        + "Location: " + dr["Location"].ToString() + "\n\n"
                        + "Purpose: " + dr["Purpose"].ToString());
                }

                //Close sql connection
                dr.Close();
                con.Close();
                if (apptInfo.Count == 0)
                {
                    await context.PostAsync("You do not have an appointment/no such NRIC");
                }
                else
                {
                    for (int i = 0; i < apptInfo.Count(); i++)
                    {
                        await context.PostAsync("Your Appointment Info is: " + "\n\n" + apptInfo[i]);
                    }
                }

            }
            else
            {
                await context.PostAsync("Form returned empty response!");
            }

            context.Wait(MessageReceived);
        }

        [LuisIntent("DeleteAppointment")]
        public async Task DeleteAppointment(IDialogContext context, LuisResult result)
        {
            System.Diagnostics.Debug.WriteLine("Entered here: A");
            var form = new RetrieveAppt();
            var retrieveAppt = new FormDialog<RetrieveAppt>(form, BuildRetrieveForm, FormOptions.PromptInStart);
            context.Call(retrieveAppt, Delete);
        }

        private async Task Delete(IDialogContext context, IAwaitable<RetrieveAppt> result)
        {
            RetrieveAppt appt = null;
            try
            {
                appt = await result;
            }
            catch (OperationCanceledException)
            {
                await context.PostAsync("You cancelled the form!");
                return;
            }

            if (appt != null)
            {
                //getting user's input value
                String nric = appt.Nric.ToString().ToUpper();
                List<string> apptInfo = new List<string>();

                //SqlAdapter for inserting new records
                SqlDataAdapter sda = new SqlDataAdapter();

                //Create connection
                SqlConnection con = new SqlConnection(sql);

                //SQL Command to check existing patient
                SqlCommand cmd = new SqlCommand("SELECT * FROM Appointment a WHERE a.Nric ='" + nric + "'", con);

                //Open sql connection
                con.Open();

                SqlDataReader dr = cmd.ExecuteReader();
                while (dr.Read())
                {
                    String date = dr["AptDate"].ToString();
                    String[] temp = date.Split(null);

                    apptInfo.Add("Appointment ID: " + dr["ApptId"].ToString() + "\n\n"
                        + "Nric: " + dr["Nric"].ToString() + "\n\n"
                        + "Date: " + temp[0] + "\n\n"
                        + "Time: " + dr["AptStartTime"].ToString() + "\n\n"
                        + "Location: " + dr["Location"].ToString() + "\n\n"
                        + "Purpose: " + dr["Purpose"].ToString());
                }

                if (apptInfo.Count != 0)
                {
                    **//this is the part that has error, i can't prompt for the appointment id that user wants to delete**
                    System.Diagnostics.Debug.WriteLine("Entered here: AA");
                    var form = new DeleteAppt();
                    var deleteAppt = new FormDialog<DeleteAppt>(form, BuildDeleteForm, FormOptions.PromptInStart);
                    context.Call(deleteAppt, DeleteComplete);

                }
                else
                {
                    //Close sql connection
                    dr.Close();
                    con.Close();
                    await context.PostAsync("Invalid NRIC/No current appointment");
                }
            }
            else
            {
                await context.PostAsync("Form returned empty response!");
            }

            context.Wait(MessageReceived);
        }

    private async Task DeleteComplete(IDialogContext context, IAwaitable<DeleteAppt> result)
    {
        DeleteAppt appt = null;
        try
        {
            appt = await result;
        }
        catch (OperationCanceledException)
        {
            await context.PostAsync("You canceled the form!");
            return;
        }

        if (appt != null)
        {
            //getting user's input value
            String apptId = appt.apptId.ToString();
            List<string> newApptInfo = new List<string>();

            //SqlAdapter for inserting new records
            SqlDataAdapter sda = new SqlDataAdapter();

            //Create connection
            SqlConnection con = new SqlConnection(sql);

            //SQL Command to check existing patient
            String cmd = "DELETE FROM Appointment a WHERE a.ApptId ='" + apptId + "'";

            //Open sql connection
            con.Open();

            try
            {
                sda.InsertCommand = new SqlCommand(cmd, con);
                sda.InsertCommand.ExecuteNonQuery();
                //Close sql connection
                con.Close();
                await context.PostAsync("Appointment " + apptId + " cancelled successfully.");
            }
            catch (Exception ex)
            {
                System.Diagnostics.Debug.WriteLine("Exception caught: " + ex);
            }

        }

        else
        {
            await context.PostAsync("Form returned empty response!");
        }

        context.Wait(MessageReceived);

    }

}

}

Expected Behavior

For example, after bot prompts user to input NRIC, user inputs "123456". So let's say, there are 3 appointments linked to NRIC "123456". So it will show all 3 appointments (with the following details: apptId, apptDate, apptTime, locatoin) first.

Next, I want the bot to prompt the user for the appointment that he/she wants to delete base on the apptId. (But this prompt is not showing)

Actual Results

Exception thrown: 'Microsoft.Bot.Builder.Internals.Fibers.InvalidNeedException' in Microsoft.Bot.Builder.dll Help needed here definitely

EricDahlvang commented 6 years ago

Hi @FabianTan93

When making the call to context.Call(deleteAppt, DeleteComplete); there should not then also follow a call to context.Wait(MessageReceived); Please try changing the method to something like:

private async Task Delete(IDialogContext context, IAwaitable<RetrieveAppt> result)
        {
            RetrieveAppt appt = null;
            try
            {
                appt = await result;
            }
            catch (OperationCanceledException)
            {
                await context.PostAsync("You cancelled the form!");
                return;
            }

            if (appt != null)
            {
                //getting user's input value
                String nric = appt.Nric.ToString().ToUpper();
                List<string> apptInfo = new List<string>();

                //SqlAdapter for inserting new records
                SqlDataAdapter sda = new SqlDataAdapter();

                //Create connection
                SqlConnection con = new SqlConnection(sql);

                //SQL Command to check existing patient
                SqlCommand cmd = new SqlCommand("SELECT * FROM Appointment a WHERE a.Nric ='" + nric + "'", con);

                //Open sql connection
                con.Open();

                SqlDataReader dr = cmd.ExecuteReader();
                while (dr.Read())
                {
                    String date = dr["AptDate"].ToString();
                    String[] temp = date.Split(null);

                    apptInfo.Add("Appointment ID: " + dr["ApptId"].ToString() + "\n\n"
                        + "Nric: " + dr["Nric"].ToString() + "\n\n"
                        + "Date: " + temp[0] + "\n\n"
                        + "Time: " + dr["AptStartTime"].ToString() + "\n\n"
                        + "Location: " + dr["Location"].ToString() + "\n\n"
                        + "Purpose: " + dr["Purpose"].ToString());
                }

                if (apptInfo.Count != 0)
                {
                    **//this is the part that has error, i can't prompt for the appointment id that user wants to delete**
                    System.Diagnostics.Debug.WriteLine("Entered here: AA");
                    var form = new DeleteAppt();
                    var deleteAppt = new FormDialog<DeleteAppt>(form, BuildDeleteForm, FormOptions.PromptInStart);
                    context.Call(deleteAppt, DeleteComplete);

                   //**RETURN AFTER context.CALL**
                    return;
                }
                else
                {
                    //Close sql connection
                    dr.Close();
                    con.Close();
                    await context.PostAsync("Invalid NRIC/No current appointment");
                }
            }
            else
            {
                await context.PostAsync("Form returned empty response!");
            }

            context.Wait(MessageReceived);
        }
FabianTan93 commented 6 years ago

@EricDahlvang It works. May I ask for your kind explanation why the "return" statement added actually allows the subsequent prompt(in DeleteAppt.cs) to be "successfully prompted". (Pardon me for my bad phrasing). I would be keen to learn more. :)

EricDahlvang commented 6 years ago

Because it avoides the call to context.Wait(MessageReceived);

The prompt internally sets up a wait.

FabianTan93 commented 6 years ago

@EricDahlvang Thank you for the explanation. Greatly appreciate your discussion and help!

EricDahlvang commented 6 years ago

You can find the code for the Prompt dialog here: https://github.com/Microsoft/BotBuilder/blob/master/CSharp/Library/Microsoft.Bot.Builder/Dialogs/PromptDialog.cs#L1145