Microsoft Bot Framework start the conversation from specific waterfall step in dialog - dialog

Microsoft Bot Framework V4, I have a waterfall Dialog defined in a dialog as below
var waterfallSteps = new WaterfallStep[]
{
CallConfirmAsync,
SimilarProductAsync,
CheckNewVersionAsync,
};
AddDialog(new WaterfallDialog("productenquiry", waterfallSteps));
After the execution of the first two waterfall steps, my conversation is stop due to unresponsiveness from user's end. So I want to resume from the third method when i.e., CheckNewVersionAsync when the user comes back again to the bot.
Can anyone please help me here.

So, at the bot level, this should happen automatically if you've configured the IStatePropertyAccessor<DialogState> with the ConversationState. No matter how long the user takes to respond, your WaterfallDialog will stay at the top of the stack and it will remember exactly what step it was on. Assuming your user comes back to the same conversation, then it will pick right up where it left off.
Given that, the fact that you are asking this question leads me to believe that perhaps you are using WebChat which doesn't maintain the same conversationId across page loads unless you set that up yourself. If that's the case, then I would suggest you ask another question about how to do that if you can't figure out how since that's a separate issue from the dialog state being persisted correctly.

Edit: Drew's answer is correct, but mine provides another potential solution. You can find more info here: Managing State. In particular:
User state is available in any turn that the bot is conversing with
that user on that channel, regardless of the conversation Conversation
state is available in any turn in a specific conversation, regardless
of user (i.e. group conversations) Private conversation state is
scoped to both the specific conversation and to that specific user
Tip
Both user and conversation state are scoped by channel. The same
person using different channels to access your bot appears as
different users, one for each channel, and each with a distinct user
state.
Additional Solution
This solution is best for if you're able to specify the from Id, but cannot ensure that conversation Id remains the same (see below, under Gotchas).
You could save what step the user is on in their UserState.
BasicBot
BasicBot does this with its GreetingState class.
From its GreetingDialog:
In the first step, it initializes the GreetingState, which tracks how far along in the dialog the user is by seeing what user variables have already been set:
private async Task<DialogTurnResult> InitializeStateStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var greetingState = await UserProfileAccessor.GetAsync(stepContext.Context, () => null);
if (greetingState == null)
{
var greetingStateOpt = stepContext.Options as GreetingState;
if (greetingStateOpt != null)
{
await UserProfileAccessor.SetAsync(stepContext.Context, greetingStateOpt);
}
else
{
await UserProfileAccessor.SetAsync(stepContext.Context, new GreetingState());
}
}
return await stepContext.NextAsync();
}
And then in each step, it loads the GreetingState:
var greetingState = await UserProfileAccessor.GetAsync(stepContext.Context);
And checks to see if the step has already been completed with something like:
if (greetingState != null && !string.IsNullOrWhiteSpace(greetingState.Name) && !string.IsNullOrWhiteSpace(greetingState.City))
If there's no greetingState or .Name or .City exists, it prompts for them, and if they are already filled out, it moves on with:
return await stepContext.NextAsync();
At each step, it saves to the GreetingState with something like:
greetingState.Name = char.ToUpper(lowerCaseName[0]) + lowerCaseName.Substring(1);
await UserProfileAccessor.SetAsync(stepContext.Context, greetingState);
Simplifying for your use case
For you, if you don't need to save user information, you could create a simple Step class:
{
/// <summary>
/// User state properties for Waterfall Step.
/// </summary>
public class Step
{
public string StepNumber { get; set; }
}
}
Make the first step of your WaterfallDialog:
private async Task<DialogTurnResult> InitializeStateStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var StepState = await UserProfileAccessor.GetAsync(stepContext.Context, () => null);
if (StepState == null)
{
var StepStateOpt = stepContext.Options as StepState;
if (StepStateOpt != null)
{
await UserProfileAccessor.SetAsync(stepContext.Context, StepStateOpt );
}
else
{
await UserProfileAccessor.SetAsync(stepContext.Context, new StepState());
}
}
return await stepContext.NextAsync();
}
On each step, load the current Step:
var stepState = await UserProfileAccessor.GetAsync(stepContext.Context);
Check to see if they're already past the current step:
if (stepState.StepNumber <= 2)
{
// ...do stuff
// Save that user has completed step
stepState.StepNumber++;
await UserProfileAccessor.SetAsync(stepContext.Context, stepState);
}
else
{
return await stepContext.NextAsync();
}
Gotchas
A couple big things to watch out for:
The UserState only persists for the same from ID and channel ID. Make sure that the user that leaves in the middle of a waterfall has the same from ID when they re-enter it and that they re-enter it from the same channel. This isn't the default for the Emulator--in the Emulator, when a session is restarted, a new from ID is created. (Note: Consider from ID to be synonymous with User ID. It just comes from Activity.From.Id)
The ConversationState only persists for the same conversation ID and channel ID. Persistence of the conversation ID within the channel varies by channel.
More info on the different IDs: ID fields in the Bot Framework.

Related

Lost ability to capture unique Conversation_ID for each new session

Using Bot Builder 4.11.1 .Net and seemed to have lost the ability to capture any unique identifier for each new session. I need a unique identifier to keep state with the AI engine I am using to respond to input. Any suggestions? So, to expand, if I have a slack bot, for example, each time a user logs into Slack and starts a conversation with MyBot, I need a new unique identifier.
protected override async Task OnMembersAddedAsync(IList<ChannelAccount> membersAdded, ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
{
var welcomeText = "Hello and welcome!";
Random rnd1 = new Random();
foreach (var member in membersAdded)
{
if (member.Id != turnContext.Activity.Recipient.Id)
{
await turnContext.SendActivityAsync(MessageFactory.Text(welcomeText, welcomeText), cancellationToken);
}
}
}
}
Unless I'm missing something, you should be able to get the information you need from TurnContext. In my code I'm running this in onMessage only (as in my case I only will message the user if they have sent the bot a message), but I don't see why you couldn't use this in onMembersAdded as well. I don't know how channels like Slack work, but in Microsoft Teams the user is just "added" when they first talk to the bot, and you don't end up in onMembersAdded unless they remove and read the bot in Teams. So if you may want to grab the conversation reference in the future, you may want to have it in onMessage or in both places. Also if you need the activity ID for some reason, as this will obviously update with each activity (though I haven't had any need for this information). Here is how you get the conversation reference. I am storing this in my conversation state and have assumed you are familiar with that but let me know if you need further help there. I also store in an Azure table to be accessed outside of the bot (e.g. I have an Azure Function that uses this to send proactive followups).
const { TurnContext } = require('botbuilder');
const conversationData = await this.dialogState.get(context, {});
conversationData.conversationReference = TurnContext.getConversationReference(context.activity);
await this.conversationState.saveChanges(context);
And that's it! Here is a sample conversation reference. Note that if you are storing this in Azure Tables or similar, you'll likely need to stringify it and re-parse when you pull it out.
{
"activityId":"ACTIVITY_ID",
"user": {
"id":"USER_ID",
"name":"USER_NAME",
"aadObjectId":"AAD_OBJECT_ID",
"role":"user"
},
"bot": {
"id":"BOT_ID",
"name":"BOT_NAME"
},
"conversation": {
"conversationType":"personal",
"tenantId":"YOUR_TENANT_ID",
"id":"YOUR_CONVERSATION_ID"
},
"channelId":"msteams",
"locale":"en-US",
"serviceUrl":"https://smba.trafficmanager.net/amer/"
}
What you are looking for (I think) is conversationReference.conversation.id. Different channels are going to have different attributes in the conversation reference, but this ID should always be there.

How can I add user info to conv.user.storage?

I'm using Actions Builder to create my chatbot and after user logins using Google I want to save his ID to storage variable.
This storage variable doesn't exist on conv.user.
So I do this:
if (conv.user.verificationStatus === 'VERIFIED') {
conv.user.storage = {};
conv.user.storage.id = str.rows[0].id;
console.log("STORAGE");
console.log(conv.user.storage.id);
}
But on Google Assistant it returns the error message and on my Webhook it's all good (no errors shown):
Google Assistant Error
What can I do to save/persist at least my user ID for future referings?
Since user has the Google Sign In process done once, every time he enters in your action you have his info on the request (payload). It´s automatically added to user storage.
You should store it on conv.user.params and refer to it in your code.
You may have a get and set method to help you with:
getUserId(conv) {
return conv.user.params.userId;
}
setUserId(conv, userId) {
try {
conv.user.params.userId = userId;
} catch (e) {
throw new error("Error setting USERID");
}
return userId;
}

Cloud Functions and Cloud Firestore USER ASSOCIATION FUNCTION

Shortly, imagine I have a Cloud Firestore DB where I store some users data such as email, geo-location data (as geopoint) and some other things.
In Cloud Functions I have "myFunc" that runs trying to "link" two users between them based on a geo-query (I use GeoFirestore for it).
Now everything works well, but I cannot figure out how to avoid this kind of situation:
User A calls myFunc trying to find a person to be associated with, and finds User B as a possible one.
At the same time, User B calls myFunc too, trying to find a person to be associated with, BUT finds User C as possible one.
In this case User A would be associated with User B, but User B would be associated with User C.
I already have a field called "associated" set to FALSE on each user initialization, that becomes TRUE whenever a new possible association has been found.
But this code cannot guarantee the right association if User A and User B trigger the function at the same time, because at the moment in which the function triggered by User A will find User B, the "associated" field of B will be still set to false because B is still searching and has not found anybody yet.
I need to find a solution otherwise I'll end up having
wrong associations ( User A pointing at User B, but User B pointing at User C ).
I also thought about adding a snapshotListener to the user who is searching, so in that way if another User would update the searching user's document, I could terminate the function, but I'm not really sure it will work as expected.
I'd be incredibly grateful if you could help me with this problem.
Thanks a lot!
Cheers,
David
HERE IS MY CODE:
exports.myFunction = functions.region('europe-west1').https.onCall( async (data , context) => {
const userDoc = await firestore.collection('myCollection').doc(context.auth.token.email).get();
if (!userDoc.exists) {
return null;
}
const userData = userDoc.data();
if (userData.associated) { // IF THE USER HAS ALREADY BEEN ASSOCIATED
return null;
}
const latitude = userData.g.geopoint["latitude"];
const longitude = userData.g.geopoint["longitude"];
// Create a GeoQuery based on a location
const query = geocollection.near({ center: new firebase.firestore.GeoPoint(latitude, longitude), radius: userData.maxDistance });
// Get query (as Promise)
let otherUser = []; // ARRAY TO SAVE THE FIRST USER FOUND
query.get().then((value) => {
// CHECK EVERY USER DOC
value.docs.map((doc) => {
doc['data'] = doc['data']();
// IF THE USER HAS NOT BEEN ASSOCIATED YET
if (!doc['data'].associated) {
// SAVE ONLY THE FIRST USER FOUND
if (otherUser.length < 1) {
otherUser = doc['data'];
}
}
return null;
});
return value.docs;
}).catch(error => console.log("ERROR FOUND: ", error));
// HERE I HAVE TO RETURN AN .update() OF DATA ON 2 DOCUMENTS, IN ORDER TO UPDATE THE "associated" and the "userAssociated" FIELDS OF THE USER WHO WAS SEARCHING AND THE USER FOUND
return ........update({
associated: true,
userAssociated: otherUser.name
});
}); // END FUNCTION
You should use a Transaction in your Cloud Function. Since Cloud Functions are using the Admin SDK in the back-end, Transactions in a Cloud Function use pessimistic concurrency controls.
Pessimistic transactions use database locks to prevent other operations from modifying data.
See the doc form more details. In particular, you will read that:
In the server client libraries, transactions place locks on the
documents they read. A transaction's lock on a document blocks other
transactions, batched writes, and non-transactional writes from
changing that document. A transaction releases its document locks at
commit time. It also releases its locks if it times out or fails for
any reason.
When a transaction locks a document, other write operations must wait
for the transaction to release its lock. Transactions acquire their
locks in chronological order.

args not returning expected LUIS result after implementing BotAuth

I have been creating a chat bot with MS Bot Framework in Nodejs and LUIS. I am recently trying to get certain information from the MS Graph API, and have (sort of) successfully implemented BotAuth and am able to get the information I want.
The issue I am facing now is that for the dialog that implements BotAuth, I am not able to get the usual args that comes with LUIS-intents triggered dialogs. Thus, I am not able to get any entities that the user might have entered. Other dialogs that do not implement BotAuth have no issues with this.
What I am getting now from args is:
{ response: undefined, resumed: 4 }
I am guessing that the issue lies with the [].concat part in this section:
bot.dialog('refreshSchDialog-oauth', [].concat(
ba.authenticate("aadv2"),
(session, args, skip) => {
let user = ba.profile(session, "aadv2");
session.endDialog(user.displayName);
session.userData.accessToken = user.accessToken;
session.userData.refreshToken = user.refreshToken;
console.log('args');
console.log(args);
if (user.accessToken) {
session.send('got leh');
// valid access token, check if luis has any entities (MV name)
// if there is, store conversationData and move to next dialog
if (args.entities) {
for (i = 0; i < args.entities.length; i++) {
if (args.entities[i].type == 'dbName') {
session.conversationData.mvName = args.entities[i].entity;
session.send(args.entities[i].entity);
}
}
}
session.beginDialog('refreshSchDialog');
} else {
// no valid access token
// TODO error message
}
}))
.triggerAction({
matches: 'refreshSchema',
intentThreshold: 0.3
});
May I know why the args is not returning the information from LUIS?
Looking at the BotAuth code it appears that the Auth dialog returns the user if properly authenticated or false if the dialog failed. It doesn’t copy over the args from LUIS. I would change your code so that the first function in your waterfall stores the LUIS data into session.dialogData, then call ba.authenticate and then use both results in your last waterfall step.

"User not found" Error when creating a child entity using ControllerB action, soon after an aspUser is created in controllerA

After the successful creation of an application user and the following line of code (in Register action in AccountController) :
await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
I am trying to add a child object
var controller=DependencyResolver.Current.GetService<AnotherController>();
controller.ControllerContext = new ControllerContext(Request.RequestContext, controller);
var res = controller.Create(
new ChildEntity
{
ApplicationUserId = user.Id,
IsAcative = true
});
my create Method looks like this
public async Task<ActionResult> Create(ChildEntity entity)
{
if (ModelState.IsValid)
{
db.ChildEntity.Add(entity);
await db.SaveChangesAsync();
return RedirectToAction("Index");
}
return View(entity);
}
My object is not created. the return valueres contains the error "user not found" propertyName : "ApplicationUserId"
Can anybody help me to understand what is going on?
ps : i have noticed that the User.Identity.GetUserId() return null !!! (may be fo some other reason, may be my problem is linked to this..)
First and foremost, the user principal is not populated until after the next page load. The sign-in process merely sets the auth cookie. That cookie needs to be sent back and the auth machinery needs to run (as part of the request pipeline), before you can get anything from User.
Second, what you're doing here is just absolutely wrong. If you want to reuse the user creation code, factor it out into another class that all your controllers can utilize. It's absolutely the wrong approach to try to new up a controller inside another action to call an action on that.

Resources