Dialog Continuation Issue on Bot Framework V4 - botframework

I want to start a user dialog right after a welcome message has been displayed in my bot - without any initial user interaction.
Code snippet to accomplish that:
public RootDialogBot(BotServices services, BotAccessors accessors, ILoggerFactory loggerFactory)
{
if (loggerFactory == null)
{
throw new System.ArgumentNullException(nameof(loggerFactory));
}
_logger = loggerFactory.CreateLogger<RootDialogBot>();
_accessors = accessors ?? throw new System.ArgumentNullException(nameof(accessors));
_botServices = services ?? throw new System.ArgumentNullException(nameof(services));
_studentProfileAccessor = _accessors.UserState.CreateProperty<StudentProfile>("studentProfile");
if (!_botServices.QnAServices.ContainsKey("QnAMaker"))
{
throw new System.ArgumentException($"Invalid configuration. Please check your '.bot' file for a QnA service named QnAMaker'.");
}
if (!_botServices.LuisServices.ContainsKey("LUIS"))
{
throw new System.ArgumentException($"Invalid configuration. Please check your '.bot' file for a Luis service named LUIS'.");
}
.Add(new Activity2MainDialog(_accessors.UserState, Activity2MainDialog))
.Add(new Activity2LedFailToWorkDialog(_accessors.UserState, Activity2LedFailToWorkDialog));
}
public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
...
if (turnContext.Activity.Type == ActivityTypes.ConversationUpdate)
{
if (turnContext.Activity.MembersAdded != null)
{
// Save the new turn count into the conversation state.
await _accessors.UserState.SaveChangesAsync(turnContext, false, cancellationToken);
await _accessors.ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
var message = "Welcome!";
await SendWelcomeMessageAsync(turnContext, dc, message,cancellationToken); //Welcome message
}
}
...
}
private static async Task SendWelcomeMessageAsync(ITurnContext turnContext, DialogContext dc,string message, CancellationToken cancellationToken)
{
foreach (var member in turnContext.Activity.MembersAdded)
{
if (member.Id != turnContext.Activity.Recipient.Id)
{
await turnContext.SendActivityAsync(message, cancellationToken: cancellationToken);
await dc.BeginDialogAsync(Activity2MainDialog, "activity2MainDialog", cancellationToken);
}
}
}
The dialog (Activity2MainDialog) works fine until it reaches a return await stepContext.ContinueDialogAsync(cancellationToken); call.
Then it halts.
I believe it has something to do with the conversation state but I still couldn't find a solution for that.
Code snippet of the return await stepContext.ContinueDialogAsync(cancellationToken); call
public class Activity2MainDialog : ComponentDialog
{
private static BellaMain BellaMain = new BellaMain();
private static FTDMain FTDMain = new FTDMain();
private readonly IStatePropertyAccessor<StudentProfile> _studentProfileAccessor;
...
public Activity2MainDialog(UserState userState, string dialogMainId)
: base(dialogMainId)
{
InitialDialogId = Id;
_studentProfileAccessor = userState.CreateProperty<StudentProfile>("studentProfile");
WaterfallStep[] waterfallSteps = new WaterfallStep[]
{
MsgWelcomeStepAsync
...
};
// Add named dialogs to the DialogSet. These names are saved in the dialog state.
AddDialog(new WaterfallDialog(dialogMainId, waterfallSteps));
AddDialog(new TextPrompt(nameof(TextPrompt)));
AddDialog(new ChoicePrompt(nameof(ChoicePrompt)));
AddDialog(new ConfirmPrompt(nameof(ConfirmPrompt)));
}
private async Task<DialogTurnResult> MsgWelcomeStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
await stepContext.Context.SendActivityAsync("**Oi**", "Oi", cancellationToken: cancellationToken);
return await stepContext.ContinueDialogAsync(cancellationToken);
}
private async Task<DialogTurnResult> QuestGoAheadStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
message = "Vamos nessa?";
await stepContext.Context.SendActivityAsync(message , message , cancellationToken: cancellationToken);
retryPromptMessage = message;
return await stepContext.PromptAsync(nameof(ChoicePrompt),
new PromptOptions
{
Prompt = null,
RetryPrompt = MessageFactory.Text(retryPromptMessage, retryPromptSpeakMessage), InputHints.ExpectingInput),
Choices = new[]
{
new Choice {Value = "Sim", Synonyms = new List<string> {"yes","yeah","esta","ta","esta","ok","vamos","curti","curtir","claro","tá","sei","top"}},
new Choice {Value = "Não", Synonyms = new List<string> {"no"}}
}.ToList(),
Style = ListStyle.Auto
});
}
Thoughts on how to fix it? Thx

I'm fairly certain the issue is the ContinueDialog call. You need to end that step with:
return await stepContext.NextAsync(null, cancellationToken);
See CoreBot for more example code.
If this doesn't fix your issue, let me know and I'll adjust my answer.

Related

Log additional information with TranscriptLoggerMiddleware

I'm using TranscriptLoggerMiddleware to log transcript to Azure blobs.
Now, I want to add additional information to the activity, for example, account ID.
Ideally I want the account ID to be the top level folder when creating the blobs so one can easily locate all conversations for a given account.
The logger is only passed the activity without any context. So I'm looking at the Entities activity property which I can potentially use for storing my account ID.
Is this a valid approach?
Any other ideas on how to implement this?
Answering my own question.
This worked for me:
public class SetEntityMiddleware : IMiddleware
{
private readonly BotState _userState;
private readonly IStatePropertyAccessor<UserProfileState> _userProfileState;
public SetEntityMiddleware(UserState userState)
{
_userState = userState;
_userProfileState = userState.CreateProperty<UserProfileState>(nameof(UserProfileState));
}
public async Task OnTurnAsync(ITurnContext turnContext, NextDelegate next, CancellationToken cancellationToken = default)
{
var userProfile = await _userProfileState.GetAsync(turnContext, () => new UserProfileState(), cancellationToken);
this.SetEntity(turnContext.Activity, userProfile);
turnContext.OnSendActivities(async (ctx, activities, nextSend) =>
{
var userProfile = await _userProfileState.GetAsync(ctx, () => new UserProfileState(), cancellationToken);
foreach (var activity in activities)
{
this.SetEntity(activity, userProfile);
}
return await nextSend().ConfigureAwait(false);
});
await next(cancellationToken).ConfigureAwait(false);
}
private void SetEntity(Activity activity, UserProfileState userProfile)
{
if (activity.Type == ActivityTypes.Message &&
!string.IsNullOrEmpty(userProfile.AccountNumber))
{
var entity = new Entity();
entity.Type = "userProfile";
entity.Properties["accountNumber"] = userProfile.AccountNumber;
if (activity.Entities == null)
{
activity.Entities = new List<Entity>();
}
activity.Entities.Add(entity);
}
}
}

How to pass the prompt values from maindialog to dispatcher?

`namespace Microsoft.BotBuilderSamples
{
public class DispatchBot : ActivityHandler
{
private ILogger _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 bot 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 your IT assistant at your service . {WelcomeText}"), cancellationToken);
}
}
}
private async Task DispatchToTopIntentAsync(ITurnContext<IMessageActivity> turnContext, string intent, RecognizerResult recognizerResult, CancellationToken cancellationToken)
{
switch (intent)
{
case "l_us_bot":
await ProcessHomeAutomationAsync(turnContext, recognizerResult.Properties["luisResult"] as LuisResult, cancellationToken);
break;
case "t_abs-bot":
await ProcessSampleQnAAsync(turnContext, cancellationToken);
break;
default:
_logger.LogInformation($"Dispatch unrecognized intent: {intent}.");
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 your IT assistant"), cancellationToken);
}
if (topIntent == "None")
{
await turnContext.SendActivityAsync(MessageFactory.Text("Sorry I didnt get you!"), cancellationToken);
}
if (topIntent == "DateTenure")
{
// Here i want to call my dialog class
}
}
}
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);
}
}
}
`I have a scenario as described:
When user says hi, the bot shows the adaptive cards (with Action.Submit on each card). ----Suppose the cards are as: 1. Book tickets 2. Need Help
3. Cancel
whenever user click any of the option ,i get the same user clicked value. Like
If the user selects "Need help". I get the same value..
then i am getting input from user like any question(query).
Then this query will go to the specific intent from dispatcher but its not going to intent from dispatcher and showing error (object reference not set to an instance of an object)
so what should i do?
for reference i already gone through this link "How to retrieve user entered input in adaptive card to the c# code and how to call next intent on submit button click"
it should take value from the dispatcher bot after user input.

Welcome message with Ibot interface

I am using the Ibot interface, but the welcome message is sent twice.
I use the OnTurnAsync method, and it seems that doesn't do anything when I call the MembersAdded property.
To send the Welcome message i have this piece of code:
if (turnContext.Activity.Type == ActivityTypes.ConversationUpdate && turnContext.Activity.MembersAdded.Count == 1)
{
userProfile.Welcome = true;
var reply = turnContext.Activity.CreateReply();
reply.Attachments = new List<Attachment>();
reply.Attachments.Add(getCard("Welcome"));
await turnContext.SendActivityAsync(reply);
}
There's a few things that could be going on with this.
MembersAdded is called for adding both the bot and the user. I'm guessing this is why it's being sent twice.
If you're using the newer ActivityHandler, you can use:
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)
{
userProfile.Welcome = true;
var reply = turnContext.Activity.CreateReply();
reply.Attachments = new List<Attachment>();
reply.Attachments.Add(getCard("Welcome"));
await turnContext.SendActivityAsync(reply);
}
}
}
Or, if you're using the old activity handler:
public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
if (activity.Type == ActivityTypes.ConversationUpdate)
{
if (activity.MembersAdded != null)
{
foreach (var member in activity.MembersAdded)
{
if (member.Name != "Bot" && member.Name != null)
{
userProfile.Welcome = true;
var reply = turnContext.Activity.CreateReply();
reply.Attachments = new List<Attachment>();
reply.Attachments.Add(getCard("Welcome"));
await turnContext.SendActivityAsync(reply);
}
}
}
}
[...]
WebChat doesn't automatically send a ConversationUpdate like Emulator does. See this WebChat sample for how to send a welcome event.
You could always ensure it doesn't double-send by checking to see if userProfile.Welcome != true. This sample may help.

I get invalid need: expected Wait, have None in my Bot code

I get "invalid need: expected Wait, have None" exception in RootDialog, MessageReceivedAsync method's context.Forward line. Why could this be happening? what correction should I make? Please find my code below.
I use -C#, Visual Studio 2015, Microsoft Bot Framework, Bot emulator. This is for Web Chat.
MessageController
public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
ConnectorClient connector = new ConnectorClient(new Uri(activity.ServiceUrl));
if (activity.Type == ActivityTypes.Message)
{
await Conversation.SendAsync(activity, () => new RootDialog());
}
else
if (activity.Type == ActivityTypes.ConversationUpdate)
{
var greeting = activity.CreateReply("Hi! I'm Cmon. Enter the Id of the Employee you want to know about.");
await connector.Conversations.ReplyToActivityAsync(greeting);
}
else
{
this.HandleSystemMessage(activity);
}
var response = Request.CreateResponse(HttpStatusCode.OK);
return response;
}
RootDialog -
public async Task StartAsync(IDialogContext context)
{
context.Wait(this.MessageReceivedAsync);
}
public virtual async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> result)
{
var message = await result;
try
{
if (message.Text != null)
{
await context.Forward(new DirectoryDialog(), null, message,CancellationToken.None);
}
}
catch (Exception ex)
{
await context.PostAsync($"Oops! Something went wrong :-(");
}
}
DirectoryDialog -
public async Task StartAsync(IDialogContext context)
{
var message = context.Activity as IMessageActivity;
var query = DirectoryQuery.Parse(message.Text);
await context.PostAsync($"Searching for {query.SearchKey}...");
try
{
await SearchEmployee(context, query);
}
catch (FormCanceledException ex)
{
await context.PostAsync($"Oops! Something went wrong :-(");
}
}
private async Task SearchEmployee(IDialogContext context, DirectoryQuery searchQuery)
{
DirectoryDal dal = new DirectoryDal();
DataSet ds = dal.GetEmployeeSearch(searchQuery.SearchKey);
if (ds.Tables.Count > 0 & ds.Tables[0].Rows.Count > 0)
{
StringBuilder employeeSearchMessageBuilder = new StringBuilder(100);
//do something with data table data
var employeeSearchReply = context.MakeMessage();
employeeSearchReply.Type = ActivityTypes.Message;
employeeSearchReply.TextFormat = TextFormatTypes.Markdown;
employeeSearchReply.Text = employeeSearchMessageBuilder.ToString();
await context.PostAsync(employeeSearchReply);
}
}
By passing null here:
await context.Forward(new DirectoryDialog(), null, message,CancellationToken.None);
you are breaking the dialog chain. You have to define your Resume method.
The Troubleshooting section of the documentation has a good section about that, here:
Ensure all dialog methods end with a plan to handle the next message.
All IDialog methods should complete with IDialogStack.Call,
IDialogStack.Wait, or IDialogStack.Done. These IDialogStack methods
are exposed through the IDialogContext that is passed to every IDialog
method. Calling IDialogStack.Forward and using the system prompts
through the PromptDialog static methods will call one of these methods
in their implementation.
I was able to resolve the issue by making the following modifications to my DirectoryDialog.
DirectoryDialog code -
public async Task StartAsync(IDialogContext context)
{
var message = context.Activity as IMessageActivity;
var query = DirectoryQuery.Parse(message.Text);
await context.PostAsync($"Searching for {query.SearchKey}...");
try
{
//await SearchEmployee(context, query);
context.Wait(MessageReceivedAsync);
}
catch (FormCanceledException ex)
{
await context.PostAsync($"Oops! Something went wrong :-(");
}
}
RootDialog code -
public virtual async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> result)
{
var message = await result;
try
{
if (message.Text != null)
{
await context.Forward(new DirectoryDialog(), ResumeAfterOptionDialog, message,CancellationToken.None);
}
}
catch (Exception ex)
{
await context.PostAsync($"Oops! Something went wrong");
}
finally{
context.Done(true);
}
}
private async Task ResumeAfterOptionDialog(IDialogContext context, IAwaitable<object> result)
{
context.Wait(MessageReceivedAsync);
}
Thank you Tanmoy and Nicolas for helping! :)
context.wait(MessageReceivedAsync);
at the end of MessageReceivedAsync(IDialogContext context, IAwaitable result) method of Root Dialog.
and
context.done(true);
at the end of StartAsync method of DirectoryDialog or within both try and catch block

Nested dialogs: message being sent twice

I have a bot where I am doing something like this:
1) A new Activity (message) arrives.
2) I dispatch the message to a RootDialog.
3) Depending on some Logic, RootDialog can:
a) Call a LuisDialog (handling natural language)
b) Call a CustomDialog (handles some business logic).
But when the user state is reset, and the flow leads to an intention inside the LuisDialog, it calls the intention method twice. Just the first time the state is empty, then it works fine.
Let me show you the code:
MessagesController:
public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
if (activity.Type == ActivityTypes.Message)
{
ConnectorClient connector = new ConnectorClient(new Uri(activity.ServiceUrl));
try
{
await Conversation.SendAsync(activity, () => new RootDialog());
}
catch (HttpRequestException e)
{
...
}
}
RootDialog:
public class RootDialog : IDialog<object>
{
public async Task StartAsync(IDialogContext context)
{
await MessageReceivedAsync(context, Awaitable.FromItem(context.Activity.AsMessageActivity()));
}
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> awaitable)
{
bool value = DoSomeCustomLogic();
if (value)
{
string message = DoSomething();
await context.PostAsync(message);
} else {
bool value2 = DoSomeCustomLogic2();
if (value2)
{
var answerValidationDialog = new ValidateAnswerWithUserDialog();
context.Call(answerValidationDialog, ValidateAnswerWithUserDialogCompletedCallback);
} else {
var luisDialog = new LuisDialog();
await context.Forward(luisDialog,LuisDialogCompletedCallback, context.Activity, CancellationToken.None);
}
}
}
Callbacks only do context.Done(true);
And LuisDialog has an Intention which goes like this:
[LuisIntent(LuisUtils.INTENT_MENU_SALUTE)]
public async Task SaluteOrMenu(IDialogContext context, LuisResult result)
{
if (LuisUtils.IntentScoreIsHighEnough(result))
{
string userName = context.Activity.From.Name;
ContextHelper helper = new ContextHelper(MessageReceived);
await helper.AskUserToDoSomeInitialAction(context, saluteWord, userName);
context.Done(true);
}
else
{
await None(context, result);
}
}
And finally class ContextHelper:
public class ContextHelper
{
private Func<IDialogContext, IAwaitable<IMessageActivity>, Task> MessageReceived;
public ContextHelper(Func<IDialogContext, IAwaitable<IMessageActivity>, Task> messageReceived)
{
MessageReceived = messageReceived;
}
public async Task AskUserToDoSomeInitialAction(IDialogContext context, string saluteWord, string userName)
{
var reply = context.MakeMessage();
List<CardAction> buttons = BuildInitialOptionActions();
List<CardImage> images = BuildInitialOptionImages();
string initialText = $"Hi stranger!"
var card = new HeroCard
{
Title = "Hello!"
Text = initialText,
Buttons = buttons,
Images = images
};
reply.Attachments = new List<Attachment> { card.ToAttachment() };
await context.PostAsync(reply);
context.Wait(AfterUserChoseOptionInSalute);
}
private async Task AfterUserChoseOptionInSalute(IDialogContext context, IAwaitable<IMessageActivity> result)
{
await ReDispatchMessageReceivedToDialog(context);
}
private async Task ReDispatchMessageReceivedToDialog(IDialogContext context)
{
await MessageReceived(context, Awaitable.FromItem(context.Activity.AsMessageActivity()));
}
}
The SaluteOrMenu Intention gets called twice (only the first time I interact with the bot or when I delete the state. After Debugging I saw that after doing context.Wait(AfterUserChoseOptionInSalute);, the bot calls that function (instead of waiting for an event to call it)
Any ideas?
Thanks in advance.
I found the line that was wrong. It was on the first dialog (the RootDialog):
public async Task StartAsync(IDialogContext context)
{
await MessageReceivedAsync(context, Awaitable.FromItem(context.Activity.AsMessageActivity()));
}
That is a line that re dispatches a message with the incoming activity. I had it somewhere in some chunk of code and (don't know why), thought it was a good idea to use it on StartAsync. So, because of that, two calls were occurring.
Dumb me.
I just changed it to this and worked:
public async Task StartAsync(IDialogContext context)
{
context.Wait(MessageReceivedAsync);
}

Resources