/// <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)); }
/// <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)); }
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); }
/// <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)); }
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); }
/// <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)); }
/// <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)); }
// 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(); }
// 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; } }
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); }
/// <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); }
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); }
/// <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)); }
/// <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)); }
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); }
// 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(); }
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)); } }
/// <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); } }
/// <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 } }
/// <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)); }