Nested dialogs: message being sent twice - botframework

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

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

Dialog Continuation Issue on Bot Framework V4

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.

How to call the Publisher Methods in ASPBoilerplate?

I am exploring the ASPBoilerplate framework and what interests me more is its real time notification capability. However, I am having this error.
Here is my Notification publisher class:
public class Publisher : Hub, ITransientDependency
{
private readonly INotificationPublisher _notificationPublisher;
public Publisher(INotificationPublisher notificationPublisher)
{
_notificationPublisher = notificationPublisher;
}
//Send a general notification to all subscribed users in current tenant (tenant in the session)
public async Task Publish_Announcement(string announcementMessage)
{
//Example "LowDiskWarningMessage" content for English -> "Attention! Only {remainingDiskInMb} MBs left on the disk!"
var data = new MessageNotificationData(announcementMessage);
await _notificationPublisher.PublishAsync("abp.notifications.received", data, severity: NotificationSeverity.Info);
}
}
And I am testing if it will notify all online users whenever a new user was created.
public override async Task<UserDto> Create(CreateUserDto input)
{
CheckCreatePermission();
var user = ObjectMapper.Map<User>(input);
user.TenantId = AbpSession.TenantId;
user.IsEmailConfirmed = true;
await _userManager.InitializeOptionsAsync(AbpSession.TenantId);
CheckErrors(await _userManager.CreateAsync(user, input.Password));
if (input.RoleNames != null)
{
CheckErrors(await _userManager.SetRoles(user, input.RoleNames));
}
CurrentUnitOfWork.SaveChanges();
//I cannot call the publisher class since it has dependencies in its contructor.
new Publisher().Publish_Announcement("Hi");
return MapToEntityDto(user);
}
Or am I just doing it wrong?
Please help. Thanks!
Basically, the Notifications is based on Publisher / Subsriber pattern and while the notifications are published through the system, the users who have subscribed to that notification will receive it.
Publish Notifications
public class MyService : ITransientDependency
{
private readonly INotificationPublisher _notificationPublisher;
public MyService(INotificationPublisher notificationPublisher)
{
_notificationPublisher = notificationPublisher;
}
//Send a general notification to a specific user
public async Task Publish_SentFriendshipRequest(string senderUserName, string friendshipMessage, UserIdentifier targetUserId)
{
await _notificationPublisher.PublishAsync("SentFriendshipRequest", new SentFriendshipRequestNotificationData(senderUserName, friendshipMessage), userIds: new[] { targetUserId });
}
//Send an entity notification to a specific user
public async Task Publish_CommentPhoto(string commenterUserName, string comment, Guid photoId, UserIdentifier photoOwnerUserId)
{
await _notificationPublisher.PublishAsync("CommentPhoto", new CommentPhotoNotificationData(commenterUserName, comment), new EntityIdentifier(typeof(Photo), photoId), userIds: new[] { photoOwnerUserId });
}
//Send a general notification to all subscribed users in current tenant (tenant in the session)
public async Task Publish_LowDisk(int remainingDiskInMb)
{
//Example "LowDiskWarningMessage" content for English -> "Attention! Only {remainingDiskInMb} MBs left on the disk!"
var data = new LocalizableMessageNotificationData(new LocalizableString("LowDiskWarningMessage", "MyLocalizationSourceName"));
data["remainingDiskInMb"] = remainingDiskInMb;
await _notificationPublisher.PublishAsync("System.LowDisk", data, severity: NotificationSeverity.Warn);
}
}
Subscribe to Notifications
public class MyService : ITransientDependency
{
private readonly INotificationSubscriptionManager _notificationSubscriptionManager;
public MyService(INotificationSubscriptionManager notificationSubscriptionManager)
{
_notificationSubscriptionManager = notificationSubscriptionManager;
}
//Subscribe to a general notification
public async Task Subscribe_SentFriendshipRequest(int? tenantId, long userId)
{
await _notificationSubscriptionManager.SubscribeAsync(new UserIdentifier(tenantId, userId), "SentFriendshipRequest");
}
//Subscribe to an entity notification
public async Task Subscribe_CommentPhoto(int? tenantId, long userId, Guid photoId)
{
await _notificationSubscriptionManager.SubscribeAsync(new UserIdentifier(tenantId, userId), "CommentPhoto", new EntityIdentifier(typeof(Photo), photoId));
}
}
Client-Side
This is done by signalr and you need to capture the event once it's triggered.
abp.event.on('abp.notifications.received', function (userNotification) {
if (userNotification.notification.data.type === 'Abp.Notifications.LocalizableMessageNotificationData') {
var localizedText = abp.localization.localize(
userNotification.notification.data.message.name,
userNotification.notification.data.message.sourceName
);
$.each(userNotification.notification.data.properties, function (key, value) {
localizedText = localizedText.replace('{' + key + '}', value);
});
alert('New localized notification: ' + localizedText);
} else if (userNotification.notification.data.type === 'Abp.Notifications.MessageNotificationData') {
alert('New simple notification: ' + userNotification.notification.data.message);
}
});
You can find more information here

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

How to flag that an exception has been handled in bot code?

I have a root dialog that creates a child dialog like so...
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> result)
{
var message = await result;
string userName = context.Activity?.From.Name;
var customerForm = new FormDialog<CarValuationDialog>(
new CarValuationDialog(userName),
() => CarValuationDialog.BuildForm(),
FormOptions.PromptInStart);
context.Call(customerForm, FormSubmitted);
}
The FormSubmitted method looks like....
public async Task FormSubmitted(IDialogContext context, IAwaitable<CarValuationDialog> result)
{
try
{
var form = await result;
}
catch (FormCanceledException<CarValuationDialog> e)
{
string reply;
if (e.InnerException == null)
{
reply = e.Message;
}
else
{
reply = e.InnerException.Message;
}
context.Reset();
await context.PostAsync(reply);
}
}
When an exception occurs in the child dialog, the method FormSubmitted is executed and goes into the catch block. However, when that method finishes, I still see the "Sorry my bot had an issue" type message appear to the user.
How can I tell the bot code not to fire the unhandled exception code, I believe is in PostUnhandledExceptionToUser? Is there a flag type property I need to set to true or something?
It looks like you are making the dialog stack empty when you have an exception: you should not have this context.Reset() below:
public async Task FormSubmitted(IDialogContext context, IAwaitable<CarValuationDialog> result)
{
try
{
var form = await result;
}
catch (FormCanceledException<CarValuationDialog> e)
{
string reply;
if (e.InnerException == null)
{
reply = e.Message;
}
else
{
reply = e.InnerException.Message;
}
context.Reset();
await context.PostAsync(reply);
}
}
Remove this line and the next message will be handled by your root dialog
in your message controller in POST method, use defaultifexception
await Conversation.SendAsync(activity, () => new Dialogs.DialogLUIS().DefaultIfException());

Resources