I am using Bot framework V4.3, I want to retrieve adaptive card submit values - botframework

I'm using Bot framework V4.3, I have been using adaptive card in waterfall dialog, to get user information, I would want to get values once user clicks submit button and also I would like to go back to previous step if user click back button.
Here is how my adaptive card looks like
I have tried the solution given by #mdrichardson in Stack Overflow
But the adaptive card re-prompts again.
And the below code help us to go back to previous step but how to implement it to back button of adaptive card.
stepContext.ActiveDialog.State["stepIndex"] =(int)stepContext.ActiveDialog.State["stepIndex"] - 2;
Adding adaptive card to dialog. I had even used TextPrompt instead of ChoicePrompt
AddDialog(new ChoicePrompt("AdaptiveCardPrompt") { Style = ListStyle.None });
This is how I'm displaying adaptive card. My adaptive card is in Json format
cardAttachment = CreateAdaptiveCardAttachment();
return await stepContext.PromptAsync("AdaptiveCardPrompt",
new PromptOptions
{
Prompt = (Activity)MessageFactory.Attachment(new Attachment
{
ContentType = AdaptiveCard.ContentType,
Content = cardAttachment.Content
}),
}, cancellationToken);
Kindly help me in solving this issue. Thank you in advance
Edit from Botframework Support: Please do not use the code block below. It only works in Emulator. Instead, use:
if (string.IsNullOrWhiteSpace(activity.Text) && activity.Value != null)
{
activity.Text = JsonConvert.SerializeObject(activity.Value);
}
Edit 1: #mdrichardson Here is how I have setup the dialog call
public static async Task Run(this Dialog dialog, ITurnContext turnContext,IStatePropertyAccessor<DialogState> accessor, CancellationToken cancellationToken = default(CancellationToken))
{
var dialogSet = new DialogSet(accessor);
dialogSet.Add(dialog);
var dialogContext = await dialogSet.CreateContextAsync(turnContext, cancellationToken);
// Ensure that message is a postBack (like a submission from Adaptive Cards)
if (dialogContext.Context.Activity.GetType().GetProperty("ChannelData") != null)
{
var channelData = JObject.Parse(dialogContext.Context.Activity.ChannelData.ToString());
if (channelData.ContainsKey("postBack"))
{
var postbackActivity = dialogContext.Context.Activity;
// Convert the user's Adaptive Card input into the input of a Text Prompt
// Must be sent as a string
postbackActivity.Text = postbackActivity.Value.ToString();
await dialogContext.Context.SendActivityAsync(postbackActivity);
}
}
var results = await dialogContext.ContinueDialogAsync(cancellationToken);
if (results.Status == DialogTurnStatus.Empty)
{
await dialogContext.BeginDialogAsync(dialog.Id, null, cancellationToken);
}
}
And in OnTurnAsync method
if (turnContext.Activity.Type == ActivityTypes.Message)
{
await Dialog.Run(turnContext, ConversationState.CreateProperty<DialogState>(nameof(DialogState)), cancellationToken);
}
Edit 2 : I modified the code and I was able to go to next waterfall step. But I'm facing another issue here.
Next prompt is not getting displayed but I can see it in Log
This is how it shows in Emulator
Emulator View
Once user clicks the button control lands in MoreInfoAsync method
private async Task<DialogTurnResult> MoreInfoAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var goback = JObject.Parse(stepContext.Result.ToString());
stepContext.Values["AdaptiveCardDetails"] = stepContext.Result.ToString();
if (goback.ContainsKey("goBack"))
{
return await stepContext.ReplaceDialogAsync(InitialDialogId);
}
// stepContext.ActiveDialog.State["stepIndex"] = (int)stepContext.ActiveDialog.State["stepIndex"] - 2;
else
return await stepContext.PromptAsync("MoreInfo", new PromptOptions { Prompt = MessageFactory.Text("Tell Me more.") }, cancellationToken);
}
I would like to go to initial dialog so I'm using ReplaceDialogAsync.
MoreInfo dialog is not displayed in emulator but its shown in log
Edit 3: Here is the complete code of waterfall steps
// This array defines how the Waterfall will execute.
var waterfallSteps = new WaterfallStep[]
{
ChoiceAsync,
CardAsync,
MoreInfoAsync,
ConfirmAsync
};
AddDialog(new WaterfallDialog(nameof(WaterfallDialog), waterfallSteps));
AddDialog(new ChoicePrompt("ChoiceType"));
AddDialog(new TextPrompt("AdaptiveCardPrompt"));
AddDialog(new TextPrompt("MoreInfo"));
InitialDialogId = nameof(WaterfallDialog);
private async Task<DialogTurnResult> ChoiceAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
options = new PromptOptions()
{
Prompt = MessageFactory.Text("Select the Choice"),
RetryPrompt = MessageFactory.Text("That was not a valid choice."),
Choices = GetChoices(),
Style = ListStyle.HeroCard
};
return await stepContext.PromptAsync("ChoiceType", options, cancellationToken);
}
private async Task<DialogTurnResult> CardAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var cardAttachment = new Attachment();
stepContext.Values["leaveType"] = stepContext.Result.ToString();
cardAttachment = CreateAdaptiveCardAttachment();
return await stepContext.PromptAsync("AdaptiveCardPrompt",
new PromptOptions
{
Prompt = (Activity)MessageFactory.Attachment(new Attachment
{
ContentType = AdaptiveCard.ContentType,
Content = cardAttachment.Content,
}),
}, cancellationToken);
}
private async Task<DialogTurnResult> MoreInfoAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var goback = JObject.Parse(stepContext.Result.ToString());
stepContext.Values["AdaptiveCardDetails"] = stepContext.Result.ToString();
if (goback.ContainsKey("goBack"))
{
return await stepContext.ReplaceDialogAsync(InitialDialogId);
}
else return await stepContext.PromptAsync("MoreInfo", new PromptOptions { Prompt = MessageFactory.Text("Tell Me more.") }, cancellationToken);
}
private async Task<DialogTurnResult> ConfirmAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
stepContext.Values["MoreInfo"] = stepContext.Result;
//As of now I wouldn't perform any task here so I'll end
return await stepContext.EndDialogAsync();
}

Dealing with the Re-Prompt
The issue is with your OnTurnAsync() method:
if (turnContext.Activity.Type == ActivityTypes.Message)
{
await Dialog.Run(turnContext, ConversationState.CreateProperty<DialogState>(nameof(DialogState)), cancellationToken);
}
Every time a user sends a message, it causes a new instance of your dialog to be run. Since Adaptive Card Input gets sent as a PostBack message (which is still a message), it causes the Dialog to run again, re-prompting the user.
If you're going to run dialogs from OnTurnAsync() or OnMessageAsync(), there's a couple of different things you should do, either:
Use if/switch statements. For example, if the message contains "help", run the HelpDialog, or
Start a dialog that saves user responses and skips steps as necessary. You can see an example of this in Core Bot's Booking Dialog. Notice how it's saving the user response in each step with something like bookingDetails.TravelDate = (string)stepContext.Result; and checks to see if it exists in the previous step before prompting with something like if (bookingDetails.TravelDate == null). For yours, you might store something like userProfile.AdaptiveCardDetails or something.
Back Button
To get the back button working, let's say it looks like this in your Adaptive Card:
{
"type": "Action.Submit",
"title": "Back",
"data": {
"goBack": "true",
}
},
When the user clicks "Back", the bot will receive an activity with:
Since the user wants to go back and you don't need the data, you could do something like:
var activity = turnContext.Activity;
if (string.IsNullOrWhiteSpace(activity.Text) && activity.Value.GetType().GetProperty("goBack"))
{
dc.Context.Activity.Text = "Back";
}
and then in your Dialog step:
if (stepContext.Result == "Back")
{
stepContext.ActiveDialog.State["stepIndex"] = (int)stepContext.ActiveDialog.State["stepIndex"] - 2;
}

Related

suggested actions inside waterfall step

I am using Bot Framework .Net SDK v4 and direct line web chat client. I have my dialogs as waterfall steps. Is it possible to use a suggested action inside a waterfall step? I am aware that a choice prompt will serve the purpose but I want the buttons to disappear after user clicks any choice, hence want to use suggested actions.
What I have tried.
In the constructor :
AddDialog(new ChoicePrompt(nameof(ChoicePrompt)));
In waterfall step:
public async Task<DialogTurnResult> FirstStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var promptOptions = new PromptOptions()
{
Prompt = MessageFactory.Text("What card would you like to see? You can click or type the card name"),
RetryPrompt = MessageFactory.Text("That was not a valid choice, please select a card or number from 1 to 9."),
Choices = GetChoices(),
Style = ListStyle.SuggestedAction
};
return await stepContext.PromptAsync(nameof(ChoicePrompt), promptOptions);
}
private IList<Choice> GetChoices()
{
var cardOptions = new List<Choice>()
{
new Choice() { Value = "Card 1", Synonyms = new List<string>() { "adaptive" } },
new Choice() { Value = "Card 2", Synonyms = new List<string>() { "animation" } },
};
return cardOptions;
}
in the the prompt options you can specify you choices style :
var promptOptions = new PromptOptions {
Prompt = (Activity) MessageFactory.Attachment(card.ToAttachment()),
Style = ListStyle.SuggestedAction
};
return await stepContext.PromptAsync(nameof(ChoicePrompt), promptOptions, cancellationToken);
// Auto: Automatically select the appropriate style for the current channel.
// HeroCard: Add choices to prompt as a HeroCard with buttons.
// Inline : Add choices to prompt as an inline list.
// List : Add choices to prompt as a numbered list.
// None: Don't include any choices for prompt.
// SuggestedAction: Add choices to prompt as suggested actions.
source

Next step after PromptAsync is not called

I have this simple dialog, with 2 simple waterfall steps.
The user sees "How may I help you today?" and when it answers, nothing happens. I can't get Validate to work.
Am I missing something? I'm using SDK 4.1.5.
public ComplaintsDialog() : base(nameof(ComplaintsDialog))
{
var steps = new WaterfallStep[]
{
Ask,
Validate
};
AddDialog(new WaterfallDialog("flow", steps));
AddDialog(new TextPrompt("asking"));
}
private static async Task<DialogTurnResult> Ask(WaterfallStepContext sc, CancellationToken cancellationToken)
{
return await sc.PromptAsync("asking", new PromptOptions { Prompt = new Activity { Text = "How may I help you today?", Type= ActivityTypes.Message} }, cancellationToken);
}
private static async Task<DialogTurnResult> Validate(WaterfallStepContext sc, CancellationToken cancellationToken)
{
var answer = sc.Result;
await sc.Context.SendActivityAsync(answer.ToString());
return await sc.EndDialogAsync();
}
}
UPDATE
I tried to simplify the code, and this is how I currently call ComplaintsDialog directly from the main bot.
It looks like the stack is always empty when it gets to await dc.ContinueDialogAsync();, so it's going into a loop and start ComplaintsDialog over and over again
public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken)
{
// Create dialog context.
var dc = await _dialogs.CreateContextAsync(turnContext);
switch (turnContext.Activity.Type)
{
case ActivityTypes.Message:
// Continue outstanding dialogs.
await dc.ContinueDialogAsync();
// Begin main dialog if no outstanding dialogs/ no one responded.
if (!dc.Context.Responded)
{
await dc.BeginDialogAsync(nameof(ComplaintsDialog));
}
break;
case ActivityTypes.ConversationUpdate:
if (dc.Context.Activity.MembersAdded != null && dc.Context.Activity.MembersAdded.Any())
{
foreach (var newMember in dc.Context.Activity.MembersAdded)
{
if (newMember.Id != dc.Context.Activity.Recipient.Id)
{
await dc.BeginDialogAsync(nameof(WelcomeDialog));
}
}
}
break;
}
}
The code example you provided looks like it should be working so the problem is probably elsewhere.
My guess is that you are starting the ComplaintsDialog inside of a WaterfallStep (from another dialog) so make sure that you are calling the BeginDialogAsync method like this:
return await stepContext.BeginDialogAsync(nameof(ComplaintsDialog));
instead of:
await stepContext.BeginDialogAsync(nameof(ComplaintsDialog));
If this is not the error probably more information is necessary
Update
Your problem is on the OnTurnAsync method. You're not saving the new turn into the conversation state. The Message case on your switch should look like this:
case ActivityTypes.Message:
if (dc.ActiveDialog == null)
{
await dc.BeginDialogAsync(nameof(ComplaintsDialog), cancellationToken);
}
else
{
await dc.ContinueDialogAsync(cancellationToken);
}
await _accessors.ConversationState.SaveChangesAsync(turnContext);
break;
And your constructor:
private readonly MyBotAccessors _accessors;
public MyBot(MyBotAccessors accessors, ILoggerFactory loggerFactory)
{
...
_accessors = accessors ?? throw new System.ArgumentNullException(nameof(accessors));
...
}
SaveChangesAsync documentation

Return values from proactive dialog

My scenario is something like this, there are 2 users -> U1 and U2.
U1 wants the bot to ping U2 and get some information back
This is where i use proactive dialog based messages.
I am currently using the v3 Bot Builder SDK sample
using (var scope = DialogModule.BeginLifetimeScope(Microsoft.Bot.Builder.Dialogs.Conversation.Container, message))
{
var botData = scope.Resolve<IBotData>();
await botData.LoadAsync(CancellationToken.None);
//This is our dialog stack
var task = scope.Resolve<IDialogTask>();
// Interrupt the stack. This means that we're stopping whatever conversation that is currently happening with the user
// Then adding this stack to run and once it's finished, we will be back to the original conversation
var dialog = new GetFeedbackDialog(this.UserData);
task.Call(dialog.Void<object, IMessageActivity>(), null);
await task.PollAsync(CancellationToken.None);
// Flush the dialog stack back to its state store.
await botData.FlushAsync(CancellationToken.None);
}
class GetFeedbackDialog : IDialog<object>
{
private RequestFeedbackData userData;
public GetFeedbackDialog()
{
}
public GetFeedbackDialog(RequestFeedbackData userData)
{
this.userData = userData;
}
public async Task StartAsync(IDialogContext context)
{
await context.PostAsync($"Hello, Please provide feedback for "+ userData.OrganizerName+" on "+userData.FeedbackSubject);
context.Wait(this.MessageReceivedAsync);
}
public virtual async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> result)
{
var message = await result;
if (message.Text.ToLower().Equals("quit"))
{
await context.PostAsync("Quitting.. Thank you for using FeedbackBot!");
context.Done(String.Empty); // Finish this dialog.
}
await context.PostAsync("Thank you for your feedback!");
context.Done(message.Text);
}
}
How do can i access the response typed by U2 in GetFeedbackDialog from where i initiated the proactive message ?
Thanks in advance for your help

A very long message for LUIS dialog resets dialog state

Using bot framework emulator v.3.5.36, if a user sends long text (about 1K characters), emulator silently resets dialog stack back to root dialog, without any errors or warnings. (see the screenshot below.)
Is there a declared message limit for bot framework?
Is there a way for bot to handle such situations and warn user instead of this silent something?
There is nothing really specific about the code at all:
[LuisModel("{GUID}", "{CODE}", LuisApiVersion.V2, domain: "westeurope.api.cognitive.microsoft.com", threshold: 0.5)]
[Serializable]
public class LuisSearchDialog2 : LuisDialog<object>
{
[LuisIntent("")]
[LuisIntent("None")]
public async Task None(IDialogContext context, LuisResult result)
{
await context.PostAsync(JsonConvert.SerializeObject(result));
context.Wait(this.MessageReceived);
}
}
A simple approach would be to check the length of your message in the MessageController and decide whether you want to process it or not.
public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
if (activity.Type == ActivityTypes.Message)
{
MicrosoftAppCredentials.TrustServiceUrl(activity.ServiceUrl);
var connector = new ConnectorClient(new Uri(activity.ServiceUrl));
if (activity.Text != null && activity.Text.Length > 200)
{
var errorReply = activity.CreateReply();
errorReply.Text = "Well well, that is too much of data. How about keeping it simple? How can I help you?";
await connector.Conversations.ReplyToActivityAsync(errorReply);
}
else
{
await Conversation.SendAsync(activity, () => new Dialogs.RootDialog());
}
}
}
The reason is that base LuisDialog doens't handle failed API requests (in case if query is too long, it returns 414 code). So the simplest way to handle such errors is to override MessageReceived as follows:
[Serializable]
public class LuisSearchDialog2 : LuisDialog<object>
{
protected override async Task MessageReceived(IDialogContext context, IAwaitable<IMessageActivity> activity)
{
try
{
await base.MessageReceived(context, activity);
}
catch(HttpRequestException e)
{
// Handle error here
//await context.PostAsync("Error: " + e.ToString());
context.Wait(this.MessageReceived);
}
}
}

Delayed dialog with resume handler

I have the following scenario I think I'm doing it wrong.
I have a RootDialog which calls a ResultDialog. The ResultDialog presents the user a list of results (using HeroCard).
The ResultDialog closes itself using context.Done(..) after the message was posted.
In the RootDialog- AfterResultDialog Resume handler I want to ask the user if he has found the matching result, using another dialog (NotificationDialog), but I want to do that after 30 seconds.
After some research, this seems like it must be done using proactive messages.
It this example, I found a way to post the NotificationDialog in a proactive way.
private async Task AfterResultDialog(IDialogContext context, IAwaitable<object> result)
{
var message = await result as IMessageActivity;
var conversationReference = context.Activity.ToConversationReference();
ConversationStarter.conversationReference = JsonConvert.SerializeObject(conversationReference);
t = new Timer(timerEvent);
t.Change(30000, Timeout.Infinite);
context.Wait<string>(NotificationDialogAfter);
}
public void timerEvent(object target)
{
t.Dispose();
ConversationStarter.Resume();
}
But the problem I have is that I'm interested in the result of this NotifcationDialog to know what the user wants to do next.
But all examples I found using proactive-messages do not regard the result of a proactive message with dialog:
public class ConversationStarter
{
public static string conversationReference;
public static async Task Resume()
{
var message = JsonConvert.DeserializeObject<ConversationReference>(conversationReference).GetPostToBotMessage();
var client = new ConnectorClient(new Uri(message.ServiceUrl));
using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, message))
{
var botData = scope.Resolve<IBotData>();
await botData.LoadAsync(CancellationToken.None);
var task = scope.Resolve<IDialogTask>();
// here it seems only to be possible to call a dialog using Void
var dialog = new NotificationDialog();
task.Call(dialog.Void<object, IMessageActivity>(), null);
await task.PollAsync(CancellationToken.None);
await botData.FlushAsync(CancellationToken.None);
}
}
}
The NotificationDialogAfter handler should decide based on the user input which dialog to call next:
private async Task NotificationDialogAfter(IDialogContext context, IAwaitable<string> result)
{
var whereToContinue = await result;
if (whereToContinue.Equals("Start over"))
{
context.ClearAllConversationDataKeys();
context.Call(new TagDialog(), this.TagDialogAfter);
}
else if (whereToContinue == "Tell friends")
{
context.Call(new TellFriendsDialog(), TellFriendsDialogAfter);
}
else if (whereToContinue == "Feedback")
{
context.Call(new FeedbackDialog(), this.FeedbackDialogAfter);
}
}
So what I basically want is that the result of the NotificationDialog is forwarded to the NotificationDialogAfter handler which the Root dialog is waiting for.
Is this even possible?
I solve the problem by defining static continue handlers (in GlobalContinueHandler), that I can provide inside the NotificationDialog, when calling other dialogs.
[Serializable]
public class NotificationDialog : IDialog<string>
{
public Task StartAsync(IDialogContext context)
{
PromptDialog.Choice(context, Resume, new List<string> { "yes", "no" },
"Found what you're looking for?");
return Task.CompletedTask;
}
private async Task Resume(IDialogContext context, IAwaitable<string> result)
{
var message = await result;
if (message == "yes")
{
context.Call(new SignupDialog(), GlobalContinueHandler.SignupDialogAfter);
}
else
{
context.Call(new FeedbackDialog(), GlobalContinueHandler.FeedbackDialogAfter);
}
}
}
I'm really not fan of this solution but for now it seems to work.

Resources