How to deal with Context.Done(R value) - bots

I have a msbot chat dialog that I want to have the following behaviour:
user -> get me some info about GARY
bot -> which gary, (prompt: choice options)
user -> gary peskett
bot -> sure, (hero card with gary's contact details)
I have this code
public class CustomerRepository
{
private IList<Customer> _customerList = new List<Customer>
{
new Customer
{
Name = "Gary Peskett"
},
new Customer
{
Name = "Gary Richards"
},
new Customer
{
Name = "Barry White"
}
};
public async Task<IEnumerable<Customer>> GetAll()
{
// usually calls a database (which is why async is on this method)
return _customerList;
}
}
public class XDialog : IDialog
{
private readonly IIntent _intent;
private readonly CustomerRepository _customerRepository;
public XDialog(IIntent intent, CustomerRepository customerRepository)
{
// An intent is decided before this point
_intent = intent;
_customerRepository = customerRepository;
}
public async Task StartAsync(IDialogContext context)
{
// // An intent can provide parameters
string name = _intent.Parameters["Name"] as string;
IEnumerable<Customer> customers = await _customerRepository.GetAll();
IList<Customer> limitedList = customers.Where(x => x.Name.Contains(name)).ToList();
if (limitedList.Any())
{
if (limitedList.Count > 1)
{
PromptDialog.Choice(context, LimitListAgain, limitedList,
"Can you specify which customer you wanted?");
}
else
{
Customer customer = limitedList.FirstOrDefault();
Finish(context, customer);
}
}
else
{
context.Done("No customers have been found");
}
}
private static async Task LimitListAgain(IDialogContext context, IAwaitable<Customer> result)
{
Customer customer = await result;
Finish(context, customer);
}
private static void Finish(IDialogContext context, Customer customer)
{
HeroCard heroCard = new HeroCard
{
Title = customer?.Name
};
context.Done(heroCard);
}
}
What i'm finding is that usually when I do context.Done(STRING) then that is output to the user, and this is really useful to end the dialog. As I want to end with a hero card, its outputing the typename
Microsoft.Bot.Connector.HeroCard
Can anyone help by either explaining a better way to use context.Done(R value) or help me return a hero card to end the dialog?
The dialog is being called with
Chain.PostToChain()
.Select(msg => Task.Run(() => _intentionService.Get(msg.ChannelId, msg.From.Id, msg.Text)).Result)
.Select(intent => _actionDialogFactory.Create(intent)) // returns IDialog based on intent
.Unwrap()
.PostToUser();

I think the problem is a side effect of using Chain.
As you may know, the context.Done doesn't post anything back to the user, it just ends the current dialog with the value provided.
The post to user is effectively happening in the .PostToUser() at the end of your Chain. Now, by looking into the PostToUser's code, I realized that at the end of the game, it's doing a context.PostAsync of item.ToString(), being item the payload provided in the context.Done in this case. See this.
One option (I haven't tested this), could be using .Do instead of .PostToUser() and manually perform what the PostToUserDialog does and finally perform a context.PostAsync() by creating a new IMessageActivity and adding the HeroCard as an attachment.

Related

How to handle options object sent by dc.beginDialog to other component dialog

In componentdialog A I have:
case '#trial': {
return await step.beginDialog(LEAD_CAPTURE_DIALOG, { intent: '#trial' });
That starts componentdialog B
In componentdialog B I need the intent to 'customize' the dialog.
class LeadCaptureDialog extends ComponentDialog {
constructor() {
super(LEAD_CAPTURE_DIALOG);
this.addDialog(new TextPrompt(NAME_PROMPT));
this.addDialog(new WaterfallDialog(WATERFALL_DIALOG, [
this.getNameStep.bind(this),
]));
this.initialDialogId = WATERFALL_DIALOG;
}
async getNameStep(stepContext) {
console.log(arguments[0]);
await stepContext.context.sendActivity('Message varies based on the intent send in the options object in beginDialog');
return await stepContext.prompt(NAME_PROMPT, 'What is your name?');
I only have the arguments object and the options object (with the intent) is indeed in there. I trust there must be a more efficient way to use the (optional) options object in beginDialog.
Does anyone know how?
This is easier in Node than C#, since stepContext.options is just a JSON object and there's no typing (if you're using TypeScript, it's better to create an Interface, similar to the C# example below).
Send The Intent to Your LEAD_CAPTURE_DIALOG
await stepContext.beginDialog(LEAD_CAPTURE_DIALOG, { intent: '#trial' });
Capture it in stepContext.options of LEAD_CAPTURE_DIALOG
const intent = stepContext.options["intent"];
C# Version, since I accidentally answered with this, first
You have two options:
1. Create a new Class/Model
(as #MattStannett suggested) This is the option likely considered "best practice".
Create MyObject.cs
namespace MyBot
{
public class MyObject
{
public string Intent { get; set; }
}
}
Send The Intent to Your LEAD_CAPTURE_DIALOG
await stepContext.BeginDialogAsync(LEAD_CAPTURE_DIALOG, new MyObject() { Intent = "#trial" });
Capture it in stepContext.Options of LEAD_CAPTURE_DIALOG
var intent = (stepContext.Options as MyObject).Intent;
2. Use an Anonymous Object
await stepContext.BeginDialogAsync(LEAD_CAPTURE_DIALOG, new { Intent = "#trial" });
var intent = stepContext.Options.GetType().GetProperty("Intent").GetValue(stepContext.Options, null);
// OR with C#4+:
var intent = (stepContext.Options as dynamic).Intent;

How does navigation work with LUIS subdialogs?

I have a question... Unfortunately all the samples on the web are too shallow and don't really cover this well:
I have a RootDialog that extends the LuisDialog. This RootDialog is responsible for figuring out what the user wants to do. It could be multiple things, but one of them would be initiating a new order. For this, the RootDialog would forward the call to the NewOrderDialog, and the responsibility of the NewOrderDialog would be to figure out some basic details (what does the user want to order, which address does he like to use) and finally it will confirm the order and return back to the RootDialog.
The code for the RootDialog is very simple:
[Serializable]
public class RootDialog : LuisDialog<object>
{
public RootDialog() : base(new LuisService(new LuisModelAttribute(ConfigurationManager.AppSettings["LuisAppId"], ConfigurationManager.AppSettings["LuisAPIKey"], domain: "westus.api.cognitive.microsoft.com")))
{
}
[LuisIntent("Order.Place")]
public async Task PlaceOrderIntent(IDialogContext context, LuisResult result)
{
await context.Forward(new NewOrderDialog(), OnPlaceOrderIntentCompleted, context.Activity, CancellationToken.None);
context.Wait(MessageReceived);
}
private async Task OnPlaceOrderIntentCompleted(IDialogContext context, IAwaitable<object> result)
{
await context.PostAsync("Your order has been placed. Thank you for shopping with us.");
context.Wait(MessageReceived);
}
}
I also had some code in mind for the NewOrderDialog:
[Serializable]
public class NewOrderDialog : LuisDialog<object>
{
private string _product;
private string _address;
public NewOrderDialog() : base(new LuisService(new LuisModelAttribute(ConfigurationManager.AppSettings["LuisAppId"], ConfigurationManager.AppSettings["LuisAPIKey"], domain: "westus.api.cognitive.microsoft.com")))
{
}
[LuisIntent("Order.RequestedItem")]
public async Task RequestItemIntent(IDialogContext context, LuisResult result)
{
EntityRecommendation item;
if (result.TryFindEntity("Item", out item))
{
_product = item.Entity;
await context.PostAsync($"Okay, I understood you want to order: {_product}.");
}
else
{
await context.PostAsync("I couldn't understand what you would like to buy. Can you try it again?");
}
context.Wait(MessageReceived);
}
[LuisIntent("Order.AddedAddress")]
public async Task AddAddressIntent(IDialogContext context, LuisResult result)
{
EntityRecommendation item;
if (result.TryFindEntity("Address", out item))
{
_address = item.Entity;
await context.PostAsync($"Okay, I understood you want to ship the item to: {_address}.");
}
else
{
await context.PostAsync("I couldn't understand where you would like to ship the item. Can you try it again?");
}
context.Wait(MessageReceived);
}
}
The code as listed doesn't work. Upon entering the Order.Place intent, it immediately executes the 'success' callback, and then throws this exception:
Exception: IDialog method execution finished with multiple resume handlers specified through IDialogStack.
[File of type 'text/plain']
So I have a few questions:
How do I solve the error that I get?
How can I, upon entering the NewOrderDialog, check if we already know what the product and address is, and if not prompt them for the correct info?
Why does the NewOrderDialog get closed even though I don't call anything like context.Done()? I only want it to be closed when all the info has been gathered and the order has been confirmed.
So the first issue is that you are doing a context.Forward and a context.Wait in "Order.Place", which by definition is wrong. You need to choose: forward to a new dialog or wait in the current. Based on your post, you want to forward, so just remove the Wait call.
Besides that, you have 1 LUIS dialog and you are trying to forward to a new LUIS dialog... I have my doubts if that will work on not; I can imagine those are two different LUIS models otherwise it will be just wrong.
Based on your comment, I now understand what you are trying to do with the second dialog. The problem (and this is related to your second question) is that using LUIS in tha way might be confusing. Eg:
user: I want to place an order
bot => Forward to new dialog. Since it's a forward, the activity.Text will likely go to LUIS again (to the model of the second dialog) and nothing will be detected. The second dialog will be in Wait state, for user input.
Now, how the user will know that he needs to enter an address or a product? Where are you prompting the user for them? See the issue?
Your third question I suspect is a side effect of the error you are having in #1, which I already provided the solution for.
If you clarify a bit more I might be even more helpful. What you are trying to do with LUIS in the second dialog it doesn't look ok, but maybe with an explanation might have sense.
A usual scenario would be: I get the intent from LUIS ("Order.Place") and then I start a FormFlow or a set of Prompts to get the info to place the order (address, product, etc) or if you want to keep using LUIS you might want to check Luis Action Binding. You can read more on https://blog.botframework.com/2017/04/03/luis-action-binding-bot/.
Do you know about the Bing Location Control for Microsoft Bot Framework? It can be used to handle the 'which address does he like to use' part in obtaining and validating the user's address.
Here is some sample code:
[LuisModel("xxx", "yyy")]
[Serializable]
public class RootDialog : LuisDialog<object>
{
...
[LuisIntent("Find Location")]
public async Task FindLocationIntent(IDialogContext context, LuisResult result)
{
try
{
context.Call(new FindUserLocationDialog(), ResumeAfterLocationDialog);
}
catch (Exception e)
{
// handle exceptions
}
}
public async Task ResumeAfterLocationDialog(IDialogContext context, IAwaitable<object> result)
{
var resultLocation = await result;
await context.PostAsync($"Your location is {resultLocation}");
// do whatever you want
context.Wait(this.MessageReceived);
}
}
And for "FindUserLocationDialog()", you can follow the sample from Line 58 onwards.
Edit:
1) Can you try using:
[LuisIntent("Order.Place")]
public async Task PlaceOrderIntent(IDialogContext context, LuisResult result)
{
context.Forward(new NewOrderDialog(), OnPlaceOrderIntentCompleted, context.Activity, CancellationToken.None);
// or this
// context.Call(new NewOrderDialog(), OnPlaceOrderIntentCompleted);
}
2) I would say it depends on how you structured your intent. Does your "Order.Place" intent include entities? Meaning to say, if your user said "I want to make an order of Product X addressed to Address Y", does your intent already pick up those entities?
I would suggest that you check the product and address under your "Order.Place" intent. After you obtained and validated the product and address, you can forward it to another Dialog (non-LUIS) to handle the rest of the order processing.
[LuisIntent("Order.Place")]
public async Task PlaceOrderIntent(IDialogContext context, LuisResult result)
{
EntityRecommendation item;
if (result.TryFindEntity("Item", out item))
{
_product = item.Entity;
}
if (result.TryFindEntity("Address", out item))
{
_address = item.Entity;
}
if (_product == null)
{
PromptDialog.Text(context, this.MissingProduct, "Enter the product");
}
else if (_address == null)
{
PromptDialog.Text(context, this.MissingAddress, "Enter the address");
}
// both product and address present
context.Forward(new NewOrderDialog(), OnPlaceOrderIntentCompleted, **object with _product and _address**, CancellationToken.None);
}
private async Task MissingProduct(IDialogContext context, IAwaitable<String> result)
{
_product = await result;
// perform some validation
if (_address == null)
{
PromptDialog.Text(context, this.MissingAddress, "Enter the address");
}
else
{
// both product and address present
context.Forward(new NewOrderDialog(), OnPlaceOrderIntentCompleted, **object with _product and _address**, CancellationToken.None);
}
}
private async Task MissingAddress(IDialogContext context, IAwaitable<String> result)
{
_address = await result;
// perform some validation
// both product and address present
context.Forward(new NewOrderDialog(), OnPlaceOrderIntentCompleted, **object with _product and _address**, CancellationToken.None);
}
Something along these lines. Might need to include try/catch.
3) I think it has got to do with your 'context.Wait(MessageReceived)' in 'Order.Place' LUIS intent

Microsoft Bot Framework - Proactive message, suspend current dialog

I've created a simple news bot which sends updates to a user every 24 hours. I've created a callback controller which handles requests from an external service, processes the resumptionCookie, then sends a carousel of articles with two buttons to the user. One of the buttons opens a browser window, the other should trigger a new dialog (OptionsDialog).
Is this implementation correct? and is it possible to suspend any active dialog, whilst the user interacts with the news article message? For example, if i'm going through a particular dialog, then suddenly I get a news alert, is it possible to suspend the current dialog, to allow the user to update the options of the alerts (almost like the news alert is outside the normal dialog flow), then once they've finished they'll return to the previous dialog. Hopefully, the question is clear enough. Any help will be greatly appreciated.
public class CallbackController : ApiController
{
public async Task<IHttpActionResult> Post(ResumptionCookie resumptionCookie)
{
var activity = (Activity)resumptionCookie.GetMessage();
var reply = activity.CreateReply();
reply.Text = "We found 7 news articles that match your criteria";
reply.Attachments = new List<Attachment>
{
new ThumbnailCard
{
Buttons = new List<CardAction>
{
new CardAction(ActionTypes.OpenUrl, "BBC", null, "http://www.bbc.co.uk"),
new CardAction(ActionTypes.PostBack, "Update Options", null, "Update Options")
}
}.ToAttachment()
};
var client = new ConnectorClient(new Uri(activity.ServiceUrl));
await client.Conversations.ReplyToActivityAsync(reply);
return Ok(new { success = true });
}
}
This is my Main dialog
[Serializable]
public class MainDialog : IDialog
{
public async Task StartAsync(IDialogContext context)
{
context.Wait(ProcessMessage);
}
private async Task ProcessMessage(IDialogContext context, IAwaitable<IMessageActivity> result)
{
var response = await result;
if (response.Text.Equals("Update Options", StringComparison.OrdinalIgnoreCase))
{
context.Call(new OptionsDialog(), FinishMainDialog);
}
else
{
PromptDialog.Confirm(context, ProcessChoice, "Do you wish to continue?");
}
}
private async Task ProcessChoice(IDialogContext context, IAwaitable<bool> result)
{
var choice = await result;
if(choice)
{
context.Call(new DialogOne(), FinishMainDialog);
}
else
{
context.Done(true);
}
}
private async Task FinishMainDialog(IDialogContext context, IAwaitable<object> result)
{
context.Done(true);
}
}
Here is my frequency dialog
[Serializable]
public class OptionsDialog : IDialog
{
public async Task StartAsync(IDialogContext context)
{
await context.PostAsync("You can update your to options here");
context.Wait(ProcessMessage);
}
public async Task ProcessMessage(IDialogContext context, IAwaitable<IMessageActivity> activity)
{
await context.PostAsync("Hello, World!");
context.Done(true);
}
}

C# Bot Framework : Form flow set value for the field based on previous Answer [duplicate]

Hello I'm new to Microsoft Bot Framework and I have a question that I couldn't find an answer to.
I have a FormFlow that ask the user for some question, after a specific question I want the bot to do some logic and show messages accordingly (for example if the user selected option 1 then show message X and if the user selected option 2 show message Y).
Here is my code:
using Microsoft.Bot.Builder.FormFlow;
using Microsoft.Bot.Builder.Dialogs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace Bot_CRM.FormFlow
{
public enum RequestOptions { Unknown, CheckStatus, CreateCase };
[Serializable]
public class CaseFormFlow
{
public RequestOptions RequestType;
[Prompt("What is your first name?")]
public string FirstName;
public string LastName;
public string ContactNumber;
[Prompt("Please enter your id")]
public string Id;
public static IForm<CaseFormFlow> BuildForm()
{
OnCompletionAsyncDelegate<CaseFormFlow> processRequest = async (context, state) =>
{
await context.PostAsync($#"Thanks for your request");
};
return new FormBuilder<CaseFormFlow>()
.Message("Hello and welcom to my service desk bot")
.Field(nameof(FirstName))
.Message("hello {FirstName}")
.Field(nameof(Id))
.Field(nameof(RequestType)) =>
//here if user select 1 start flow of check status and if user select 2 start flow of create case
.AddRemainingFields()
.Message("Thank you request. Our help desk team will get back to you shortly.")
.OnCompletion(processRequest)
.Build();
}
}
}
Updated code after Ezequiel's suggestion:
return new FormBuilder<CaseFormFlow>()
.Message("Hello and welcom to my service desk bot")
.Field(nameof(FirstName))
.Message("hello {FirstName}")
.Field(new FieldReflector<CaseFormFlow>(nameof(RequestType))
.SetActive(state => state.AskUserForRequestType)
.SetNext((value, state) =>
{
var selection = (RequestOptions)value;
if (selection == RequestOptions.CheckStatus)
{
return new NextStep(new[] { nameof(Id) });
}
else
{
return new NextStep();
}
}))
Thanks in advance for the help
This is a great question.The key thing is to use the SetActive and SetNext methods of the Field<T> class. You should consider using the FieldReflector class; though you can implement your own IField.
SetActive is described in the Dynamic Fields section of the FormFlow documentation. Basically it provides a delegate that enables the field based on a condition.
SetNext will allow you to decide what step of the form should come next based on your custom logic.
You can take a look to the ContosoFlowers sample. In the Order form; something similar is being done.
public static IForm<Order> BuildOrderForm()
{
return new FormBuilder<Order>()
.Field(nameof(RecipientFirstName))
.Field(nameof(RecipientLastName))
.Field(nameof(RecipientPhoneNumber))
.Field(nameof(Note))
.Field(new FieldReflector<Order>(nameof(UseSavedSenderInfo))
.SetActive(state => state.AskToUseSavedSenderInfo)
.SetNext((value, state) =>
{
var selection = (UseSaveInfoResponse)value;
if (selection == UseSaveInfoResponse.Edit)
{
state.SenderEmail = null;
state.SenderPhoneNumber = null;
return new NextStep(new[] { nameof(SenderEmail) });
}
else
{
return new NextStep();
}
}))
.Field(new FieldReflector<Order>(nameof(SenderEmail))
.SetActive(state => !state.UseSavedSenderInfo.HasValue || state.UseSavedSenderInfo.Value == UseSaveInfoResponse.Edit)
.SetNext(
(value, state) => (state.UseSavedSenderInfo == UseSaveInfoResponse.Edit)
? new NextStep(new[] { nameof(SenderPhoneNumber) })
: new NextStep()))
.Field(nameof(SenderPhoneNumber), state => !state.UseSavedSenderInfo.HasValue || state.UseSavedSenderInfo.Value == UseSaveInfoResponse.Edit)
.Field(nameof(SaveSenderInfo), state => !state.UseSavedSenderInfo.HasValue || state.UseSavedSenderInfo.Value == UseSaveInfoResponse.Edit)
.Build();
}
}
}

ASP.net Identity Disable User

Using the new ASP.net Identity in MVC 5, How do we disable a user from logging in? I don't want to delete them, maybe just disable their account for a time period.
Does anyone have any ideas on this as I don't see a status column or anything on the ASPNetUsers table.
await userManager.SetLockoutEnabledAsync(applicationUser.Id, true);
await userManager.SetLockoutEndDateAsync(DateTime.Today.AddYears(10));
Update: As CountZero points out, if you're using v2.1+, then you should try and use the lockout functionality they added first, before trying the solution below. See their blog post for a full sample: http://blogs.msdn.com/b/webdev/archive/2014/08/05/announcing-rtm-of-asp-net-identity-2-1-0.aspx
Version 2.0 has the IUserLockoutStore interface that you can use to lockout users, but the downside is that there is no OOB functionality to actually leverage it beyond the pass-through methods exposed by the UserManager class. For instance, it would be nice if it would actually increment the lockout count as a part of the standard username/password verification process. However, it's fairly trivial to implement yourself.
Step #1: Create a custom user store that implements IUserLockoutStore.
// I'm specifying the TKey generic param here since we use int's for our DB keys
// you may need to customize this for your environment
public class MyUserStore : IUserLockoutStore<MyUser, int>
{
// IUserStore implementation here
public Task<DateTimeOffset> GetLockoutEndDateAsync(MyUser user)
{
//..
}
public Task SetLockoutEndDateAsync(MyUser user, DateTimeOffset lockoutEnd)
{
//..
}
public Task<int> IncrementAccessFailedCountAsync(MyUser user)
{
//..
}
public Task ResetAccessFailedCountAsync(MyUser user)
{
//..
}
public Task<int> GetAccessFailedCountAsync(MyUser user)
{
//..
}
public Task<bool> GetLockoutEnabledAsync(MyUser user)
{
//..
}
public Task SetLockoutEnabledAsync(MyUser user, bool enabled)
{
//..
}
}
Step #2: Instead of UserManager, use the following class in your login/logout actions, passing it an instance of your custom user store.
public class LockingUserManager<TUser, TKey> : UserManager<TUser, TKey>
where TUser : class, IUser<TKey>
where TKey : IEquatable<TKey>
{
private readonly IUserLockoutStore<TUser, TKey> _userLockoutStore;
public LockingUserManager(IUserLockoutStore<TUser, TKey> store)
: base(store)
{
if (store == null) throw new ArgumentNullException("store");
_userLockoutStore = store;
}
public override async Task<TUser> FindAsync(string userName, string password)
{
var user = await FindByNameAsync(userName);
if (user == null) return null;
var isUserLockedOut = await GetLockoutEnabled(user);
if (isUserLockedOut) return user;
var isPasswordValid = await CheckPasswordAsync(user, password);
if (isPasswordValid)
{
await _userLockoutStore.ResetAccessFailedCountAsync(user);
}
else
{
await IncrementAccessFailedCount(user);
user = null;
}
return user;
}
private async Task<bool> GetLockoutEnabled(TUser user)
{
var isLockoutEnabled = await _userLockoutStore.GetLockoutEnabledAsync(user);
if (isLockoutEnabled == false) return false;
var shouldRemoveLockout = DateTime.Now >= await _userLockoutStore.GetLockoutEndDateAsync(user);
if (shouldRemoveLockout)
{
await _userLockoutStore.ResetAccessFailedCountAsync(user);
await _userLockoutStore.SetLockoutEnabledAsync(user, false);
return false;
}
return true;
}
private async Task IncrementAccessFailedCount(TUser user)
{
var accessFailedCount = await _userLockoutStore.IncrementAccessFailedCountAsync(user);
var shouldLockoutUser = accessFailedCount > MaxFailedAccessAttemptsBeforeLockout;
if (shouldLockoutUser)
{
await _userLockoutStore.SetLockoutEnabledAsync(user, true);
var lockoutEndDate = new DateTimeOffset(DateTime.Now + DefaultAccountLockoutTimeSpan);
await _userLockoutStore.SetLockoutEndDateAsync(user, lockoutEndDate);
}
}
}
Example:
[AllowAnonymous]
[HttpPost]
public async Task<ActionResult> Login(string userName, string password)
{
var userManager = new LockingUserManager<MyUser, int>(new MyUserStore())
{
DefaultAccountLockoutTimeSpan = /* get from appSettings */,
MaxFailedAccessAttemptsBeforeLockout = /* get from appSettings */
};
var user = await userManager.FindAsync(userName, password);
if (user == null)
{
// bad username or password; take appropriate action
}
if (await _userManager.GetLockoutEnabledAsync(user.Id))
{
// user is locked out; take appropriate action
}
// username and password are good
// mark user as authenticated and redirect to post-login landing page
}
If you want to manually lock someone out, you can set whatever flag you're checking in MyUserStore.GetLockoutEnabledAsync().
You can have a new class, which should be derived from IdentityUser class. YOu can add a boolean property in the new class and can use this new property of take care per check for login process. I also done it pretty well. I might wanna take a look at : blog
UserManager.RemovePasswordAsync("userId") will effectively disable a user. If the user has no password he will not be able to log in. You will need to set a new password to enable the user again.

Resources