Not able to get response from adaptive card in NLP Dispatch example - c#

I have Luis and QnA running simultaneously , by using Dispatch cli.
I was trying to implement Adaptive Card which is also working fine, i
can add/view any type easily. The issue arises when i try to get
response on Adaptive card submit button click. My bot backtracks it to
the switch statement where when the intent is not being traced a
default case is being triggered. On commenting the default i am
getting this error
System.Collections.Generic.KeyNotFoundException: The given key
‘luisResult’ was not present in the dictionary.
at System.Collections.Generic.Dictionary`2.get_Item(TKey key)
at Microsoft.BotBuilderSamples.DispatchBot.DispatchToTopIntentAsync(ITurnContext`1
turnContext, String intent, RecognizerResult recognizerResult,
CancellationToken cancellationToken) in
F:\nikhil\Alfi\mts-qna+luis\Bots\DispatchBot.cs:line 76
at Microsoft.BotBuilderSamples.DispatchBot.OnMessageActivityAsync(ITurnContext`1
turnContext, CancellationToken cancellationToken) in
F:\nikhil\Alfi\mts-qna+luis\Bots\DispatchBot.cs:line 45
at Microsoft.Bot.Builder.BotFrameworkAdapter.TenantIdWorkaroundForTeamsMiddleware.OnTurnAsync(ITurnContext
turnContext, NextDelegate next, CancellationToken cancellationToken)
in
d:\a\1\s\libraries\Microsoft.Bot.Builder\BotFrameworkAdapter.cs:line
964
at Microsoft.Bot.Builder.MiddlewareSet.ReceiveActivityWithStatusAsync(ITurnContext
turnContext, BotCallbackHandler callback, CancellationToken
cancellationToken) in
d:\a\1\s\libraries\Microsoft.Bot.Builder\MiddlewareSet.cs:line 55
at Microsoft.Bot.Builder.BotAdapter.RunPipelineAsync(ITurnContext turnContext, BotCallbackHandler callback, CancellationToken
cancellationToken) in
d:\a\1\s\libraries\Microsoft.Bot.Builder\BotAdapter.cs:line 167
I have tried this -
if (turnContext.Activity.Value != null)
{
var activity = turnContext.Activity;
activity.Text = JsonConvert.SerializeObject(activity.Value);
await turnContext.SendActivityAsync(MessageFactory.Text("activity.Text"), cancellationToken);
}
On debugging i can see the values like this -
activity.Value = {{ "startdate": "2017-10-12", "enddate": "2017-10-12" }}
namespace Microsoft.BotBuilderSamples
{
public class DispatchBot : ActivityHandler
{
private ILogger<DispatchBot> _logger;
private IBotServices _botServices;
public DispatchBot(IBotServices botServices, ILogger<DispatchBot> logger)
{
_logger = logger;
_botServices = botServices;
}
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
// First, we use the dispatch model to determine which cognitive service (LUIS or QnA) to use.
var recognizerResult = await _botServices.Dispatch.RecognizeAsync(turnContext, cancellationToken);
// Top intent tell us which cognitive service to use.
var topIntent = recognizerResult.GetTopScoringIntent();
// Next, we call the dispatcher with the top intent.
await DispatchToTopIntentAsync(turnContext, topIntent.intent, recognizerResult, cancellationToken);
}
protected override async Task OnMembersAddedAsync(IList<ChannelAccount> membersAdded, ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
{
const string WelcomeText = "I am here to make your experience much more easier";
foreach (var member in membersAdded)
{
if (member.Id != turnContext.Activity.Recipient.Id)
{
await turnContext.SendActivityAsync(MessageFactory.Text($"Hi {member.Name}, I am at your service . {WelcomeText}"), cancellationToken);
}
}
}
private async Task DispatchToTopIntentAsync(ITurnContext<IMessageActivity> turnContext, string intent, RecognizerResult recognizerResult, CancellationToken cancellationToken)
{
var activity = turnContext.Activity;
activity.Text = JsonConvert.SerializeObject(activity.Value);
switch (intent)
{
case "l_mts-bot-809f":
activity.Text = JsonConvert.SerializeObject(activity.Value);
await ProcessHomeAutomationAsync(turnContext, recognizerResult.Properties["luisResult"] as LuisResult, cancellationToken);
break;
case "q_mts-bot":
await ProcessSampleQnAAsync(turnContext, cancellationToken);
break;
default:
await ProcessHomeAutomationAsync(turnContext, recognizerResult.Properties["luisResult"] as LuisResult, cancellationToken);
Console.WriteLine(">>>>>>>>>>>"+recognizerResult.Properties["luisResult"]);
break;
// _logger.LogInformation($"Dispatch unrecognized intent: {intent}.");
// activity.Text = JsonConvert.SerializeObject(activity.Value);
// await turnContext.SendActivityAsync(MessageFactory.Text($"Dispatch unrecognized intent: {intent}."), cancellationToken);
// break;
}
}
private Activity CreateResponse(IActivity activity, Attachment attachment)
{
var response = ((Activity)activity).CreateReply();
response.Attachments = new List<Attachment>() { attachment };
return response;
}
private async Task ProcessHomeAutomationAsync(ITurnContext<IMessageActivity> turnContext, LuisResult luisResult, CancellationToken cancellationToken)
{
_logger.LogInformation("ProcessHomeAutomationAsync");
// Retrieve LUIS result for Process Automation.
var result = luisResult.ConnectedServiceResult;
var topIntent = result.TopScoringIntent.Intent;
var entity = result.Entities;
if (topIntent == "welcome")
{
await turnContext.SendActivityAsync(MessageFactory.Text("Hi,This is Alfie"), cancellationToken);
}
if (topIntent == "None")
{
await turnContext.SendActivityAsync(MessageFactory.Text("Sorry I didnt get you!"), cancellationToken);
}
if (topIntent == "LeaveApplication")
{
await turnContext.SendActivityAsync(MessageFactory.Text("Do you want to apply leaves for yourself or for someone else?"), cancellationToken);
}
if (topIntent == "LeaveSelfApplication")
{
var DatesRange = LeavesDatesAdaptiveCardAttachment();
var response = CreateResponse(turnContext.Activity, DatesRange);
await turnContext.SendActivityAsync(response, cancellationToken);
if (turnContext.Activity.Value != null)
{
var activity = turnContext.Activity;
activity.Text = JsonConvert.SerializeObject(activity.Value);
await turnContext.SendActivityAsync(MessageFactory.Text("2019-07-30"), cancellationToken);
}
//await turnContext.SendActivityAsync(MessageFactory.Text("2019-07-30"), cancellationToken);
}
if (topIntent == "LeavesDateTenure")
{
string jsonData = JsonConvert.SerializeObject(result);
dynamic json = JsonConvert.DeserializeObject(jsonData);
await turnContext.SendActivityAsync(MessageFactory.Text("Please Provide me with your Leaves Tenure"), cancellationToken);
}
}
//string jsonData = JsonConvert.SerializeObject(result);
private async Task ProcessSampleQnAAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
_logger.LogInformation("ProcessSampleQnAAsync");
var results = await _botServices.SampleQnA.GetAnswersAsync(turnContext);
if (results.Any())
{
await turnContext.SendActivityAsync(MessageFactory.Text(results.First().Answer), cancellationToken);
}
else
{
await turnContext.SendActivityAsync(MessageFactory.Text("Sorry, could not find an answer in the Q and A system."), cancellationToken);
}
}
// Load attachment from file.
private Attachment CreateAdaptiveCardAttachment()
{
// combine path for cross platform support
string[] paths = { ".", "Cards", "AddingLeaveDetails.json" };
string fullPath = Path.Combine(paths);
var adaptiveCard = File.ReadAllText(fullPath);
return new Attachment()
{
ContentType = "application/vnd.microsoft.card.adaptive",
Content = JsonConvert.DeserializeObject(adaptiveCard),
};
}
private Attachment LeavesDatesAdaptiveCardAttachment()
{
// combine path for cross platform support
string[] paths = { ".", "Cards", "LeavesDates.json" };
string fullPath = Path.Combine(paths);
var adaptiveCard = File.ReadAllText(fullPath);
return new Attachment()
{
ContentType = "application/vnd.microsoft.card.adaptive",
Content = JsonConvert.DeserializeObject(adaptiveCard),
};
}
}
}

Related

Waterfall Dialog not executes all steps when called from other Dialog

I'm building a bot connected to LUIS to get intents and with some dialogs that make other things.
I have the following dialogs:
MainDialog: that calls LUIS to get intents
DialogOne: with some steps
DialogTwo: with some steps
ScoreDialog: with a survey to get opinnion from user after other dialogs. this dialog can be called from others.
MainDialog is the entry point and this is the code:
public class MainDialog : ComponentDialog
{
public MainDialog() : base($"{nameof(MainDialog)}.main")
{
var waterfallSteps = new WaterfallStep[]
{
RedirectStepAsync
};
AddDialog(new WaterfallDialog($"{nameof(MainDialog)}.main", waterfallSteps));
AddDialog(new DialogOne($"{nameof(MainDialog)}.dialogOne"));
AddDialog(new DialogTwo($"{nameof(MainDialog)}.dialogTwo"));
InitialDialogId = $"{nameof(MainDialog)}.main";
}
private async Task<DialogTurnResult> RedirectStepAsync(WaterfallStepContext waterfallStepContext, CancellationToken cancellationToken)
{
await waterfallStepContext.Context.SendActivityAsync("What you want?");
var result = await _myLuisService.RecognizeAsync(waterfallStepContext.Context, cancellationToken);
var topIntent = result.GetTopScoringIntent();
switch (topIntent.intent)
{
case "None": break;
case "IntentOne":
await waterfallStepContext.BeginDialogAsync($"{nameof(MainDialog)}.dialogOne", null, cancellationToken);
break;
case "IntentTwo":
await waterfallStepContext.BeginDialogAsync($"{nameof(MainDialog)}.dialogTwo", null, cancellationToken);
break;
}
return await waterfallStepContext.EndDialogAsync(null, cancellationToken);
}
}
Note that MainDialog only have one step that calls to LUIS in order to get user intent.
When user intent is received I switch to another specific dialog with 'BeginDialogAsync' method
For example, we are switching to DialogOne.
This is the code:
public class DialogOne : ComponentDialog
{
public DialogOne(string dialogId) : base(dialogId)
{
var waterfallSteps = new WaterfallStep[]
{
StepOne,
StepTwo,
StepThree
};
AddDialog(new WaterfallDialog($"{nameof(DialogOne)}.main", waterfallSteps));
AddDialog(new TextPrompt($"{nameof(ScoreDialog)}.score"));
InitialDialogId = $"{nameof(DialogOne)}.main";
}
private async Task<DialogTurnResult> StepOne(WaterfallStepContext waterfallStepContext, CancellationToken cancellationToken)
{
// do something...
}
private async Task<DialogTurnResult> StepTwo(WaterfallStepContext waterfallStepContext, CancellationToken cancellationToken)
{
// do something...
}
private async Task<DialogTurnResult> StepThree(WaterfallStepContext waterfallStepContext, CancellationToken cancellationToken)
{
await waterfallStepContext.BeginDialogAsync($"{nameof(DialogOne)}.score", null, cancellationToken);
}
}
This DialogOne contains some steps, and when I finish, I want to show a new one dialog to ask their opinnion about the process. It's a votting process.
Note that I'm using await waterfallStepContext.BeginDialogAsync($"{nameof(DialogOne)}.score", null, cancellationToken); to call ScoreDialog
Finally, the ScoreDialog prompt two options to vote and then, in the next steps we capture and process the response, we thank the user for their vote...
public class ScoreDialog : ComponentDialog
{
public ScoreDialog(string dialogId) : base(dialogId)
{
var waterfallSteps = new WaterfallStep[]
{
AskForScoreStepAsync,
GratitudeForScoreStepAsync
};
AddDialog(new WaterfallDialog($"{nameof(ScoreDialog)}.main", waterfallSteps));
AddDialog(new TextPrompt($"{nameof(ScoreDialog)}.ask"));
InitialDialogId = $"{nameof(ScoreDialog)}.main";
}
private async Task<DialogTurnResult> AskForScoreStepAsync(WaterfallStepContext waterfallStepContext, CancellationToken cancellationToken)
{
await SaveConversationAsync(waterfallStepContext, data);
return await waterfallStepContext.PromptAsync(
$"{nameof(ScoreDialog)}.ask",
new PromptOptions()
{
Prompt = CreateScoreButtons()
},
cancellationToken);
}
private Activity CreateScoreButtons()
{
var reply = MessageFactory.Text(string.Empty);
reply.SuggestedActions = new SuggestedActions()
{
Actions = new List<CardAction>()
{
new() { Title = "I like", Value = "I like", Type = ActionTypes.ImBack },
new() { Title = "I dislike", Value = "I like", Type = ActionTypes.ImBack },
}
};
return reply;
}
private async Task<DialogTurnResult> GratitudeForScoreStepAsync(WaterfallStepContext waterfallStepContext, CancellationToken cancellationToken)
{
await waterfallStepContext.Context.SendActivityAsync("Thanks for voting!");
//TODO: do something with the vote
return await waterfallStepContext.EndDialogAsync(null, cancellationToken);
}
}
But, the issue is when I redirects to ScoreDialog. This dialog only shows the first step (AskForScoreStepAsync), but no continues to the last (AskForScoreStepAsync).
What is does is return to MainDialog with the value "I like" or "I dislike" from previous step... this "I like" or "I dislike" text is passed again to LUIS with wrong result (because isn't a valid utterance)
So, what's wrong?
How can I redirect the control from dialog to another dialog, and on this new one dialog, complete all steps?

How do you detect active Dialog in On MessageActivityAsync

I have created a DispatchBot using Luis and QnA and I want to additionally use multiple Dialogs (one for each intent.
Everything is working except if the dialog needs to prompt for a question. If you get the all the entities in the first utterance you can respond and all is good, however if you need to request further info that is when it fails. When the user responds it goes back to OnMessageActivityAsync and then forgets about the dialog.
I understand that I need to run RunAsync(..) to reopen the dialog however I can't get the right context. Everything I have tried either opens the dialog with null Accessors/DialogState or fails to open the dialog.
I am very new to Azure Bot Framework and I have spent days googling but each example doesn't do everything I need to do.
My bot is as follows:
public class DispatchBot : ActivityHandler
{
private readonly ILogger<DispatchBot> _logger;
private readonly IBotServices _botServices;
private readonly DialogSet _dialogSet;
private readonly MilaAccessors _milaAccessors;
private readonly BotState _userState;
public DispatchBot(IBotServices botServices, ILogger<DispatchBot> logger, MilaAccessors accessors)
{
_logger = logger;
_botServices = botServices;
_dialogSet = new DialogSet(accessors.ConversationDialogState);
_milaAccessors = accessors;
_userState = accessors.UserState;
}
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
await base.OnMessageActivityAsync(turnContext, cancellationToken);
await _milaAccessors.ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
List<Dialog> dialogs = _dialogSet.GetDialogs().ToList();
if (dialogs.Any()) //This is always false
{
//If count is greater than zero, then you can continue dialog conversation.
await dialogs.First().RunAsync(turnContext, _milaAccessors.ConversationState.CreateProperty<DialogState>(nameof(DialogState)), cancellationToken);
}
else
{
// First, we use the dispatch model to determine which cognitive service (LUIS or QnA) to use.
var recognizerResult = await _botServices.Dispatch.RecognizeAsync(turnContext, cancellationToken);
// Top intent tell us which cognitive service to use.
var topIntent = recognizerResult.GetTopScoringIntent();
// Next, we call the dispatcher with the top intent.
if (topIntent.score > 0.5)
{
await DispatchToTopIntentAsync(turnContext, topIntent.intent, recognizerResult, cancellationToken);
}
else
{
await ProcessQnAAsync(turnContext, cancellationToken);
}
}
}
private async Task DispatchToTopIntentAsync(ITurnContext<IMessageActivity> turnContext, string intent, RecognizerResult recognizerResult, CancellationToken cancellationToken)
{
switch (intent)
{
case "Form15":
await Form15IntentAsync(turnContext, recognizerResult.Properties["luisResult"] as LuisResult, cancellationToken);
break;
case "Layouts":
await ProcessLayoutsAsync(turnContext, recognizerResult.Properties["luisResult"] as LuisResult, cancellationToken);
break;
case "Weather":
await ProcessWeatherAsync(turnContext, recognizerResult.Properties["luisResult"] as LuisResult, cancellationToken);
break;
default:
await ProcessQnAAsync(turnContext, cancellationToken);
break;
}
}
}
And the Dialogs are of the form:
public class Form15Dialog : ComponentDialog
{
private const string UserInfo = "form15-userInfo";
private readonly MilaAccessors _milaAccessors;
private readonly string DlgAddressId = "AddressDlg";
private readonly string Form15Id = "Form15DialogName";
private readonly BotState _userState;
private readonly BotState _conversationState;
public Form15Dialog(MilaAccessors milaAccessors) : base(nameof(Form15Dialog))
{
_milaAccessors = milaAccessors;
_userState = milaAccessors.UserState;
_conversationState = milaAccessors.ConversationState;
AddDialog(new TextPrompt(DlgAddressId, AddressValidation));
AddDialog(new WaterfallDialog(nameof(Form15Id), new WaterfallStep[]
{
InitialiseStepAsync,
GetAddressStepAsync,
DisplayForm15StepAsync
}));
InitialDialogId = nameof(Form15Id);
}
private async Task<DialogTurnResult> InitialiseStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
stepContext.Values["MilaAccessors"] = _milaAccessors;
UserProfile userProfile = _milaAccessors.UserProfile.GetAsync(stepContext.Context, () => new UserProfile(), cancellationToken).Result;
Form15DialogValues userInfo = new Form15DialogValues {Name = userProfile.Name, MilaSessionId = userProfile.SessionId};
IList<EntityModel> options = (IList<EntityModel>)stepContext.Options;
foreach (EntityModel model in options)
{
switch (model.Type)
{
case "JobNumber":
userInfo.JobNumber = model.Entity;
break;
case "Sections":
userInfo.Sections = model.Entity;
break;
case "StreetAddress":
userInfo.StreetAddress = model.Entity;
break;
case "Suburb":
userInfo.Suburb = model.Entity;
break;
case "PostCode":
userInfo.PostCode = model.Entity;
break;
}
}
// Create an object in which to collect the user's information within the dialog.
stepContext.Values[UserInfo] = userInfo;
if (UpdateUserInfoFromWebService(userInfo))
{
stepContext.Values[UserInfo] = userInfo;
return await stepContext.NextAsync(new List<string>(), cancellationToken);
}
await _userState.SaveChangesAsync(stepContext.Context, false, cancellationToken);
await _conversationState.SaveChangesAsync(stepContext.Context, false, cancellationToken);
PromptOptions promptOptions = new PromptOptions { Prompt = MessageFactory.Text("Could you give me the full Job Address?") };
return await stepContext.PromptAsync(DlgAddressId, promptOptions, cancellationToken);
}
private async Task<DialogTurnResult> GetAddressStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
Form15DialogValues userInfo = (Form15DialogValues)stepContext.Values[UserInfo];
if (!(stepContext.Result is string[] results) || results.Length == 0)
{
return await stepContext.NextAsync(new List<string>(), cancellationToken);
}
userInfo.JobNumber = null;
userInfo.StreetAddress = results[0];
userInfo.Suburb = null;
if (UpdateUserInfoFromWebService(userInfo))
{
stepContext.Values[UserInfo] = userInfo;
return await stepContext.NextAsync(new List<string>(), cancellationToken);
}
await _userState.SaveChangesAsync(stepContext.Context, false, cancellationToken);
await _conversationState.SaveChangesAsync(stepContext.Context, false, cancellationToken);
var promptOptions = new PromptOptions { Prompt = MessageFactory.Text("I'm unable to find that address. Could you please enter the job number?") };
return await stepContext.PromptAsync(nameof(TextPrompt), promptOptions, cancellationToken);
}
I have been following the info in https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-dialog-manage-conversation-flow?view=azure-bot-service-4.0&tabs=csharp however I can not get "Dialog.RunAsync".
Any help/links/pointers you could give me would be most appreciated.
You can inject an instance of your dialog into the bot constructor like below
private readonly BotState _userState;
private readonly Dialog Dialog;
public DispatchBot(IBotServices botServices, ILogger<DispatchBot> logger, MilaAccessors accessors,Form15Dialog dialog)
{
_logger = logger;
_botServices = botServices;
_dialogSet = new DialogSet(accessors.ConversationDialogState);
_milaAccessors = accessors;
_userState = accessors.UserState;
Dialog = dialog;
}
Now you can run the below code in OnMessageActivityAsync to continue with your previous dialog
await Dialog.RunAsync(turnContext, ConversationState.CreateProperty<DialogState>(nameof(DialogState)), cancellationToken);
If you never call stepContext.EndDialogAsync WaterfallStep always finishes with the end of the Waterfall. In your code, it's DisplayForm15StepAsync and your dialog will be finished and goes back to OnMessageActivityAsync
So no need to reopen the active dialog, you should re-prompt at your WaterfallStep when it fails and doesn't let it finish until you get the right context

To route the QnAMaker to the hero card in the bot framework

I'm developing a chatbot using Microsoft Azure Web App Service. Among them, I'd like to insert a hero card in the greeting, and then click Info Dynamics365 among the corresponding hero buttons to start calling the card list source I've created, and if I press FAQ, I'd like to connect with QnAMaker for questioning. If you use ActionType now, both will call up a list of cards, one of which type should be used to connect to QnAMaker, or how to specify a path.
Bpts/QnABot.cs
namespace Microsoft.BotBuilderSamples
{
public class QnABot<T> : ActivityHandler
{
private BotState _conversationState;
private BotState _userState;
//QnAMaker
//protected readonly BotState ConversationState;
//protected readonly Microsoft.Bot.Builder.Dialogs.Dialog Dialog;
//protected readonly BotState UserState;
private string KBID = "fcd905a3-7269-4ea5-9a58-7b02c888ddb6";
private string ENDPOINT_KEY = "7b9a938a-e7ac-46f6-ab69-97d4e2e04f66";
private string HOST = "myfirstqa.azurewebsites.net";
//Azure 첫 세팅 소스
public QnABot(ConversationState conversationState, UserState userState)
{
_conversationState = conversationState;
_userState = userState;
}
public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
await base.OnTurnAsync(turnContext, cancellationToken);
// Save any state changes that might have occured during the turn.
await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken);
await _userState.SaveChangesAsync(turnContext, false, cancellationToken);
}
protected override async Task OnMembersAddedAsync(IList<ChannelAccount> membersAdded, ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
{
foreach (var member in membersAdded)
{
if (member.Id != turnContext.Activity.Recipient.Id)
{
var card = new HeroCard();
card.Title = "";
card.Text = #"Welcome to Welcome Users bot sample! This Introduction card";
card.Images = new List<CardImage>() { new CardImage("https://www.google.com/url?sa=i&source=images&cd=&ved=2ahUKEwjQjeeS4obmAhUIfnAKHQGgCB0QjRx6BAgBEAQ&url=https%3A%2F%2Fdougame.tistory.com%2F98&psig=AOvVaw11Y-BZJtsxh1pTp0Qxzedb&ust=1574819546545481") };
card.Buttons = new List<CardAction>()
{
new CardAction(ActionTypes.PostBack, "테스트 FAQ연결", null,"Connect QnA-Makeshfrjflrk todrur","Connect QnA-Maker", "Connection QnAMaker"),
new CardAction(ActionTypes.PostBack, "YOUTUBE LINK", null,"Connect YouTube","Connect YouTube", "Connection YouTube"),
new CardAction(ActionTypes.PostBack, "테스트 F연결", null,"Connect QnA-Maker","Connect QnA-Maker", "Connection A")
};
var response = MessageFactory.Attachment(card.ToAttachment());
await turnContext.SendActivityAsync(response, cancellationToken);
}
}
}
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
//QnA Maker연결
var qnaMaker = new QnAMaker(new QnAMakerEndpoint
{
KnowledgeBaseId = "fcd905a3-7269-4ea5-9a58-7b02c888ddb6",
EndpointKey = "7b9a938a-e7ac-46f6-ab69-97d4e2e04f66",
Host = "myfirstqa.azurewebsites.net"
},
null,
new System.Net.Http.HttpClient());
var conversationStateAccessors = _conversationState.CreateProperty<ConversationService>(nameof(ConversationService));
var conversationService = await conversationStateAccessors.GetAsync(turnContext, () => new ConversationService());
var input = turnContext.Activity.Text;
if (String.IsNullOrEmpty(conversationService.currentService) && (input.Equals("Connection QnAMaker")))
{
conversationService.currentService = input;
await turnContext.SendActivityAsync(MessageFactory.Text("선택 : " + input + " service ,\n 입력할 내용 " + input + " question"), cancellationToken);
}
else if (String.IsNullOrEmpty(conversationService.currentService))
{
await turnContext.SendActivityAsync(MessageFactory.Text("select a service from hero card first"), cancellationToken);
}
else if (conversationService.currentService.Equals("Connection QnAMaker"))
{
//call your dy QNA service here
var result = qnaMaker.GetAnswersAsync(turnContext).GetAwaiter().GetResult();
if (result.Length == 0)
{
await turnContext.SendActivityAsync(MessageFactory.Text("Sorry , I can't find any answer for it"), cancellationToken);
}
else
{
await turnContext.SendActivityAsync(MessageFactory.Text(result[0].Answer), cancellationToken);
}
////await turnContext.SendActivityAsync(MessageFactory.Text(result[0].Answer), cancellationToken);
}
else if (conversationService.currentService.Equals("dy365"))
{
//call your dy 365 service here
await turnContext.SendActivityAsync(MessageFactory.Text("dy365 response"), cancellationToken);
}
else
{
await turnContext.SendActivityAsync(MessageFactory.Text("error"), cancellationToken);
};
}
}
public class ConversationService
{
public string currentService { get; set; }
}
}
Dialog/RootDialog.cs
namespace Microsoft.BotBuilderSamples.Dialog
{
public class RootDialog : ComponentDialog
{
private const string InitialDialog = "initial-dialog";
public RootDialog(IBotServices services)
: base("root")
{
AddDialog(new QnAMakerBaseDialog(services));
AddDialog(new WaterfallDialog(InitialDialog)
.AddStep(InitialStepAsync));
// The initial child Dialog to run.
InitialDialogId = InitialDialog;
}
private async Task<DialogTurnResult> InitialStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
// Set values for generate answer options.
var qnamakerOptions = new QnAMakerOptions
{
ScoreThreshold = QnAMakerBaseDialog.DefaultThreshold,
Top = QnAMakerBaseDialog.DefaultTopN,
Context = new QnARequestContext()
};
// Set values for dialog responses.
var qnaDialogResponseOptions = new QnADialogResponseOptions
{
NoAnswer = QnAMakerBaseDialog.DefaultNoAnswer,
ActiveLearningCardTitle = QnAMakerBaseDialog.DefaultCardTitle,
CardNoMatchText = QnAMakerBaseDialog.DefaultCardNoMatchText,
CardNoMatchResponse = QnAMakerBaseDialog.DefaultCardNoMatchResponse
};
var dialogOptions = new Dictionary<string, object>
{
[QnAMakerBaseDialog.QnAOptions] = qnamakerOptions,
[QnAMakerBaseDialog.QnADialogResponseOptions] = qnaDialogResponseOptions
};
return await stepContext.BeginDialogAsync(nameof(QnAMakerBaseDialog), dialogOptions, cancellationToken);
}
}
}
The above is the part that selects the desired function at the same time as the greeting.
Try the code below which based on this official demo .Replace the content of Bots/StateManagementBot.cs with the code below :
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Schema;
using Microsoft.Bot.Builder.AI.QnA;
namespace Microsoft.BotBuilderSamples
{
public class StateManagementBot : ActivityHandler
{
private BotState _conversationState;
private BotState _userState;
private string KBID = "<KBID>";
private string ENDPOINT_KEY = "<KEY>";
private string HOST = "<QnA maker host>";
public StateManagementBot(ConversationState conversationState, UserState userState)
{
_conversationState = conversationState;
_userState = userState;
}
public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
await base.OnTurnAsync(turnContext, cancellationToken);
// Save any state changes that might have occured during the turn.
await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken);
await _userState.SaveChangesAsync(turnContext, false, cancellationToken);
}
protected override async Task OnMembersAddedAsync(IList<ChannelAccount> membersAdded, ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
{
foreach (var member in membersAdded)
{
if (member.Id != turnContext.Activity.Recipient.Id)
{
var card = new HeroCard();
card.Title = "";
card.Text = #"Welcome to Welcome Users bot sample! This Introduction card";
card.Images = new List<CardImage>() { new CardImage("https://www.google.com/url?sa=i&source=images&cd=&ved=2ahUKEwjQjeeS4obmAhUIfnAKHQGgCB0QjRx6BAgBEAQ&url=https%3A%2F%2Fdougame.tistory.com%2F98&psig=AOvVaw11Y-BZJtsxh1pTp0Qxzedb&ust=1574819546545481") };
card.Buttons = new List<CardAction>()
{
//new CardAction(ActionTypes.OpenUrl, "FAQ", null, "Get an overview", "Get an overview", "https://learn.microsoft.com/en-us/azure/bot-service/?view=azure-bot-service-4.0"),
new CardAction(ActionTypes.PostBack, "Info Dynamics365", null, "Ask a question", "Ask a question", "dy365"),
new CardAction(ActionTypes.PostBack, "FAQ",null , "Ask a question", "Ask a question", "FAQ" ),
new CardAction(ActionTypes.OpenUrl, "Connect", null, "Learn how to deploy", "Learn how to deploy", "https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-howto-deploy-azure?view=azure-bot-service-4.0"),
};
var response = MessageFactory.Attachment(card.ToAttachment());
await turnContext.SendActivityAsync(response, cancellationToken);
}
}
}
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
var qnaMaker = new QnAMaker(new QnAMakerEndpoint
{
KnowledgeBaseId = KBID,
EndpointKey = ENDPOINT_KEY,
Host = HOST
},
null,
new System.Net.Http.HttpClient());
var conversationStateAccessors = _conversationState.CreateProperty<ConversationService>(nameof(ConversationService));
var conversationService = await conversationStateAccessors.GetAsync(turnContext, () => new ConversationService());
var input = turnContext.Activity.Text;
if (String.IsNullOrEmpty(conversationService.currentService) && (input.Equals("FAQ") || input.Equals("dy365")))
{
conversationService.currentService = input;
await turnContext.SendActivityAsync(MessageFactory.Text("using "+ input + " service , pls enter your " + input + " question"), cancellationToken);
} else if (String.IsNullOrEmpty(conversationService.currentService)) {
await turnContext.SendActivityAsync(MessageFactory.Text("select a service from hero card first"), cancellationToken);
}
else if (conversationService.currentService.Equals("FAQ"))
{
var result = qnaMaker.GetAnswersAsync(turnContext).GetAwaiter().GetResult();
if (result.Length == 0)
{
await turnContext.SendActivityAsync(MessageFactory.Text("Sorry , I can't find any answer for it"), cancellationToken);
}
else {
await turnContext.SendActivityAsync(MessageFactory.Text(result[0].Answer), cancellationToken);
}
}
else if (conversationService.currentService.Equals("dy365"))
{
//call your dy 365 service here
await turnContext.SendActivityAsync(MessageFactory.Text("dy365 response"), cancellationToken);
}
else {
await turnContext.SendActivityAsync(MessageFactory.Text("error"), cancellationToken);
};
}
}
public class ConversationService{
public string currentService { get; set; }
}
}
You can find all QnA related params on QnA portal after you publish it :
Result :
In brief , let user select a service first and save the service type into conversation state so that users' requests will be redirected to corresponding service .
Hope it helps .

Dialog starts via interruption by Intents and QnaMaker Answer only when there is no active dialog

I don't know if i can explain this well but please bear with me. There are codes and images below.
So I am migrating my codes from the first version of Bot Framework V4 to the latest version.
I am trying to make a base for a bot that can call other dialog and cancel the current dialog anytime. and also answer questions with QnAMaker when there are no active dialogs.
There are no errors but the bot is not behaving as expected.
Expecting: When user first interracts with "Get Started" the main menu will be called because I added "Get started" to the intents of Main menu.
Actual Results: The main menu is being called twice.
Expecting: When i call DialogA from interruption via intent. The dialogA will be called and if there is any active dialog it will be cancelled.
Actual Results: Dialog A is called and current active dialog is ended BUT dialogA also ends abruptly.(Even when you did not answer its choice prompt yet).
Note: when i call dialogA via choice prompt in the main menu. the dialog A is starting normally and not ending.
Expecting: when there are no active dialog(Example if you cancel dialogs) user can ask a question and the bot check for answers in the QnaMaker. Actual results: the bot answer the question then start main menu. and even when there are active dialog the bot still answer the questions.
Here are the codes:
DialogBot:
namespace SabikoBotV2.Bots
{
public class DialogBot<T> : ActivityHandler
where T : Dialog
{
public readonly IStatePropertyAccessor<DialogState> _dialogAccessor;
protected readonly Dialog Dialog;
protected readonly BotState ConversationState;
protected readonly BotState UserState;
protected readonly ILogger Logger;
private readonly IBotServices BotServices;
private DialogSet Dialogs { get; set; }
public DialogBot(IBotServices botServices, ConversationState conversationState, UserState userState, T dialog, ILogger<DialogBot<T>> logger)
{
ConversationState = conversationState;
UserState = userState;
Dialog = dialog;
Logger = logger;
BotServices = botServices;
Dialogs = new DialogSet(conversationState.CreateProperty<DialogState>(nameof(DialogBot<T>)));
RegisterDialogs(Dialogs);
}
public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
await base.OnTurnAsync(turnContext, cancellationToken);
await ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
await UserState.SaveChangesAsync(turnContext, false, cancellationToken);
}
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
Logger.LogInformation("Running dialog with Message Activity.");
string text = string.IsNullOrEmpty(turnContext.Activity.Text) ? string.Empty : turnContext.Activity.Text.ToLower();
string topIntent = string.Empty;
RecognizerResult luisRecognizerResult = null;
string topDispatch = string.Empty;
RecognizerResult dispatchRecognizerResult = null;
if (!string.IsNullOrEmpty(text))
{
dispatchRecognizerResult = await BotServices.DispatchService.RecognizeAsync(turnContext, cancellationToken);
var topScoringDispatch = dispatchRecognizerResult?.GetTopScoringIntent();
topDispatch = topScoringDispatch.Value.intent;
luisRecognizerResult = await BotServices.LuisService.RecognizeAsync(turnContext, cancellationToken);
var topScoringIntent = luisRecognizerResult?.GetTopScoringIntent();
topIntent = topScoringIntent.Value.intent;
turnContext.TurnState.Add("topDispatch", topDispatch);
turnContext.TurnState.Add("dispatchRecognizerResult", dispatchRecognizerResult);
turnContext.TurnState.Add("botServices", BotServices);
turnContext.TurnState.Add("topIntent", topIntent);
}
var dc = await Dialogs.CreateContextAsync(turnContext, cancellationToken);
var dialogResult = await dc.ContinueDialogAsync();
if (!dc.Context.Responded)
{
switch (dialogResult.Status)
{
case DialogTurnStatus.Empty:
await DispatchToTopIntentAsync(turnContext, topDispatch, dispatchRecognizerResult, cancellationToken);
break;
case DialogTurnStatus.Waiting:
break;
case DialogTurnStatus.Complete:
await dc.EndDialogAsync();
break;
default:
await dc.CancelAllDialogsAsync();
break;
}
}
await Dialog.RunAsync(turnContext, ConversationState.CreateProperty<DialogState>("DialogState"), cancellationToken);
}
private void RegisterDialogs(DialogSet dialogs)
{
dialogs.Add(new MainDialog());
dialogs.Add(new DialogA());
dialogs.Add(new DialogB());
}
private async Task DispatchToTopIntentAsync(ITurnContext turnContext, string intent, RecognizerResult recognizerResult, CancellationToken cancellationToken)
{
switch (intent)
{
case QnAModel:
await DispatchToQnAMakerAsync(turnContext, cancellationToken);
break;
}
}
private async Task DispatchToQnAMakerAsync(ITurnContext turnContext, CancellationToken cancellationToken)
{
if (!string.IsNullOrEmpty(turnContext.Activity.Text))
{
var results = await BotServices.QnaService.GetAnswersAsync(turnContext);
if (results.Any())
{
await turnContext.SendActivityAsync(MessageFactory.Text(results.First().Answer), cancellationToken);
}
else
{
await turnContext.SendActivityAsync(MessageFactory.Text("Sorry, could not find an answer in the Q and A system."), cancellationToken);
}
}
}
}
}
startup
namespace SabikoBotV2
{
public class Startup
{
public Startup()
{
}
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<ICredentialProvider, ConfigurationCredentialProvider>();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddSingleton<IBotFrameworkHttpAdapter, AdapterWithErrorHandler>();
services.AddSingleton<IStorage, MemoryStorage>();
services.AddSingleton<UserState>();
services.AddSingleton<ConversationState>();
services.AddSingleton<IBotServices, BotServices>();
services.AddTransient<MainDialog>();
services.AddTransient<DialogA>();
services.AddTransient<DialogB>();
services.AddTransient<IBot, DialogBot<MainDialog>>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseMvc();
}
}
}
CancelAndHelpDialog
namespace SabikoBotV2.DialogsV2
{
public class CancelAndHelpDialog : ComponentDialog
{
public CancelAndHelpDialog(string id)
: base(id)
{
}
protected override async Task<DialogTurnResult> OnBeginDialogAsync(DialogContext innerDc, object options, CancellationToken cancellationToken = default(CancellationToken))
{
var result = await IsTurnInterruptedAsyncHelpAndCancel(innerDc, cancellationToken);
if (result != null)
{
return result;
}
return await base.OnBeginDialogAsync(innerDc, options, cancellationToken);
}
protected override async Task<DialogTurnResult> OnContinueDialogAsync(DialogContext innerDc, CancellationToken cancellationToken = default)
{
var result = await IsTurnInterruptedAsyncHelpAndCancel(innerDc, cancellationToken);
if (result != null)
{
return result;
}
return await base.OnContinueDialogAsync(innerDc, cancellationToken);
}
protected virtual async Task<DialogTurnResult> IsTurnInterruptedAsyncHelpAndCancel(DialogContext innerDc, CancellationToken cancellationToken)
{
var topIntent = innerDc.Context.TurnState.Get<string>("topIntent");
var text = innerDc.Context.TurnState.Get<string>("text");
if (topIntent.Equals("Cancel"))
{
if (innerDc.ActiveDialog != null)
{
await innerDc.CancelAllDialogsAsync();
await innerDc.Context.SendActivityAsync("👍 Ok. I've cancelled our last activity.");
}
else
{
await innerDc.Context.SendActivityAsync("I don't have anything to cancel.");
}
}
if (topIntent.Equals("Help"))
{
await innerDc.Context.SendActivityAsync("Let me help you");
if (innerDc.ActiveDialog != null)
{
await innerDc.RepromptDialogAsync();
}
}
if (topIntent.Equals("MainDialog"))
{
if (innerDc.ActiveDialog != null)
{
await innerDc.CancelAllDialogsAsync();
await innerDc.BeginDialogAsync(nameof(MainDialog));
}
else
{
await innerDc.BeginDialogAsync(nameof(MainDialog));
}
}
if (topIntent.Equals("DialogA"))
{
if (innerDc.ActiveDialog != null)
{
await innerDc.CancelAllDialogsAsync();
await innerDc.BeginDialogAsync(nameof(DialogA));
}
else
{
await innerDc.BeginDialogAsync(nameof(DialogA));
}
}
if (topIntent.Equals("DialogB"))
{
if (innerDc.ActiveDialog != null)
{
await innerDc.CancelAllDialogsAsync();
await innerDc.BeginDialogAsync(nameof(DialogB));
}
else
{
await innerDc.BeginDialogAsync(nameof(DialogB));
}
}
return null;
}
}
}
MainDialog
namespace SabikoBotV2.Dialogs
{
public class MainDialog : CancelAndHelpDialog
{
private const string InitialId = nameof(MainDialog);
public MainDialog()
: base(nameof(MainDialog))
{
InitialDialogId = InitialId;
WaterfallStep[] waterfallSteps = new WaterfallStep[]
{
FirstStepAsync,
SecondStepAsync,
ThirdStepAsync,
};
AddDialog(new WaterfallDialog(InitialId, waterfallSteps));
AddDialog(new ChoicePrompt(nameof(ChoicePrompt)));
AddDialog(new TextPrompt(nameof(TextPrompt)));
AddDialog(new DialogA());
AddDialog(new DialogB());
}
private static async Task<DialogTurnResult> FirstStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
await stepContext.Context.SendActivityAsync("Start of Main");
return await stepContext.PromptAsync(
nameof(ChoicePrompt),
new PromptOptions
{
Prompt = MessageFactory.Text($"What do you want to do next?"),
Choices = new List<Choice>
{
new Choice
{
Value = "choice1",
},
new Choice
{
Value = "choice2",
},
},
RetryPrompt = MessageFactory.Text($"Please choose one of the options."),
});
}
private static async Task<DialogTurnResult> SecondStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken = default(CancellationToken))
{
switch ((stepContext.Result as FoundChoice).Value.ToString().ToLower())
{
case "choice1":
return await stepContext.BeginDialogAsync(nameof(DialogA));
case "choice2":
return await stepContext.BeginDialogAsync(nameof(DialogB));
default:
return await stepContext.ReplaceDialogAsync(nameof(MainDialog));
}
}
private static async Task<DialogTurnResult> ThirdStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken = default(CancellationToken))
{
await stepContext.Context.SendActivityAsync("End of Main");
return await stepContext.EndDialogAsync();
}
}
}
DialogA and DialogB are same with maindialog but they inherit ComponentDialog instead of CancelAndHelpDialog.
some screenshot for reference:
when dialogA is called via choice of main menu it starts normally
dialogA is called via intent interruption on it end abruptly.
qna maker anwering questions even if there is an active dialog
Hello I managed to fix my problem. For future reference of others here is how i fixed it.
I fixed the running of dialog via intent interruption by adding this line in the cancelandhelpdialog
return new DialogTurnResult(DialogTurnStatus.Waiting);
if (topIntent.Equals("MainDialog"))
{
if (innerDc.ActiveDialog != null)
{
await innerDc.CancelAllDialogsAsync();
await innerDc.BeginDialogAsync(nameof(MainDialog));
}
else
{
await innerDc.BeginDialogAsync(nameof(MainDialog));
}
return new DialogTurnResult(DialogTurnStatus.Waiting);
}
and the QnaMaker to only answer when there are no active dialog by doing this in the OnTurnAsync:
public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
await base.OnTurnAsync(turnContext, cancellationToken);
var topDispatch = turnContext.TurnState.Get<string>("topDispatch");
var dispatchRecognizerResult = turnContext.TurnState.Get<RecognizerResult>("dispatchRecognizerResult");
var dc = await Dialogs.CreateContextAsync(turnContext, cancellationToken);
var dialogResult = await dc.ContinueDialogAsync();
if (!dc.Context.Responded)
{
switch (dialogResult.Status)
{
case DialogTurnStatus.Empty:
await DispatchToTopIntentAsync(turnContext, topDispatch, dispatchRecognizerResult, cancellationToken);
break;
case DialogTurnStatus.Waiting:
break;
case DialogTurnStatus.Complete:
await dc.EndDialogAsync();
break;
default:
await dc.CancelAllDialogsAsync();
break;
}
}
// Save any state changes that might have occured during the turn.
await ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
await UserState.SaveChangesAsync(turnContext, false, cancellationToken);
}
Hope this will help somebody.

Adaptive Card response from a WaterfallStep Dialog MS Bot framework v4

I am trying to send an adaptive card which has 2 options for user to select.
When user submit the response from adaptive card I am receiving :
Newtonsoft.Json.JsonReaderException: Error reading JArray from JsonReader. Current JsonReader item is not an array: StartObject. Path ‘[‘BotAccessors.DialogState’].DialogStack.$values[0].State.options.Prompt.attachments.$values[0].content.body’.
Full code example Link : Manage a complex conversation flow with dialogs
Modification made in HotelDialogs.cs:-
public static async Task<DialogTurnResult> PresentMenuAsync(
WaterfallStepContext stepContext,
CancellationToken cancellationToken)
{
// Greet the guest and ask them to choose an option.
await stepContext.Context.SendActivityAsync(
"Welcome to Contoso Hotel and Resort.",
cancellationToken: cancellationToken);
//return await stepContext.PromptAsync(
// Inputs.Choice,
// new PromptOptions
// {
// Prompt = MessageFactory.Text("How may we serve you today?"),
// RetryPrompt = Lists.WelcomeReprompt,
// Choices = Lists.WelcomeChoices,
// },
// cancellationToken);
var reply = stepContext.Context.Activity.CreateReply();
reply.Attachments = new List<Attachment>
{
new Attachment
{
Content = GetAnswerWithFeedbackSelectorCard("Choose: "),
ContentType = AdaptiveCard.ContentType,
},
};
return await stepContext.PromptAsync(
"testPrompt",
new PromptOptions
{
Prompt = reply,
RetryPrompt = Lists.WelcomeReprompt,
},
cancellationToken).ConfigureAwait(true);
}
Note: ["testPrompt"] I tried with Text Prompt and slightly customizing the TextPrompt to read Activity Value. If Text prompt is not the appropriate prompt for adaptive card response,please let me know is there any other prompt that can be used or some custom prompt will be helpful for this kind of scenario.
Custom Prompt:-
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Schema;
namespace HotelBot
{
public class CustomPrompt : Prompt<string>
{
public CustomPrompt(string dialogId, PromptValidator<string> validator = null)
: base(dialogId, validator)
{
}
protected async override Task OnPromptAsync(ITurnContext turnContext, IDictionary<string, object> state, PromptOptions options, bool isRetry, CancellationToken cancellationToken = default(CancellationToken))
{
if (turnContext == null)
{
throw new ArgumentNullException(nameof(turnContext));
}
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
if (isRetry && options.RetryPrompt != null)
{
await turnContext.SendActivityAsync(options.RetryPrompt, cancellationToken).ConfigureAwait(false);
}
else if (options.Prompt != null)
{
await turnContext.SendActivityAsync(options.Prompt, cancellationToken).ConfigureAwait(false);
}
}
protected override Task<PromptRecognizerResult<string>> OnRecognizeAsync(ITurnContext turnContext, IDictionary<string, object> state, PromptOptions options, CancellationToken cancellationToken = default(CancellationToken))
{
if (turnContext == null)
{
throw new ArgumentNullException(nameof(turnContext));
}
var result = new PromptRecognizerResult<string>();
if (turnContext.Activity.Type == ActivityTypes.Message)
{
var message = turnContext.Activity.AsMessageActivity();
if (!string.IsNullOrEmpty(message.Text))
{
result.Succeeded = true;
result.Value = message.Text;
}
else if (message.Value != null)
{
result.Succeeded = true;
result.Value = message.Value.ToString();
}
}
return Task.FromResult(result);
}
}
}
Card Creation Method:-
private static AdaptiveCard GetAnswerWithFeedbackSelectorCard(string answer)
{
if (answer == null)
{
return null;
}
AdaptiveCard card = new AdaptiveCard();
card.Body = new List<AdaptiveElement>();
var choices = new List<AdaptiveChoice>()
{
new AdaptiveChoice()
{
Title = "Reserve Table",
Value = "1",
},
new AdaptiveChoice()
{
Title = "Order food",
Value = "0",
},
};
var choiceSet = new AdaptiveChoiceSetInput()
{
IsMultiSelect = false,
Choices = choices,
Style = AdaptiveChoiceInputStyle.Expanded,
Value = "1",
Id = "Feedback",
};
var text = new AdaptiveTextBlock()
{
Text = answer,
Wrap = true,
};
card.Body.Add(text);
card.Body.Add(choiceSet);
card.Actions.Add(new AdaptiveSubmitAction() { Title = "Submit" });
return card;
}
Thanks!
After digging for some way forward I came across:
Issue#614
Thus to make adaptive card response work from Dialog, I made a compatible adaptive card prompt by one modification each in Prompt.cs and TextPrompt.cs from Microsoft bot framework.
Prompt.cs => Prompt2.cs ;
TextPrompt.cs => CustomPrompt.cs
Prompt2.cs :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder.Dialogs.Choices;
using Microsoft.Bot.Schema;
using Newtonsoft.Json;
namespace Microsoft.Bot.Builder.Dialogs
{
//Reference: Prompt.cs
/// <summary>
/// Basic configuration options supported by all prompts.
/// </summary>
/// <typeparam name="T">The type of the <see cref="Prompt{T}"/>.</typeparam>
public abstract class Prompt2<T> : Dialog
{
private const string PersistedOptions = "options";
private const string PersistedState = "state";
private readonly PromptValidator<T> _validator;
public Prompt2(string dialogId, PromptValidator<T> validator = null)
: base(dialogId)
{
_validator = validator;
}
public override async Task<DialogTurnResult> BeginDialogAsync(DialogContext dc, object options, CancellationToken cancellationToken = default(CancellationToken))
{
if (dc == null)
{
throw new ArgumentNullException(nameof(dc));
}
if (!(options is PromptOptions))
{
throw new ArgumentOutOfRangeException(nameof(options), "Prompt options are required for Prompt dialogs");
}
// Ensure prompts have input hint set
var opt = (PromptOptions)options;
if (opt.Prompt != null && string.IsNullOrEmpty(opt.Prompt.InputHint))
{
opt.Prompt.InputHint = InputHints.ExpectingInput;
}
if (opt.RetryPrompt != null && string.IsNullOrEmpty(opt.RetryPrompt.InputHint))
{
opt.RetryPrompt.InputHint = InputHints.ExpectingInput;
}
// Initialize prompt state
var state = dc.ActiveDialog.State;
state[PersistedOptions] = opt;
state[PersistedState] = new Dictionary<string, object>();
// Send initial prompt
await OnPromptAsync(dc.Context, (IDictionary<string, object>)state[PersistedState], (PromptOptions)state[PersistedOptions], false, cancellationToken).ConfigureAwait(false);
// Customization starts here for AdaptiveCard Response:
/* Reason for removing the adaptive card attachments after prompting it to user,
* from the stat as there is no implicit support for adaptive card attachments.
* keeping the attachment will cause an exception : Newtonsoft.Json.JsonReaderException: Error reading JArray from JsonReader. Current JsonReader item is not an array: StartObject. Path ‘[‘BotAccessors.DialogState’].DialogStack.$values[0].State.options.Prompt.attachments.$values[0].content.body’.
*/
var option = state[PersistedOptions] as PromptOptions;
option.Prompt.Attachments = null;
/* Customization ends here */
return Dialog.EndOfTurn;
}
public override async Task<DialogTurnResult> ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken))
{
if (dc == null)
{
throw new ArgumentNullException(nameof(dc));
}
// Don't do anything for non-message activities
if (dc.Context.Activity.Type != ActivityTypes.Message)
{
return Dialog.EndOfTurn;
}
// Perform base recognition
var instance = dc.ActiveDialog;
var state = (IDictionary<string, object>)instance.State[PersistedState];
var options = (PromptOptions)instance.State[PersistedOptions];
var recognized = await OnRecognizeAsync(dc.Context, state, options, cancellationToken).ConfigureAwait(false);
// Validate the return value
var isValid = false;
if (_validator != null)
{
}
else if (recognized.Succeeded)
{
isValid = true;
}
// Return recognized value or re-prompt
if (isValid)
{
return await dc.EndDialogAsync(recognized.Value).ConfigureAwait(false);
}
else
{
if (!dc.Context.Responded)
{
await OnPromptAsync(dc.Context, state, options, true).ConfigureAwait(false);
}
return Dialog.EndOfTurn;
}
}
public override async Task<DialogTurnResult> ResumeDialogAsync(DialogContext dc, DialogReason reason, object result = null, CancellationToken cancellationToken = default(CancellationToken))
{
// Prompts are typically leaf nodes on the stack but the dev is free to push other dialogs
// on top of the stack which will result in the prompt receiving an unexpected call to
// dialogResume() when the pushed on dialog ends.
// To avoid the prompt prematurely ending we need to implement this method and
// simply re-prompt the user.
await RepromptDialogAsync(dc.Context, dc.ActiveDialog).ConfigureAwait(false);
return Dialog.EndOfTurn;
}
public override async Task RepromptDialogAsync(ITurnContext turnContext, DialogInstance instance, CancellationToken cancellationToken = default(CancellationToken))
{
var state = (IDictionary<string, object>)instance.State[PersistedState];
var options = (PromptOptions)instance.State[PersistedOptions];
await OnPromptAsync(turnContext, state, options, false).ConfigureAwait(false);
}
protected abstract Task OnPromptAsync(ITurnContext turnContext, IDictionary<string, object> state, PromptOptions options, bool isRetry, CancellationToken cancellationToken = default(CancellationToken));
protected abstract Task<PromptRecognizerResult<T>> OnRecognizeAsync(ITurnContext turnContext, IDictionary<string, object> state, PromptOptions options, CancellationToken cancellationToken = default(CancellationToken));
protected IMessageActivity AppendChoices(IMessageActivity prompt, string channelId, IList<Choice> choices, ListStyle style, ChoiceFactoryOptions options = null, CancellationToken cancellationToken = default(CancellationToken))
{
// Get base prompt text (if any)
var text = prompt != null && !string.IsNullOrEmpty(prompt.Text) ? prompt.Text : string.Empty;
// Create temporary msg
IMessageActivity msg;
switch (style)
{
case ListStyle.Inline:
msg = ChoiceFactory.Inline(choices, text, null, options);
break;
case ListStyle.List:
msg = ChoiceFactory.List(choices, text, null, options);
break;
case ListStyle.SuggestedAction:
msg = ChoiceFactory.SuggestedAction(choices, text);
break;
case ListStyle.None:
msg = Activity.CreateMessageActivity();
msg.Text = text;
break;
default:
msg = ChoiceFactory.ForChannel(channelId, choices, text, null, options);
break;
}
// Update prompt with text and actions
if (prompt != null)
{
// clone the prompt the set in the options (note ActivityEx has Properties so this is the safest mechanism)
prompt = JsonConvert.DeserializeObject<Activity>(JsonConvert.SerializeObject(prompt));
prompt.Text = msg.Text;
if (msg.SuggestedActions != null && msg.SuggestedActions.Actions != null && msg.SuggestedActions.Actions.Count > 0)
{
prompt.SuggestedActions = msg.SuggestedActions;
}
return prompt;
}
else
{
msg.InputHint = InputHints.ExpectingInput;
return msg;
}
}
}
}
CustomPrompt.cs :
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Schema;
namespace HotelBot
{
//Reference: TextPrompt.cs
public class CustomPrompt : Prompt2<string>
{
public CustomPrompt(string dialogId, PromptValidator<string> validator = null)
: base(dialogId, validator)
{
}
protected async override Task OnPromptAsync(ITurnContext turnContext, IDictionary<string, object> state, PromptOptions options, bool isRetry, CancellationToken cancellationToken = default(CancellationToken))
{
if (turnContext == null)
{
throw new ArgumentNullException(nameof(turnContext));
}
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
if (isRetry && options.RetryPrompt != null)
{
await turnContext.SendActivityAsync(options.RetryPrompt, cancellationToken).ConfigureAwait(false);
}
else if (options.Prompt != null)
{
await turnContext.SendActivityAsync(options.Prompt, cancellationToken).ConfigureAwait(false);
}
}
protected override Task<PromptRecognizerResult<string>> OnRecognizeAsync(ITurnContext turnContext, IDictionary<string, object> state, PromptOptions options, CancellationToken cancellationToken = default(CancellationToken))
{
if (turnContext == null)
{
throw new ArgumentNullException(nameof(turnContext));
}
var result = new PromptRecognizerResult<string>();
if (turnContext.Activity.Type == ActivityTypes.Message)
{
var message = turnContext.Activity.AsMessageActivity();
if (!string.IsNullOrEmpty(message.Text))
{
result.Succeeded = true;
result.Value = message.Text;
}
/*Add handling for Value from adaptive card*/
else if (message.Value != null)
{
result.Succeeded = true;
result.Value = message.Value.ToString();
}
}
return Task.FromResult(result);
}
}
}
Thus workaround until official release of Adaptive Card Prompt for dialog in V4 botframework, is to use this custom prompt.
Usage: (Only for sending adaptive cards which have submit actions)
Referring to the example in the question section:
Add(new CustomPrompt("testPrompt"));
The response for the adaptive card submit action will be received in the next waterfall step : ProcessInputAsync()
var choice = (string)stepContext.Result;
choice will be JSON string of the body posted by the adaptive card.
this is current problem, do we know when we will be able create multi-turn conversation flow using adaptive card in V4 bot framework and wait for Adaptive card's response in stepcontext.result variable instead always sending user to original OnTurn method.
Hit this issue today. This looks like a known issue and there is a workaround available on GitHub
Attachment attachment = new Attachment()
{
ContentType = AdaptiveCard.ContentType,
Content = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(AdpCard)),
};
https://github.com/Microsoft/AdaptiveCards/issues/2148#issuecomment-462708622

Categories