Adaptive Card response from a WaterfallStep Dialog MS Bot framework v4 - c#

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

Related

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 .

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

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),
};
}
}
}

Bot framework DialogContext.Continue does not work

I have implement a bot using Microsoft Bot builder SDK v-4 (pre-release). To manage the conversation flow I have used two simple dialogs-
GreetingDialog - DialogBegin: To greet the user first time
public Task DialogBegin(DialogContext dc, IDictionary<string, object> dialogArgs = null)
{
var state = dc.Context.GetConversationState<EchoState>();
string greetMessage = string.Format("Hi, I am {0}.", _botName);
dc.Context.SendActivity(greetMessage);
IList<CardAction> suggestedActions = new List<CardAction>
{
//some card action suggestions
};
var activity = MessageFactory.SuggestedActions(suggestedActions, text: "Please select the area of conversation.");
dc.Context.SendActivity(activity);
dc.End();
return Task.CompletedTask;
}
ConversationDialog - DialogBegin: To continue the subsequent conversation after the user has been greeted
public Task DialogBegin(DialogContext dc, IDictionary<string, object> dialogArgs = null)
{
string activity = "test";
dc.Context.SendActivity(activity);
dc.Continue();
return Task.CompletedTask;
}
I am calling the GreetingDialog in the ConversationUpdate event and the ConversationDialog in the subsequent message received event, within the OnTurn method in my Bot class.
OnTurn event in my Bot class:
public async Task OnTurn(ITurnContext context)
{
var state = context.GetConversationState<EchoState>();
var dialogCtx = _dialogs.CreateContext(context, state);
if (context.Activity.Type == ActivityTypes.ConversationUpdate)
{
//Greet user first time
if (context.Activity.MembersAdded[0].Id == "default-user")
{
return;
}
if (!context.Responded)
{
var args = new Dictionary<string, object>
{
["greetingArgs"] = context.Activity.Text
};
await dialogCtx.Begin("greetingDialog", args);
}
}
else if (context.Activity.Type == ActivityTypes.Message)
{
await dialogCtx.Continue(); //this line is supposed to execute Begin the active dialog again??
//if (!context.Responded)
if(dialogCtx.ActiveDialog == null || !dialogCtx.Context.Responded)
{
var args = new Dictionary<string, object>
{
["conversationArgs"] = context.Activity.Text
};
await dialogCtx.Begin("conversationDialog", args);
}
}
}
Using the above code, I get redirected to ConversationDialog but it only happens through await dialogCtx.Begin("conversationDialog", args);. Isn't it supposed to redirect to DialogBegin of the Active dialog when I do await dialogCtx.Continue();? I can see the Active dialog is 'conversationDialog' and the debugger steps over through await dialogCtx.Continue();. Any help with this please?
I think I figured it out. We can implement the IDialogContinue interface for our Dialog class like this-
public class QnADialog : IDialog, IDialogContinue
{
public Task DialogBegin(DialogContext dc, IDictionary<string, object> dialogArgs = null)
{
string activity = "test";
dc.Context.SendActivity(activity);
//dc.Continue();
return Task.CompletedTask;
}
public Task DialogContinue(DialogContext dc)
{
dc.Context.SendActivity("dc continue");
dc.Context.Responded = true;
return Task.CompletedTask;
}
}
Then we can use the DialogContinue method to handle the DialogContext.Continue() from the calling code.

Bot Framework messes up dialog state

I'm currently making a chatbot with Microsoft's Bot Framework. In my flow I have a final dialog that lets the user know, that they are participating in the competition. There is also an error-handling method for unknown input. The two methods are seen here:
[Serializable]
public class ConcertCityDialog : AbstractBasicDialog<DialogResult>
{
private static FacebookService FacebookService => new FacebookService(new FacebookClient());
[LuisIntent("ConcertCity")]
public async Task ConcertCityIntent(IDialogContext context, LuisResult result)
{
var fbAccount = await FacebookService.GetAccountAsync(context.Activity.From.Id);
var selectedCityName = result.Entities.FirstOrDefault()?.Entity;
concert_city selectedCity;
using (var concertCityService = new ConcertCityService())
{
selectedCity = concertCityService.FindConcertCity(selectedCityName);
}
if (selectedCity == null)
{
await NoneIntent(context, result);
return;
}
user_interaction latestInteraction;
using (var userService = new MessengerUserService())
{
var user = userService.FindByFacebookIdIncludeInteractions(context.Activity.From.Id);
latestInteraction = user.user_interaction.MaxBy(e => e.created_at);
}
latestInteraction.preferred_city_id = selectedCity.id;
latestInteraction.gif_created = true;
using (var userInteractionService = new UserInteractionService())
{
userInteractionService.UpdateUserInteraction(latestInteraction);
}
var shareIntroReply = context.MakeMessage();
shareIntroReply.Text = "Great choice! You are now participating in the competition. If you dare then pass your message \uD83D\uDE0E";
await context.PostAsync(shareIntroReply);
var reply = await MessageUtility.MakeShareMessageCard(context, fbAccount, latestInteraction, false);
await context.PostAsync(reply);
context.Done(DialogResult.Done);
}
[LuisIntent("")]
[LuisIntent("None")]
public async Task NoneIntent(IDialogContext context, LuisResult result)
{
messenger_user user;
using (var userService = new MessengerUserService())
{
user = userService.FindByFacebookId(context.Activity.From.Id);
}
var phrase = CreateMisunderstoodPhrase(user, result.Query);
using (var misunderstoodPhraseService = new MisunderstoodPhraseService())
{
misunderstoodPhraseService.CreatePhrase(phrase);
}
List<concert_city> concertCities;
using (var concertCityService = new ConcertCityService())
{
concertCities = concertCityService.GetUpcomingConcertCities().ToList();
}
// Prompt city
var reply = context.MakeMessage();
reply.Text = "I'm not sure what you mean \uD83E\uDD14<br/>Which Grøn Koncert would you like to attend?";
reply.SuggestedActions = new SuggestedActions
{
Actions = concertCities.Select(e => MessageUtility.MakeQuickAnswer(e.name)).ToList()
};
await context.PostAsync(reply);
context.Wait(MessageReceived);
}
protected override void OnDeserializedCustom(StreamingContext context)
{
}
}
And here is the AbstractBasicDialog implementation:
[Serializable]
public abstract class AbstractBasicDialog<T> : LuisDialog<T>
{
protected AbstractBasicDialog() : base(new LuisService(new LuisModelAttribute(
ConfigurationManager.AppSettings["LuisAppId"],
ConfigurationManager.AppSettings["LuisAPIKey"],
domain: ConfigurationManager.AppSettings["LuisAPIHostName"])))
{
}
[LuisIntent("Cancel")]
public virtual async Task CancelIntent(IDialogContext context, LuisResult result)
{
var randomQuotes = new List<string>
{
"If you say so, I'll leave you alone for now",
"alright then, I'll leave you alone",
"Okay then, I won't bother you anymore"
};
await context.PostAsync(MessageUtility.RandAnswer(randomQuotes));
context.Done(DialogResult.Cancel);
}
[LuisIntent("Start")]
public virtual async Task StartIntent(IDialogContext context, LuisResult result)
{
context.Done(DialogResult.Restart);
}
[LuisIntent("CustomerSupport")]
public async Task CustomerSupportIntent(IDialogContext context, LuisResult result)
{
using (var userService = new MessengerUserService())
{
var user = userService.FindByFacebookId(context.Activity.From.Id);
if (user != null)
{
user.receiving_support = true;
userService.UpdateUser(user);
}
}
await context.PostAsync("I'll let customer service know, that you want to talk to them. They will get back to you within 24 hours.<br/>If at any time you want to return to me, and start passing a message, just type \"Stop customer support\".");
context.Call(new CustomerSupportDialog(), ResumeAfterCustomerSupport);
}
private async Task ResumeAfterCustomerSupport(IDialogContext context, IAwaitable<DialogResult> result)
{
context.Done(await result);
}
protected misunderstood_phrase CreateMisunderstoodPhrase(messenger_user user, string phrase)
{
return new misunderstood_phrase
{
phrase = phrase,
dialog = GetType().Name,
messenger_user_id = user.id
};
}
[OnDeserialized]
private void OnDeserialized(StreamingContext context)
{
OnDeserializedCustom(context);
}
protected abstract void OnDeserializedCustom(StreamingContext context);
}
The call chain starts at this dialog:
[Serializable]
public class BasicLuisDialog : LuisDialog<DialogResult>
{
private static FacebookService FacebookService => new FacebookService(new FacebookClient());
public BasicLuisDialog() : base(new LuisService(new LuisModelAttribute(
ConfigurationManager.AppSettings["LuisAppId"],
ConfigurationManager.AppSettings["LuisAPIKey"],
domain: ConfigurationManager.AppSettings["LuisAPIHostName"])))
{
}
[LuisIntent("")]
[LuisIntent("None")]
public async Task NoneIntent(IDialogContext context, LuisResult result)
{
var facebookAccount = await FacebookService.GetAccountAsync(context.Activity.From.Id);
RegisterUser(facebookAccount, null, out var user);
var phrase = CreateMisunderstoodPhrase(user, result.Query);
using (var misunderstoodPhraseService = new MisunderstoodPhraseService())
{
misunderstoodPhraseService.CreatePhrase(phrase);
}
var reply = context.MakeMessage();
reply.SuggestedActions = new SuggestedActions
{
Actions = new List<CardAction>
{
new CardAction { Title = "Get started", Type = ActionTypes.ImBack, Value = "Get started" },
new CardAction { Title = "Customer support", Type = ActionTypes.ImBack, Value = "Customer support" }
}
};
var name = string.IsNullOrEmpty(facebookAccount.FirstName) ? "" : $"{facebookAccount.FirstName} ";
reply.Text = $"Hm, I'm not sure what you mean {name} \uD83E\uDD14 Here are some ways you can interact with me:";
await context.PostAsync(reply);
context.Wait(MessageReceived);
}
[LuisIntent("Greeting")]
[LuisIntent("Positive")]
[LuisIntent("Start")]
public async Task GreetingIntent(IDialogContext context, LuisResult result)
{
var rnd = new Random();
var facebookAccount = await FacebookService.GetAccountAsync(context.Activity.From.Id);
// Initial Greeting
var greetings = new List<string>
{
"Well hello there",
"Hi there"
};
if (!string.IsNullOrEmpty(facebookAccount.FirstName))
{
greetings.Add("Hi {0}");
greetings.Add("Hello {0}");
greetings.Add("Welcome {0}");
}
if (facebookAccount.Gender == "male")
greetings.Add("Hey handsome");
else if (facebookAccount.Gender == "female")
greetings.Add("Hi gorgeous");
var randIndex = rnd.Next(greetings.Count);
var greeting = string.Format(greetings[randIndex], facebookAccount.FirstName);
await context.PostAsync(greeting);
await MessageUtility.StartTyping(context, 300);
country country;
using (var countryService = new CountryService())
{
country = countryService.FindCountry(facebookAccount.Locale);
}
var userHasCountry = RegisterUser(facebookAccount, country, out var user);
// If user contry not found prompt for answer
if (!userHasCountry)
{
var countryReply = context.MakeMessage();
countryReply.Text = "You are hard to keep track of - where are you from?";
countryReply.SuggestedActions = new SuggestedActions
{
Actions = new List<CardAction>
{
MessageUtility.MakeQuickAnswer("Denmark"),
MessageUtility.MakeQuickAnswer("Norway"),
MessageUtility.MakeQuickAnswer("Sweden"),
MessageUtility.MakeQuickAnswer("Other")
}
};
await context.PostAsync(countryReply);
context.Call(new CountryDialog(), AfterCountryDialog);
}
else
{
await FunPrompt(context, country);
}
}
private async Task AfterCountryDialog(IDialogContext countryContext, IAwaitable<country> countryAwaitable)
{
var country = await countryAwaitable;
var facebookAccount = await FacebookService.GetAccountAsync(countryContext.Activity.From.Id);
using (var userService = new MessengerUserService())
{
var user = userService.FindByFacebookId(facebookAccount.Id);
user.country = country;
userService.UpdateUser(user);
}
var reply = countryContext.MakeMessage();
reply.Text = "That's cool \uD83D\uDE0E";
await countryContext.PostAsync(reply);
await MessageUtility.StartTyping(countryContext, 350);
await FunPrompt(countryContext, country);
}
private async Task FunPrompt(IDialogContext context, country country)
{
if (country?.name == "norway" && DateTime.Now < new DateTime(2018, 8, 13))
{
var reply = context.MakeMessage();
reply.Text = "Unfortunately the competition isn't open in Norway yet. You can still talk to customer support if you want to";
reply.SuggestedActions = new SuggestedActions
{
Actions = new List<CardAction>
{
MessageUtility.MakeQuickAnswer("Customer support")
}
};
await context.PostAsync(reply);
context.Wait(MessageReceived);
}
else if ((country?.name == "denmark" && DateTime.Now >= new DateTime(2018, 7, 29)) ||
(country?.name == "norway" && DateTime.Now >= new DateTime(2018, 10, 21)))
{
var reply = context.MakeMessage();
reply.Text = "The competition has ended. You can still talk to customer support if you want to";
reply.SuggestedActions = new SuggestedActions
{
Actions = new List<CardAction>
{
MessageUtility.MakeQuickAnswer("Customer support")
}
};
await context.PostAsync(reply);
context.Wait(MessageReceived);
}
else
{
await context.PostAsync("Are you up for some fun?");
context.Call(new IntroductionDialog(), ResumeAfterDialog);
}
}
[LuisIntent("CustomerSupport")]
public async Task CustomerSupportIntent(IDialogContext context, LuisResult result)
{
using (var userService = new MessengerUserService())
{
var user = userService.FindByFacebookId(context.Activity.From.Id);
if (user != null)
{
user.receiving_support = true;
userService.UpdateUser(user);
}
}
await context.PostAsync("I'll let customer support know, that you want to talk to them. They should be messaging you shortly.<br/>You can end your conversation with customer support at any time by typing \"Stop customer support\".");
context.Call(new CustomerSupportDialog(), ResumeAfterDialog);
}
private async Task ResumeAfterDialog(IDialogContext context, IAwaitable<DialogResult> result)
{
var resultState = await result;
if (resultState == DialogResult.Restart)
await GreetingIntent(context, null);
else if (resultState == DialogResult.CustomerSupport)
await ResumeAfterCustomerSupport(context);
else if (resultState == DialogResult.Done || resultState == DialogResult.Cancel)
context.Done(resultState);
else
context.Wait(MessageReceived);
}
private async Task ResumeAfterCustomerSupport(IDialogContext context)
{
using (var userService = new MessengerUserService())
{
var user = userService.FindByFacebookId(context.Activity.From.Id);
if (user != null)
{
user.receiving_support = false;
userService.UpdateUser(user);
}
}
await context.PostAsync("I hope you got the help you needed. Would you like to pass a message to a friend?");
context.Call(new IntroductionDialog(), ResumeAfterDialog);
}
private bool RegisterUser(FacebookAccount fbAccount, country country, out messenger_user user)
{
if (string.IsNullOrEmpty(fbAccount?.Id))
{
user = null;
return false;
}
using (var userService = new MessengerUserService())
{
user = userService.FindByFacebookId(fbAccount.Id);
if (user != null)
return user.country != null;
user = new messenger_user
{
id = fbAccount.Id,
country = country
};
userService.CreateUser(user);
return user.country != null;
}
}
protected misunderstood_phrase CreateMisunderstoodPhrase(messenger_user user, string phrase)
{
return new misunderstood_phrase
{
phrase = phrase,
dialog = GetType().Name,
messenger_user_id = user.id
};
}
}
This works most of the time. The user is told that their registration was a success and the flow exits with the context.Done() call. Sometimes however the chatbot doesn't register the dialog as being exited, as seen here:
As you can see the chatbot is still in the same Dialog even though I have called the Done() method. This is a general problem in my chatbot, as it happens sometimes in all my dialogs.
Do you have any input as to what could be wrong?
EDIT:
When debugging this I've added breakpoints every time it calls context.Call. When my issue arises it stops hitting these breakpoints afterwards. Could this be a side-effect of some DI or something? This is my DI code:
Conversation.UpdateContainer(builder =>
{
builder.RegisterModule(new DialogModule());
builder.RegisterModule(new ReflectionSurrogateModule());
builder.RegisterModule(new DialogModule_MakeRoot());
builder.RegisterModule(new AzureModule(Assembly.GetExecutingAssembly()));
var store = new TableBotDataStore(ConfigurationManager.ConnectionStrings["StorageConnectionString"].ConnectionString);
builder.Register(c => store)
.Keyed<IBotDataStore<BotData>>(AzureModule.Key_DataStore)
.AsSelf()
.SingleInstance();
builder.Register(c => new CachingBotDataStore(store,
CachingBotDataStoreConsistencyPolicy
.ETagBasedConsistency))
.As<IBotDataStore<BotData>>()
.AsSelf()
.InstancePerLifetimeScope();
builder.RegisterType<BasicLuisDialog>().As<LuisDialog<DialogResult>>().InstancePerDependency();
});
I think I finally found the problem. In my code I had implemented a helper method in a static class that would send a typing response and wait a certain amount of time. Seeing as the context was passed into this static method it seems that this was causing some issues.
After changing the method to an extension method of the LuisDialog I no longer have this issue.
I would appreciate if anyone can expand on why this might have been a problem.
EDIT: The method in question:
public static async Task StartTyping(IDialogContext context, int sleep)
{
var typingMsg = context.MakeMessage();
typingMsg.Type = ActivityTypes.Typing;
await context.PostAsync(typingMsg);
await Task.Delay(sleep);
}
I faced a very similar issue and while moving the typing sending into a base class from a static helper class as Frederik did help to highly reduce the number of times the problem occured, the final solution was this: https://github.com/Microsoft/BotBuilder/issues/4477
In short, I had to downgrade the bot-related NuGet packages (Microsoft.Bot.Builder, Microsoft.Bot.Builder.History, Microsoft.Bot.Connector) to 3.13.1 and the issue disappeared.
since in [LuisIntent("ConcertCity")] you are using context.Done() so the current dialog gets exit from the stack. This is why the next message is being handled by the previous dialog or the message controller where the 'None' intent is being called and you are getting this response
reply.Text = "I'm not sure what you mean \uD83E\uDD14<br/>Which Grøn Koncert would you like to attend?";
You should not do context.Done() every places, this should only be called when you have to go to the previous dialog on the stack.

Error using QnAMaker sample with feedback

I have been trying to use Microsoft Cognitive and AI toolkit with QnAMaker API, in order to create a simplistic chat bot.
While my normal qnaMakerAi chat bot works fine, there is an issue while I was trying to enhance it's feature and include the bot feedback within the response.
I have been following the exact code sample as is referred here.
The issue I'm having is:
Exception: Object reference not set to an instance of an object.
[File of type 'text/plain'].
The debugger is giving error in the code section - (in the file WebApiConfig.cs)
JsonConvert.DefaultSettings = () => new JsonSerializerSettings()
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
Formatting = Newtonsoft.Json.Formatting.Indented,
NullValueHandling = NullValueHandling.Ignore,
};
I have also raised a detailed description of the issue in - https://github.com/Microsoft/BotBuilder/issues/4267.
Please check and suggest.
Based on the user comments, here is the code for MessagesController -
using System;
using System.Threading.Tasks;
using System.Web.Http;
using Microsoft.Bot.Connector;
using Microsoft.Bot.Builder.Dialogs;
using System.Web.Http.Description;
using System.Net.Http;
using QnABot.Dialogs;
namespace Microsoft.Bot.Sample.QnABot
{
[BotAuthentication]
public class MessagesController : ApiController
{
/// <summary>
/// POST: api/Messages
/// receive a message from a user and send replies
/// </summary>
/// <param name="activity"></param>
[ResponseType(typeof(void))]
public virtual async Task<HttpResponseMessage> Post([FromBody] Activity activity)
{
// check if activity is of type message
if (activity.GetActivityType() == ActivityTypes.Message)
{
//await Conversation.SendAsync(activity, () => new RootDialog());
await Conversation.SendAsync(activity, () => new QnaDialog());
}
else
{
HandleSystemMessage(activity);
}
return new HttpResponseMessage(System.Net.HttpStatusCode.Accepted);
}
private Activity HandleSystemMessage(Activity message)
{
if (message.Type == ActivityTypes.DeleteUserData)
{
// Implement user deletion here
// If we handle user deletion, return a real message
}
else if (message.Type == ActivityTypes.ConversationUpdate)
{
// Handle conversation state changes, like members being added and removed
// Use Activity.MembersAdded and Activity.MembersRemoved and Activity.Action for info
// Not available in all channels
}
else if (message.Type == ActivityTypes.ContactRelationUpdate)
{
// Handle add/remove from contact lists
// Activity.From + Activity.Action represent what happened
}
else if (message.Type == ActivityTypes.Typing)
{
// Handle knowing tha the user is typing
}
else if (message.Type == ActivityTypes.Ping)
{
}
return null;
}
}
}
For QnADialog -
using Microsoft.Bot.Builder.Azure;
using Microsoft.Bot.Builder.CognitiveServices.QnAMaker;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Connector;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
namespace QnABot.Dialogs
{
[Serializable]
public class QnaDialog : QnAMakerDialog
{
public QnaDialog() : base(new QnAMakerService(new QnAMakerAttribute("b372e477-0a2f-4a5a-88d5-3a664d16a4c3", "4ee02ead3xxxxxx", "Sorry, I couldn't find an answer for that", 0.5)))
{
}
protected override async Task RespondFromQnAMakerResultAsync(IDialogContext context, IMessageActivity message, QnAMakerResults result)
{
// answer is a string
var answer = result.Answers.First().Answer;
Activity reply = ((Activity)context.Activity).CreateReply();
string[] qnaAnswerData = answer.Split(';');
int dataSize = qnaAnswerData.Length;
string title = qnaAnswerData[0];
string description = qnaAnswerData[1];
string url = qnaAnswerData[2];
string imageURL = qnaAnswerData[3];
HeroCard card = new HeroCard
{
Title = title,
Subtitle = description,
};
card.Buttons = new List<CardAction>
{
new CardAction(ActionTypes.OpenUrl, "Learn More", value: url)
};
card.Images = new List<CardImage>
{
new CardImage( url = imageURL)
};
reply.Attachments.Add(card.ToAttachment());
await context.PostAsync(reply);
}
protected override async Task DefaultWaitNextMessageAsync(IDialogContext context, IMessageActivity message, QnAMakerResults result)
{
// get the URL
var answer = result.Answers.First().Answer;
string[] qnaAnswerData = answer.Split(';');
string qnaURL = qnaAnswerData[2];
// pass user's question
var userQuestion = (context.Activity as Activity).Text;
context.Call(new FeedbackDialog(qnaURL, userQuestion), ResumeAfterFeedback);
}
private async Task ResumeAfterFeedback(IDialogContext context, IAwaitable<IMessageActivity> result)
{
if (await result != null)
{
await MessageReceivedAsync(context, result);
}
else
{
context.Done<IMessageActivity>(null);
}
}
}
}
For FeedBackDialog -
using Microsoft.ApplicationInsights;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Connector;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
namespace QnABot.Dialogs
{
[Serializable]
public class FeedbackDialog : IDialog<IMessageActivity>
{
private string qnaURL;
private string userQuestion;
public FeedbackDialog(string url, string question)
{
// keep track of data associated with feedback
qnaURL = url;
userQuestion = question;
}
public async Task StartAsync(IDialogContext context)
{
var feedback = ((Activity)context.Activity).CreateReply("Did you find what you need?");
feedback.SuggestedActions = new SuggestedActions()
{
Actions = new List<CardAction>()
{
new CardAction(){ Title = "👍", Type=ActionTypes.PostBack, Value=$"yes-positive-feedback" },
new CardAction(){ Title = "👎", Type=ActionTypes.PostBack, Value=$"no-negative-feedback" }
}
};
await context.PostAsync(feedback);
context.Wait(this.MessageReceivedAsync);
}
public async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> result)
{
var userFeedback = await result;
if (userFeedback.Text.Contains("yes-positive-feedback") || userFeedback.Text.Contains("no-negative-feedback"))
{
// create telemetry client to post to Application Insights
TelemetryClient telemetry = new TelemetryClient();
if (userFeedback.Text.Contains("yes-positive-feedback"))
{
// post feedback to App Insights
var properties = new Dictionary<string, string>
{
{"Question", userQuestion },
{"URL", qnaURL },
{"Vote", "Yes" }
// add properties relevant to your bot
};
telemetry.TrackEvent("Yes-Vote", properties);
}
else if (userFeedback.Text.Contains("no-negative-feedback"))
{
// post feedback to App Insights
}
await context.PostAsync("Thanks for your feedback!");
context.Done<IMessageActivity>(null);
}
else
{
// no feedback, return to QnA dialog
context.Done<IMessageActivity>(userFeedback);
}
}
}
}
1st, bad config
Ok, the 1st problem is the fact that you inverted 2 parameters in your QnaDialog declaration:
public QnaDialog() : base(new QnAMakerService(new QnAMakerAttribute("b372e477-0a2f-4a5a-88d5-3a664d16a4c3", "4ee02ead3xxxxxx", "Sorry, I couldn't find an answer for that", 0.5)))
The syntax is: Qn
public QnAMakerAttribute(string subscriptionKey, string knowledgebaseId, ...
Here you inverted your Key and your knowledgebaseId. The Guid should be in 2nd position, not 1st.
Note that I modified your subscription key in the question and reply, you should note share them like that.
Code improvement
The sample that you used seems to be not valid:
in the case there is no match
when your answer is not made of a string with ; separator (like when you type "hi", the reply is "hello"
I added some security to avoid errors in those cases:
protected override async Task RespondFromQnAMakerResultAsync(IDialogContext context, IMessageActivity message, QnAMakerResults result)
{
// answer is a string
var answer = result.Answers.First().Answer;
Activity reply = ((Activity)context.Activity).CreateReply();
var qnaAnswerData = answer.Split(';');
var dataSize = qnaAnswerData.Length;
if (dataSize == 3)
{
var title = qnaAnswerData[0];
var description = qnaAnswerData[1];
var url = qnaAnswerData[2];
var imageUrl = qnaAnswerData[3];
var card = new HeroCard
{
Title = title,
Subtitle = description,
Buttons = new List<CardAction>
{
new CardAction(ActionTypes.OpenUrl, "Learn More", value: url)
},
Images = new List<CardImage>
{
new CardImage(url = imageUrl)
},
};
reply.Attachments.Add(card.ToAttachment());
}
else
{
reply.Text = answer;
}
await context.PostAsync(reply);
}
protected override async Task DefaultWaitNextMessageAsync(IDialogContext context, IMessageActivity message, QnAMakerResults result)
{
if (result.Answers.Count > 0)
{
// get the URL
var answer = result.Answers.First().Answer;
var qnaAnswerData = answer.Split(';');
var dataSize = qnaAnswerData.Length;
if (dataSize == 3)
{
var qnaUrl = qnaAnswerData[2];
// pass user's question
var userQuestion = (context.Activity as Activity).Text;
context.Call(new FeedbackDialog(qnaUrl, userQuestion), ResumeAfterFeedback);
}
else
{
await ResumeAfterFeedback(context, new AwaitableFromItem<IMessageActivity>(null));
}
}
else
{
await ResumeAfterFeedback(context, new AwaitableFromItem<IMessageActivity>(null));
}
}
private async Task ResumeAfterFeedback(IDialogContext context, IAwaitable<IMessageActivity> result)
{
if (await result != null)
{
await MessageReceivedAsync(context, result);
}
else
{
context.Done<IMessageActivity>(null);
}
}

Categories