예제 #1
0
        /// <summary>
        /// Confirm the selection before moving on to Restaurant choice.
        /// </summary>
        /// <param name="sc">Waterfall Step Context.</param>
        /// <param name="cancellationToken">Cancellation Token.</param>
        /// <returns>Dialog Turn Result.</returns>
        private async Task <DialogTurnResult> ConfirmSelectionBeforeBooking(WaterfallStepContext sc, CancellationToken cancellationToken)
        {
            var state = await ConversationStateAccessor.GetAsync(sc.Context);

            var reservation = state.Booking;

            var tokens = new StringDictionary
            {
                { "FoodType", reservation.Category },
                { "ReservationDate", reservation.ReservationDate?.ToShortDateString() },
                { "ReservationDateSpeak", reservation.ReservationDate?.ToSpeakString(true) },
                { "ReservationTime", reservation.ReservationTime?.ToShortTimeString() },
                { "AttendeeCount", reservation.AttendeeCount.ToString() }
            };

            var cardData = new ReservationConfirmCard
            {
                Category        = reservation.Category,
                Location        = reservation.Location,
                ReservationDate = reservation.ReservationDate?.ToShortDateString(),
                ReservationTime = reservation.ReservationTime?.ToShortTimeString(),
                AttendeeCount   = reservation.AttendeeCount.ToString()
            };

            var replyMessage = ResponseManager.GetCardResponse(
                RestaurantBookingSharedResponses.BookRestaurantConfirmationPrompt,
                new Card("ReservationConfirmCard", cardData),
                tokens);

            return(await sc.PromptAsync(Actions.ConfirmSelectionBeforeBookingStep, new PromptOptions { Prompt = replyMessage }, cancellationToken));
        }
예제 #2
0
        /// <summary>
        /// Prompt for the Food type if not already provided on the initial utterance.
        /// </summary>
        /// <param name="sc">Waterfall Step Context.</param>
        /// <param name="cancellationToken">Cancellation Token.</param>
        /// <returns>Dialog Turn Result.</returns>
        private async Task <DialogTurnResult> AskForFoodTypeAsync(WaterfallStepContext sc, CancellationToken cancellationToken)
        {
            var state = await ConversationStateAccessor.GetAsync(sc.Context, cancellationToken : cancellationToken);

            var reservation = state.Booking;

            // If we already have a Cuisine provided we skip to next step
            if (reservation.Category != null)
            {
                return(await sc.NextAsync(sc.Values, cancellationToken));
            }

            // Fixed test data provided at this time
            var foodTypes = SeedReservationSampleData
                            .GetListOfDefaultFoodTypes()
                            .Select(
                r => new FoodTypeInfo
            {
                TypeName = r.Category,
                ImageUrl = BotImageForFoodType(r.Category)
            }).ToList();

            var tokens = new Dictionary <string, object>
            {
                { "FoodTypeList", foodTypes.ToSpeechString(TemplateManager.GetString(BotStrings.Or), f => f.TypeName) }
            };

            state.Cuisine = foodTypes;

            var cards   = new List <Card>();
            var options = new PromptOptions()
            {
                Choices = new List <Choice>(),
            };

            foreach (var foodType in foodTypes)
            {
                cards.Add(new Card(
                              GetDivergedCardName(sc.Context, "CuisineChoiceCard"),
                              new CuisineChoiceCardData
                {
                    ImageUrl   = foodType.ImageUrl,
                    ImageSize  = AdaptiveImageSize.Stretch,
                    ImageAlign = AdaptiveHorizontalAlignment.Stretch,
                    Cuisine    = foodType.TypeName,
                }));

                options.Choices.Add(new Choice(foodType.TypeName));
            }

            var replyMessage = TemplateManager.GenerateActivity(
                RestaurantBookingSharedResponses.BookRestaurantFoodSelectionPrompt,
                cards,
                tokens);

            // Prompt for restaurant choice
            return(await sc.PromptAsync(Actions.AskForFoodType, new PromptOptions { Prompt = replyMessage, Choices = options.Choices }, cancellationToken));
        }
예제 #3
0
        private async Task <bool> ValidateAttendeeCount(PromptValidatorContext <int> promptContext, CancellationToken cancellationToken)
        {
            var state = await ConversationStateAccessor.GetAsync(promptContext.Context, cancellationToken : cancellationToken);

            if (promptContext.Recognized.Succeeded == true)
            {
                state.Booking.AttendeeCount = promptContext.Recognized.Value;
                return(true);
            }

            return(false);
        }
예제 #4
0
        /// <summary>
        /// Make the reservation.
        /// </summary>
        /// <param name="sc">Waterfall Step Context.</param>
        /// <param name="cancellationToken">Cancellation Token.</param>
        /// <returns>Dialog Turn Result.</returns>
        private async Task <DialogTurnResult> ProcessReservationAsync(WaterfallStepContext sc, CancellationToken cancellationToken)
        {
            var state = await ConversationStateAccessor.GetAsync(sc.Context, cancellationToken : cancellationToken);

            var reservation = state.Booking;

            // TODO Process reservation request here.
            // Simulate the booking process through a delay;
            await Task.Delay(16000);

            // Send an update to the user (this would be done asynchronously and through a proactive notification
            var tokens = new Dictionary <string, object>
            {
                { "Restaurant", reservation.BookingPlace.Name },
                { "Location", reservation.BookingPlace.Location },
                { "ReservationDate", reservation.ReservationDate?.ToShortDateString() },
                { "ReservationDateSpeak", reservation.ReservationDate?.ToSpeakString(TemplateManager, true) },
                { "ReservationTime", reservation.ReservationTime?.ToShortTimeString() },
                { "AttendeeCount", reservation.AttendeeCount.ToString() },
            };

            var cardData = new ReservationConfirmationData
            {
                ImageUrl        = reservation.BookingPlace.PictureUrl,
                ImageSize       = AdaptiveImageSize.Stretch,
                ImageAlign      = AdaptiveHorizontalAlignment.Center,
                BookingPlace    = reservation.BookingPlace.Name,
                Location        = reservation.BookingPlace.Location,
                ReservationDate = reservation.ReservationDate?.ToShortDateString(),
                ReservationTime = reservation.ReservationTime?.ToShortTimeString(),
                AttendeeCount   = reservation.AttendeeCount.ToString()
            };

            var replyMessage = TemplateManager.GetCardResponse(
                RestaurantBookingSharedResponses.BookRestaurantAcceptedMessage,
                new Card("ReservationConfirmationCard", cardData),
                tokens);

            await sc.Context.SendActivityAsync(replyMessage, cancellationToken);

            if (state.IsAction)
            {
                var actionResult = new ActionResult()
                {
                    ActionSuccess = true
                };
                return(await sc.EndDialogAsync(actionResult, cancellationToken));
            }

            state.Clear();
            return(await sc.EndDialogAsync(cancellationToken : cancellationToken));
        }
예제 #5
0
        private async Task <bool> ValidateReservationDate(PromptValidatorContext <IList <DateTimeResolution> > promptContext, CancellationToken cancellationToken)
        {
            var state = await ConversationStateAccessor.GetAsync(promptContext.Context, cancellationToken : cancellationToken);

            var reservation = state.Booking;

            if (promptContext.Recognized.Succeeded)
            {
                reservation.ReservationDate = DateTime.Parse(promptContext.Recognized.Value.First().Value);
                return(true);
            }

            return(false);
        }
예제 #6
0
        /// <summary>
        /// Prompt for Attendee Count if not already provided.
        /// </summary>
        /// <param name="sc">Waterfall Step Context.</param>
        /// <param name="cancellationToken">Cancellation Token.</param>
        /// <returns>Dialog Turn Result.</returns>
        private async Task <DialogTurnResult> AskForAttendeeCountAsync(WaterfallStepContext sc, CancellationToken cancellationToken)
        {
            var state = await ConversationStateAccessor.GetAsync(sc.Context, cancellationToken : cancellationToken);

            var reservation = state.Booking;

            if (reservation.AttendeeCount != null)
            {
                return(await sc.NextAsync(sc.Values, cancellationToken));
            }

            var reply = TemplateManager.GenerateActivity(RestaurantBookingSharedResponses.BookRestaurantAttendeePrompt);

            return(await sc.PromptAsync(Actions.AskAttendeeCountStep, new PromptOptions { Prompt = reply }, cancellationToken));
        }
예제 #7
0
        /// <summary>
        /// Prompt for Date if not already provided.
        /// If the user says "today at 6pm" then we have everything we need and the time prompt is skipped
        /// Otherwise if the user just says "today" they will then be prompted for time.
        /// </summary>
        /// <param name="sc">Waterfall Step Context.</param>
        /// <param name="cancellationToken">Cancellation Token.</param>
        /// <returns>Dialog Turn Result.</returns>
        private async Task <DialogTurnResult> AskForDateAsync(WaterfallStepContext sc, CancellationToken cancellationToken)
        {
            var state = await ConversationStateAccessor.GetAsync(sc.Context, cancellationToken : cancellationToken);

            var reservation = state.Booking;

            // If we have the ReservationTime already provided (slot filling) then we skip
            if (reservation.ReservationDate != null)
            {
                return(await sc.NextAsync(sc.Values, cancellationToken));
            }

            var reply = TemplateManager.GenerateActivity(RestaurantBookingSharedResponses.BookRestaurantDatePrompt);

            return(await sc.PromptAsync(Actions.AskReservationDateStep, new PromptOptions { Prompt = reply }, cancellationToken));
        }
예제 #8
0
        // This method is called by any waterfall step that throws an exception to ensure consistency
        protected async Task HandleDialogExceptions(WaterfallStepContext sc, Exception ex)
        {
            // send trace back to emulator
            var trace = new Activity(type: ActivityTypes.Trace, text: $"DialogException: {ex.Message}, StackTrace: {ex.StackTrace}");
            await sc.Context.SendActivityAsync(trace);

            // log exception
            TelemetryClient.TrackExceptionEx(ex, sc.Context.Activity, sc.ActiveDialog?.Id);

            // send error message to bot user
            await sc.Context.SendActivityAsync(ResponseManager.GetResponse(SharedResponses.ErrorMessage));

            // clear state
            var state = await ConversationStateAccessor.GetAsync(sc.Context);

            state.Clear();
        }
예제 #9
0
        // Helpers
        protected async Task GetLuisResult(DialogContext dc)
        {
            if (dc.Context.Activity.Type == ActivityTypes.Message)
            {
                var state = await ConversationStateAccessor.GetAsync(dc.Context);

                // Get luis service for current locale
                var locale       = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName;
                var localeConfig = Services.LocaleConfigurations[locale];
                var luisService  = localeConfig.LuisServices["$safeprojectname$"];

                // Get intent and entities for activity
                var result = await luisService.RecognizeAsync <$safeprojectname$LU>(dc.Context, CancellationToken.None);

                state.LuisResult = result;
            }
        }
예제 #10
0
        private async Task <bool> ValidateReservationTime(PromptValidatorContext <IList <DateTimeResolution> > promptContext, CancellationToken cancellationToken)
        {
            var state = await ConversationStateAccessor.GetAsync(promptContext.Context, cancellationToken : cancellationToken);

            var reservation = state.Booking;

            if (promptContext.Recognized.Succeeded)
            {
                // Add the time element to the existing date that we have
                var recognizerValue = promptContext.Recognized.Value.First();

                reservation.ReservationTime = DateTime.Parse(recognizerValue.Value);

                return(true);
            }

            return(false);
        }
예제 #11
0
        /// <summary>
        /// Validate the chosen time.
        /// </summary>
        /// <param name="promptContext">Prompt Validator Context.</param>
        /// <param name="cancellationToken">Cancellation Token.</param>
        /// <returns>Dialog Turn Result.</returns>
        private async Task <bool> ValidateAmbiguousTimePrompt(PromptValidatorContext <FoundChoice> promptContext, CancellationToken cancellationToken)
        {
            var state = await ConversationStateAccessor.GetAsync(promptContext.Context, cancellationToken : cancellationToken);

            if (promptContext.Recognized.Succeeded)
            {
                var timexFromNaturalLanguage = state.AmbiguousTimexExpressions.First(t => t.Value == promptContext.Recognized.Value.Value);
                if (!string.IsNullOrEmpty(timexFromNaturalLanguage.Key))
                {
                    var property = new TimexProperty(timexFromNaturalLanguage.Key);
                    state.Booking.ReservationTime = DateTime.Parse($"{property.Hour.Value}:{property.Minute.Value}:{property.Second.Value}");

                    return(true);
                }
            }

            return(false);
        }
예제 #12
0
        private async Task <bool> ValidateBookingSelectionConfirmation(PromptValidatorContext <bool> promptContext, CancellationToken cancellationToken)
        {
            var state = await ConversationStateAccessor.GetAsync(promptContext.Context, cancellationToken : cancellationToken);

            var reservation = state.Booking;

            if (promptContext.Recognized.Succeeded == true)
            {
                reservation.Confirmed = promptContext.Recognized.Value;

                var reply = TemplateManager.GenerateActivity(RestaurantBookingSharedResponses.BookRestaurantRestaurantSearching);
                await promptContext.Context.SendActivityAsync(reply, cancellationToken);

                return(true);
            }

            return(false);
        }
예제 #13
0
        /// <summary>
        /// Confirm the selection before moving on to Restaurant choice.
        /// </summary>
        /// <param name="sc">Waterfall Step Context.</param>
        /// <param name="cancellationToken">Cancellation Token.</param>
        /// <returns>Dialog Turn Result.</returns>
        private async Task <DialogTurnResult> ConfirmSelectionBeforeBookingAsync(WaterfallStepContext sc, CancellationToken cancellationToken)
        {
            var state = await ConversationStateAccessor.GetAsync(sc.Context, cancellationToken : cancellationToken);

            var reservation = state.Booking;

            if (state.IsAction)
            {
                return(await sc.NextAsync(cancellationToken : cancellationToken));
            }

            var tokens = new Dictionary <string, object>
            {
                { "FoodType", reservation.Category },
                { "ReservationDate", reservation.ReservationDate?.ToShortDateString() },
                { "ReservationDateSpeak", reservation.ReservationDate?.ToSpeakString(TemplateManager, true) },
                { "ReservationTime", reservation.ReservationTime?.ToShortTimeString() },
                { "AttendeeCount", reservation.AttendeeCount.ToString() }
            };

            var cardData = new ReservationConfirmCard
            {
                Category        = reservation.Category,
                Location        = reservation.Location,
                ReservationDate = reservation.ReservationDate?.ToShortDateString(),
                ReservationTime = reservation.ReservationTime?.ToShortTimeString(),
                AttendeeCount   = reservation.AttendeeCount.ToString()
            };

            var replyMessage = TemplateManager.GetCardResponse(
                RestaurantBookingSharedResponses.BookRestaurantConfirmationPrompt,
                new Card("ReservationConfirmCard", cardData),
                tokens);

            // Workaround. In teams, HeroCard will be used for prompt and adaptive card could not be shown. So send them separatly
            if (Channel.GetChannelId(sc.Context) == Channels.Msteams)
            {
                await sc.Context.SendActivityAsync(replyMessage, cancellationToken);

                replyMessage = null;
            }

            return(await sc.PromptAsync(Actions.ConfirmSelectionBeforeBookingStep, new PromptOptions { Prompt = replyMessage }, cancellationToken));
        }
예제 #14
0
        /// <summary>
        /// Prompt for Time if not already provided.
        /// </summary>
        /// <param name="sc">Waterfall Step Context.</param>
        /// <param name="cancellationToken">Cancellation Token.</param>
        /// <returns>Dialog Turn Result.</returns>
        private async Task <DialogTurnResult> AskForTimeAsync(WaterfallStepContext sc, CancellationToken cancellationToken)
        {
            var state = await ConversationStateAccessor.GetAsync(sc.Context, cancellationToken : cancellationToken);

            var reservation = state.Booking;

            // Do we have a time from the previous date prompt (e.g. user said today at 6pm rather than just today)
            if (reservation.ReservationTime != null)
            {
                return(await sc.NextAsync(sc.Values, cancellationToken));
            }
            else if (state.AmbiguousTimexExpressions.Count > 0)
            {
                // We think the user did provide a time but it was ambiguous so we should clarify
                var ambiguousReply = TemplateManager.GenerateActivity(RestaurantBookingSharedResponses.AmbiguousTimePrompt);

                var choices = new List <Choice>();

                foreach (var option in state.AmbiguousTimexExpressions)
                {
                    var choice = new Choice(option.Value)
                    {
                        Synonyms = new List <string>()
                    };

                    // The timex natural language variant provides options in the format of "today 4am", "today 4pm" so we provide
                    // synonyms to make things easier for the user especially when using speech
                    var timePortion = option.Value.Split(' ');
                    if (timePortion != null && timePortion.Length == 2)
                    {
                        choice.Synonyms.Add(timePortion[1]);
                    }

                    choices.Add(choice);
                }

                return(await sc.PromptAsync(Actions.AmbiguousTimePrompt, new PromptOptions { Prompt = ambiguousReply, Choices = choices }, cancellationToken));
            }

            // We don't have the time component so prompt for time
            var reply = TemplateManager.GenerateActivity(RestaurantBookingSharedResponses.BookRestaurantTimePrompt);

            return(await sc.PromptAsync(Actions.AskReservationTimeStep, new PromptOptions { Prompt = reply }, cancellationToken));
        }
예제 #15
0
        private async Task <bool> ValidateRestaurantSelection(PromptValidatorContext <FoundChoice> promptContext, CancellationToken cancellationToken)
        {
            var state = await ConversationStateAccessor.GetAsync(promptContext.Context, cancellationToken : cancellationToken);

            // This is a workaround to a known issue with Adaptive Card button responses and prompts whereby the "Text" of the adaptive card button response
            // is put into a Value object not the Text as expected causing prompt validation to fail.
            // If the prompt was about to fail and the Value property is set with Text set to NULL we do special handling.
            if (!promptContext.Recognized.Succeeded && (promptContext.Context.Activity.Value != null) && string.IsNullOrEmpty(promptContext.Context.Activity.Text))
            {
                dynamic value          = promptContext.Context.Activity.Value;
                string  promptResponse = value["selectedItem"];  // The property will be named after your choice set's ID

                if (!string.IsNullOrEmpty(promptResponse))
                {
                    // Override what the prompt has done
                    promptContext.Recognized.Succeeded = true;
                    var foundChoice = new FoundChoice
                    {
                        Value = promptResponse
                    };
                    promptContext.Recognized.Value = foundChoice;
                }
            }

            if (promptContext.Recognized.Succeeded)
            {
                var restaurants = SeedReservationSampleData.GetListOfRestaurants(state.Booking.Category, "London", _urlResolver);
                var restaurant  = restaurants.First(r => r.Name == promptContext.Recognized.Value.Value);
                if (restaurant != null)
                {
                    state.Booking.BookingPlace = restaurant;

                    var reply = TemplateManager.GenerateActivity(RestaurantBookingSharedResponses.BookRestaurantBookingPlaceSelectionEcho, new Dictionary <string, object> {
                        { "BookingPlaceName", restaurant.Name }
                    });
                    await promptContext.Context.SendActivityAsync(reply, cancellationToken);

                    return(true);
                }
            }

            return(false);
        }
예제 #16
0
        // This method is called by any waterfall step that throws an exception to ensure consistency
        protected async Task HandleDialogExceptionsAsync(WaterfallStepContext sc, Exception ex, CancellationToken cancellationToken)
        {
            // send trace back to emulator
            var trace = new Activity(type: ActivityTypes.Trace, text: $"DialogException: {ex.Message}, StackTrace: {ex.StackTrace}");
            await sc.Context.SendActivityAsync(trace, cancellationToken);

            // log exception
            TelemetryClient.TrackException(ex, new Dictionary <string, string> {
                { nameof(sc.ActiveDialog), sc.ActiveDialog?.Id }
            });

            // send error message to bot user
            await sc.Context.SendActivityAsync(TemplateManager.GenerateActivity(RestaurantBookingSharedResponses.ErrorMessage), cancellationToken);

            // clear state
            var state = await ConversationStateAccessor.GetAsync(sc.Context, cancellationToken : cancellationToken);

            state.Clear();
        }
예제 #17
0
        protected async Task <DialogTurnResult> AfterGetAuthToken(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken))
        {
            try
            {
                // When the user authenticates interactively we pass on the tokens/Response event which surfaces as a JObject
                // When the token is cached we get a TokenResponse object.
                var skillOptions = (SkillTemplateDialogOptions)sc.Options;
                ProviderTokenResponse providerTokenResponse;
                if (skillOptions != null && skillOptions.SkillMode)
                {
                    var resultType = sc.Context.Activity.Value.GetType();
                    if (resultType == typeof(ProviderTokenResponse))
                    {
                        providerTokenResponse = sc.Context.Activity.Value as ProviderTokenResponse;
                    }
                    else
                    {
                        var tokenResponseObject = sc.Context.Activity.Value as JObject;
                        providerTokenResponse = tokenResponseObject?.ToObject <ProviderTokenResponse>();
                    }
                }
                else
                {
                    providerTokenResponse = sc.Result as ProviderTokenResponse;
                }

                if (providerTokenResponse != null)
                {
                    var state = await ConversationStateAccessor.GetAsync(sc.Context);

                    state.Token = providerTokenResponse.TokenResponse.Token;
                }

                return(await sc.NextAsync());
            }
            catch (Exception ex)
            {
                await HandleDialogExceptions(sc, ex);

                return(new DialogTurnResult(DialogTurnStatus.Cancelled, CommonUtil.DialogTurnResultCancelAllDialogs));
            }
        }
예제 #18
0
        /// <summary>
        /// Validate the Food Type when we have prmpted the user.
        /// </summary>
        /// <param name="promptContext">Prompt Validator Context.</param>
        /// <param name="cancellationToken">Cancellation Token.</param>
        /// <returns>Dialog Turn Result.</returns>
        private async Task <bool> ValidateFoodType(PromptValidatorContext <FoundChoice> promptContext, CancellationToken cancellationToken)
        {
            var state = await ConversationStateAccessor.GetAsync(promptContext.Context);

            // This is a workaround to a known issue with Adaptive Card button responses and prompts whereby the "Text" of the adaptive card button response
            // is put into a Value object not the Text as expected causing prompt validation to fail.
            // If the prompt was about to fail and the Value property is set with Text set to NULL we do special handling.
            if (!promptContext.Recognized.Succeeded && (promptContext.Context.Activity.Value != null) && string.IsNullOrEmpty(promptContext.Context.Activity.Text))
            {
                dynamic value          = promptContext.Context.Activity.Value;
                string  promptResponse = value["selectedItem"];  // The property will be named after your choice set's ID

                if (!string.IsNullOrEmpty(promptResponse))
                {
                    // Override what the prompt has done
                    promptContext.Recognized.Succeeded = true;
                    var foundChoice = new FoundChoice
                    {
                        Value = promptResponse
                    };
                    promptContext.Recognized.Value = foundChoice;
                }
            }

            if (promptContext.Recognized.Succeeded)
            {
                state.Booking.Category = promptContext.Recognized.Value.Value;

                var reply = ResponseManager.GetResponse(RestaurantBookingSharedResponses.BookRestaurantFoodSelectionEcho, new StringDictionary {
                    { "FoodType", state.Booking.Category }
                });
                await promptContext.Context.SendActivityAsync(reply, cancellationToken);

                return(true);
            }
            else
            {
                return(false);
            }
        }
예제 #19
0
        /// <summary>
        /// Initialise the Dialog.
        /// </summary>
        /// <param name="sc">Waterfall Step Context.</param>
        /// <param name="cancellationToken">Cancellation Token.</param>
        /// <returns>Dialog Turn Result.</returns>
        private async Task <DialogTurnResult> InitAsync(WaterfallStepContext sc, CancellationToken cancellationToken = default(CancellationToken))
        {
            var state = await ConversationStateAccessor.GetAsync(sc.Context, cancellationToken : cancellationToken);

            if (state.Booking == null)
            {
                state.Booking = new ReservationBooking();
                state.AmbiguousTimexExpressions = new Dictionary <string, string>();
            }

            // This would be passed from the Virtual Assistant moving forward
            var tokens = new Dictionary <string, object>
            {
                { "UserName", state.Name ?? string.Empty }
            };

            // Start the flow
            var reply = TemplateManager.GenerateActivity(RestaurantBookingSharedResponses.BookRestaurantFlowStartMessage, tokens);
            await sc.Context.SendActivityAsync(reply, cancellationToken);

            return(await sc.NextAsync(sc.Values, cancellationToken));
        }
        // Helpers
        protected async Task GetLuisResult(DialogContext dc)
        {
            if (dc.Context.Activity.Type == ActivityTypes.Message)
            {
                // Adaptive card responses come through with empty text properties
                if (!string.IsNullOrEmpty(dc.Context.Activity.Text))
                {
                    var state = await ConversationStateAccessor.GetAsync(dc.Context);

                    // Get luis service for current locale
                    var localeConfig = Services.GetCognitiveModels();
                    var luisService  = localeConfig.LuisServices["Restaurant"];

                    // Get intent and entities for activity
                    var result = await luisService.RecognizeAsync <ReservationLuis>(dc.Context, CancellationToken.None);

                    state.LuisResult = result;

                    // Extract key data out into state ready for use
                    await DigestLuisResult(dc, result);
                }
            }
        }
        protected async Task DigestLuisResult(DialogContext dc, ReservationLuis luisResult)
        {
            try
            {
                var state = await ConversationStateAccessor.GetAsync(dc.Context);

                // Extract entities and store in state here.
                if (luisResult != null)
                {
                    var entities = luisResult.Entities;

                    // Extract the cuisines out (already normalized to canonical form) and put in State thus slot-filling for the dialog.
                    if (entities.cuisine != null)
                    {
                        foreach (var cuisine in entities.cuisine)
                        {
                            var type = cuisine.First <string>();
                            state.Booking.Category = type;
                        }
                    }

                    if (entities.datetime != null)
                    {
                        var results = DateTimeRecognizer.RecognizeDateTime(dc.Context.Activity.Text, CultureInfo.CurrentUICulture.ToString());
                        if (results.Count > 0)
                        {
                            // We only care about presence of one DateTime
                            var result = results.First();

                            // The resolution could include two example values: one for AM and one for PM.
                            var distinctTimexExpressions = new HashSet <string>();
                            var values = (List <Dictionary <string, string> >)result.Resolution["values"];
                            foreach (var value in values)
                            {
                                // Each result includes a TIMEX expression that captures the inherent date but not time ambiguity.
                                // We are interested in the distinct set of TIMEX expressions.
                                if (value.TryGetValue("timex", out var timex))
                                {
                                    distinctTimexExpressions.Add(timex);
                                }
                            }

                            // Now we have the timex properties let's see if we have a definite date and time
                            // If so we slot-fill this and move on, if we don't we'll ignore for now meaning the user will be prompted
                            var timexProperty = new TimexProperty(distinctTimexExpressions.First());

                            if (timexProperty.Types.Contains(Constants.TimexTypes.Date) && timexProperty.Types.Contains(Constants.TimexTypes.Definite))
                            {
                                // We have definite date (no ambiguity)
                                state.Booking.ReservationDate = new DateTime(timexProperty.Year.Value, timexProperty.Month.Value, timexProperty.DayOfMonth.Value);

                                // Timex doesn't capture time ambiguity (e.g. 4 rather than 4pm)
                                if (timexProperty.Types.Contains(Constants.TimexTypes.Time))
                                {
                                    // If we have multiple TimeX
                                    if (distinctTimexExpressions.Count == 1)
                                    {
                                        // We have definite time (no ambiguity)
                                        state.Booking.ReservationTime = DateTime.Parse($"{timexProperty.Hour.Value}:{timexProperty.Minute.Value}:{timexProperty.Second.Value}");
                                    }
                                    else
                                    {
                                        // We don't have a distinct time so add the TimeEx expressions to enable disambiguation later and prepare the natural language versions
                                        foreach (var timex in distinctTimexExpressions)
                                        {
                                            var property = new TimexProperty(timex);
                                            state.AmbiguousTimexExpressions.Add(timex, property.ToNaturalLanguage(DateTime.Now));
                                        }
                                    }
                                }
                            }
                            else if (timexProperty.Types.Contains(Constants.TimexTypes.Time))
                            {
                                // We might have a time but no date (e.g. book a table for 4pm)
                                // If we have multiple timex (and time) this means we have a AM and PM component (e.g. ambiguous - book a table at 9)
                                if (distinctTimexExpressions.Count == 1)
                                {
                                    state.Booking.ReservationTime = DateTime.Parse($"{timexProperty.Hour.Value}:{timexProperty.Minute.Value}:{timexProperty.Second.Value}");
                                }
                            }
                            else
                            {
                                // We don't have a distinct time so add the TimeEx expressions to enable disambiguation later and prepare the natural language versions
                                foreach (var timex in distinctTimexExpressions)
                                {
                                    var property = new TimexProperty(timex);
                                    state.AmbiguousTimexExpressions.Add(timex, property.ToNaturalLanguage(DateTime.Now));
                                }
                            }
                        }
                    }

                    if (entities.geographyV2 != null)
                    {
                        state.Booking.Location = entities.geographyV2.First().Location;
                    }

                    // Establishing attendee count can be problematic as the number entity can be picked up for poorly qualified
                    // times, e.g. book a restaurant tomorrow at 2 for 4 people so we rely on a composite entity
                    if (entities.attendees != null)
                    {
                        var attendeesComposite = entities.attendees.First();
                        if (attendeesComposite != null)
                        {
                            int.TryParse(attendeesComposite.number.First().ToString(), out var attendeeCount);
                            if (attendeeCount > 0)
                            {
                                state.Booking.AttendeeCount = attendeeCount;
                            }
                        }
                    }
                }
            }
            catch
            {
                // put log here
            }
        }
예제 #22
0
        /// <summary>
        /// Prompt for Restaurant to book.
        /// </summary>
        /// <param name="sc">Waterfall Step Context.</param>
        /// <param name="cancellationToken">Cancellation Token.</param>
        /// <returns>Dialog Turn Result.</returns>
        private async Task <DialogTurnResult> AskForRestaurantAsync(WaterfallStepContext sc, CancellationToken cancellationToken)
        {
            var state = await ConversationStateAccessor.GetAsync(sc.Context, cancellationToken : cancellationToken);

            var reservation = state.Booking;

            if (state.IsAction)
            {
                return(await sc.NextAsync(cancellationToken : cancellationToken));
            }

            // Reset the dialog if the user hasn't confirmed the reservation.
            if (!reservation.Confirmed)
            {
                state.Booking = CreateNewReservationInfo();
                return(await sc.EndDialogAsync(cancellationToken : cancellationToken));
            }

            // Prompt for restaurant
            var restaurants = SeedReservationSampleData.GetListOfRestaurants(reservation.Category, "London", _urlResolver);

            state.Restaurants = restaurants;

            var restaurantOptionsForSpeak = new StringBuilder();

            for (var i = 0; i < restaurants.Count; i++)
            {
                restaurantOptionsForSpeak.Append(restaurants[i].Name);
                restaurantOptionsForSpeak.Append(i == restaurants.Count - 2 ? $" {TemplateManager.GetString(BotStrings.Or)} " : ", ");
            }

            var tokens = new Dictionary <string, object>
            {
                { "RestaurantCount", restaurants.Count.ToString() },
                { "ServerUrl", _urlResolver.ServerUrl },
                { "RestaurantList", restaurantOptionsForSpeak.ToString() }
            };

            var cards   = new List <Card>();
            var options = new PromptOptions()
            {
                Choices = new List <Choice>(),
            };

            foreach (var restaurant in restaurants)
            {
                cards.Add(new Card(
                              GetDivergedCardName(sc.Context, "RestaurantChoiceCard"),
                              new RestaurantChoiceCardData
                {
                    ImageUrl         = restaurant.PictureUrl,
                    ImageSize        = AdaptiveImageSize.Stretch,
                    ImageAlign       = AdaptiveHorizontalAlignment.Stretch,
                    Name             = restaurant.Name,
                    Title            = restaurant.Name,
                    Location         = restaurant.Location,
                    SelectedItemData = restaurant.Name
                }));

                options.Choices.Add(new Choice(restaurant.Name));
            }

            var replyMessage = TemplateManager.GenerateActivity(RestaurantBookingSharedResponses.BookRestaurantRestaurantSelectionPrompt, cards, tokens);

            return(await sc.PromptAsync(Actions.RestaurantPrompt, new PromptOptions { Prompt = replyMessage, Choices = options.Choices }, cancellationToken));
        }