TextPrompt reprompts after interruption - botframework

TL;DR
When my bot is waiting in a Prompt (e.g. TextPrompt) and another dialog ends (because user input triggered an interrupt action such as 'help', which started an help dialog that just outputs help text), the OnPromptAsync method of that Prompt is called and prompts the Prompts text again. I don't want this. I want the Prompt dialog to wait for user input after the help dialog has ended.
Detailed
I have a bot that prompts something using TextPrompt and then waits for the user to reply. I've implemented user interruptions as described here to catch requests for help. If the user typed in 'help' the bot should output some help text (in the ExampleDialog, see below) and then again wait for user inputs.
The MainDialog
public class MainDialog : ComponentDialog
{
public MainDialog() : base("Main")
{
AddDialog(new TextPrompt(nameof(TextPrompt)));
AddDialog(new ExampleDialog(nameof(ExampleDialog)));
AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[]
{
PromptStep,
EvaluationStep
}));
InitialDialogId = nameof(WaterfallDialog);
}
protected override async Task<DialogTurnResult> OnContinueDialogAsync(DialogContext innerDc, CancellationToken cancellationToken = default)
{
if (!string.IsNullOrEmpty(innerDc.Context.Activity.Text))
{
// Check for interruptions
var result = await InterruptAsync(innerDc, cancellationToken);
if (result != null)
{
return result;
}
}
return await base.OnContinueDialogAsync(innerDc, cancellationToken);
}
private async Task<DialogTurnResult> InterruptAsync(DialogContext innerDc, CancellationToken cancellationToken)
{
if (innerDc.Context.Activity.Type == ActivityTypes.Message)
{
string text = innerDc.Context.Activity.Text;
// Catch request for help
if (text == "help")
{
await innerDc.BeginDialogAsync(nameof(ExampleDialog), null, cancellationToken);
return Dialog.EndOfTurn;
}
}
return null;
}
private async Task<DialogTurnResult> PromptStep(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
return await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions()
{
Prompt = MessageFactory.Text("Please enter some text. Type 'help' if you need some examples."),
});
}
private async Task<DialogTurnResult> EvaluationStep(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
await stepContext.Context.SendActivityAsync(MessageFactory.Text("You typed: " + stepContext.Result as string));
return await stepContext.EndDialogAsync(null, cancellationToken);
}
}
The ExampleDialog
public class ExampleDialog : ComponentDialog
{
public ExampleDialog(string dialogId) : base(dialogId)
{
AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[]
{
ExampleStep
}));
InitialDialogId = nameof(WaterfallDialog);
}
private async Task<DialogTurnResult> ExampleStep(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
await stepContext.Context.SendActivityAsync(MessageFactory.Text("Example: bla bla"));
return await stepContext.NextAsync(null, cancellationToken);
// ExampleDialog ends here
}
}
The problem is, that when the ExampleDialog ends after outputting the help text, the TextPrompt resumes and again prompts its message. This results in this conversation:
Bot: Hello world!
Bot: Please enter some text. Type ‘help’ if you need some examples.
User: help
Bot: Example: bla bla
Bot: Please enter some text. Type ‘help’ if you need some examples.
I don't want this last line to be reprompted by the bot. How can I fix this?
Thanks in advance
EDIT 1: A (not really satisfying) workaround
I've found a solution which does not really satisfy me. I've created my own TextPrompt class called MyTextPrompt and overwritten ResumeDialogAsync:
public class MyTextPrompt : TextPrompt
{
public MyTextPrompt(string id) : base(id)
{
}
public override async Task<DialogTurnResult> ResumeDialogAsync(DialogContext dc, DialogReason reason, object result = null, CancellationToken cancellationToken = default)
{
return Dialog.EndOfTurn;
}
}
In MainDialog I simply replaced TextPrompt with MyTextPrompt in the constructor
AddDialog(new MyTextPrompt(nameof(MyTextPrompt)));
and use the correct dialog id in the PromptStep
private async Task<DialogTurnResult> PromptStep(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
return await stepContext.PromptAsync(nameof(MyTextPrompt), new PromptOptions()
{
Prompt = MessageFactory.Text("Please enter some text. Type 'help' if you need some examples."),
});
}
The result is this conversation:
Bot: Hello world!
Bot: Please enter some text. Type ‘help’ if you need some examples.
User: help
Bot: Example: bla bla
/* bot now waits at this point of the conversation */
User: bla bla
Bot: You typed: bla bla
Ok, great. This is what I wanted, isn't it?
Yes it is, but there are some drawbacks:
This has to be done for every single type of prompt dialog.
If you've already overwritten the ResumeDialogAsync method in a custom prompt class, in some way you have to keep track what causes the call of ResumeDailogAsync.
How can this be solved in an elegant way?

I can think of three major possibilities for getting a prompt to not reprompt when it's resumed.
1. Nullify the prompt
The only way to actually stop the built-in prompts from reprompting when they're resumed is to set the Prompt property to null. I mentioned in the comments that there has been some question about whether resuming a prompt should count as retrying a prompt, and you happen to be in luck because resuming a prompt does not count as a retry. This means you can leave the RetryPrompt property intact so that it still automatically reprompts on invalid input like normal. All you have to take care of is making the initial prompt show up without a prompt property, and that's easy enough because you can just send a message that's worded like a prompt before you call the actual prompt.
2. Use middleware
Even if the prompt tries to reprompt, it does so through the turn context and so the activity it tries to send to the user will pass through the middleware pipeline. This means you can catch the activity in custom middleware and prevent it from actually getting sent to the user.
In order for your custom middleware to recognize the prompt activity as something it should catch, the activity will need some kind of tag in one of its properties that the middleware is programmed to look for. Activities have a lot of hidden metadata that you could use for something like this, like ChannelData or Entities or Value or Properties. You won't want the middleware to block the activity all the time because most of the time you'll actually want the prompt to be displayed, so there also needs to be a switch in the turn state that you can activate in the help dialog and that the middleware can check for.
In order for your custom middleware to actually stop the activity from going through, you can either short-circuit the pipeline or you can replace the activity with something that won't show up in the conversation.
3. Create a wrapper class
This possibility is based on the idea you already came up with, but instead of deriving from every existing prompt you can just make one class that can wrap any prompt. I won't make the whole implementation for you but the class signature might look something like this:
class NonResumingPrompt<TPrompt, TValue> : Prompt<TValue> where TPrompt : Prompt<TValue>
You could instantiate NonResumingPrompt like this for example:
new NonResumingPrompt<TextPrompt, string>()
Then the code could internally create an instance of TPrompt and define NonResumingPrompt.OnPromptAsync, NonResumingPrompt.OnRecognizeAsync, and NonResumingPrompt.OnPreBubbleEventAsync to call the corresponding methods in the wrapped TPrompt instance.
This would solve the problem of having to make your own version of every prompt, but it would require you to override a few more methods than in your solution. I should mention that making your own version of each prompt is not so different from what happened when the adaptive dialogs library was made. New "adaptive" versions (called inputs) of each prompt were made, and a lot of code ended up getting duplicated unfortunately. You can feel free to try out adaptive dialogs if you like.

Related

Propagate states to dialogs in MS BotFramework

In Microsoft BotFramework v4 you normally propagate the states (UserState, ConversationState, PrivateConversationState) to a dialog by passing them as parameters to its constructor.
This way:
Startup.cs
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// ...
IStorage storage = new MemoryStorage(); // For testing only !
services.AddSingleton(new UserState(storage));
services.AddSingleton(new ConversationState(storage));
// ...
services.AddSingleton<IBotFrameworkHttpAdapter, AdapterWithErrorHandler>();
services.AddSingleton<GoviiBaseDialog>(x => new RootDialog(
x.GetRequiredService<UserState>(),
x.GetRequiredService<ConversationState>()
);
services.AddTransient<IBot, Bot<RootDialog>>();
}
}
Bot.cs
public class Bot<T> : ActivityHandler where T : Dialog
{
T _dialog;
BotState _userState, _conversationState;
public Bot(T dialog, UserState userState, ConversationState conversationState,)
{
_userState = userState;
_conversationState = conversationState;
_dialog = dialog;
}
public override async Task OnTurnAsync(ITurnContext context, CancellationToken cancellationToken = default)
{
await base.OnTurnAsync(context, cancellationToken);
await _userState.SaveChangesAsync(context);
await _conversationState.SaveChangesAsync(context);
}
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> context, CancellationToken cancellationToken)
{
await _dialog.RunAsync(context, _conversationState.CreateProperty<DialogState>(nameof(DialogState)), cancellationToken);
}
}
RootDialog.cs
public class RootDialog : ComponentDialog
{
UserState _userState;
ConversationState _conversationState;
public RootDialog(UserState uState, ConversationState cState) : base("id")
{
_userState = uState;
_conversationState = cState;
// Add some dialogs and pass states as parameters
AddDialog(new CustomDialog_1(uState, cState));
AddDialog(new CustomDialog_2(uState, cState));
// ...
AddDialog(new CustomDialog_N(uState, cState));
}
}
Now let's assume that those CustomDialogs again uses some other CustomDialogs which needs to access the state. The states have to be passed again and again as parameters to the constructors.
The question is: Is there another way to access the states to avoid passing them again and again as parameters?
How you access state in a dialog will depend on the scope of the state.
If the state is scoped to the dialog then you should be using dialog state. More specifically, you should be using the state property of the dialog's associated dialog instance. There's some discussion of that in a recent answer here: Dialogs keep their variable values in new conversation in MS BotFramework v4
(Read about dialog instances here). Anything you want your dialog to keep track of, you should put in the associated dialog instance's state object, and the best place to see examples of how to do that is in the SDK source code itself. For example, you can see how a waterfall dialog keeps track of things like its custom values and what step it's on:
// Update persisted step index
var state = dc.ActiveDialog.State;
state[StepIndex] = index;
If the state has a greater scope than one instance of one dialog, you can pass the bot state objects to your dialogs like you've been doing. This can potentially be made easier if you put the dialogs in dependency injection so that your bot state can be automatically injected into their constructors. If your dialogs access state properties that are also used outside of the dialogs then it makes sense to give the dialogs a state property accessor instead of the state itself, reducing redundancy and separating concerns.
If you want to make sure your bot state is accessible anywhere you have a turn context, there's actually a built-in way to automatically add your bot state to turn state every turn. This is with the UseBotState extension method:
adapter.UseBotState(userState, conversationState);
You can then retrieve the state like this:
var userState = turnContext.TurnState.Get<UserState>();
var conversationState = turnContext.TurnState.Get<ConversationState>();

How to read state property accessors outside the dialog in V4 Bot Framework

I'm using bot framework version 4. I would like to access user state properties in the validator method but I didn't find any solution to it.
GitHub
In the GitHub sample above, we have a validator AgePromptValidatorAsync which validates age.
But I would want to access Name which I have stored in State property.
How could that be achieved.
And is it possible to access state/use GetAsync in a method outside dialog which doesn't contain context.
#mdrichardson could you please help me in this.Thank you in advance.
1. Ensure that UserProfile.Name is saved before hitting validation.
That sample doesn't do this on it's own, so you would:
private async Task<DialogTurnResult> NameConfirmStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
stepContext.Values["name"] = (string)stepContext.Result;
// ADDED: This code block saves the Name
if (!string.IsNullOrEmpty((string)stepContext.Result)) {
var userProfile = await _userProfileAccessor.GetAsync(stepContext.Context, () => new UserProfile(), cancellationToken);
userProfile.Name = (string)stepContext.Result;
await _userProfileAccessor.SetAsync(stepContext.Context, userProfile);
}
// We can send messages to the user at any point in the WaterfallStep.
await stepContext.Context.SendActivityAsync(MessageFactory.Text($"Thanks {stepContext.Result}."), cancellationToken);
// WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog.
return await stepContext.PromptAsync(nameof(ConfirmPrompt), new PromptOptions { Prompt = MessageFactory.Text("Would you like to give your age?") }, cancellationToken);
}
2. Access the User Profile
// CHANGED: Since this accesses the userProfile, the method is no longer static. Also must be async
private async Task<bool> AgePromptValidatorAsync(PromptValidatorContext<int> promptContext, CancellationToken cancellationToken)
{
// ADDED: Here is how you can access the Name
// Note: You can use promptContext.Context instead of stepContext.Context since they're both ITurnContext
var userProfile = await _userProfileAccessor.GetAsync(promptContext.Context, () => new UserProfile(), cancellationToken);
var name = userProfile.Name;
// Do whatever you want with the Name
// CHANGED: Since this is now async, we don't return Task.FromResult(). We just return the result
return promptContext.Recognized.Succeeded && promptContext.Recognized.Value > 0 && promptContext.Recognized.Value < 150;
}
Accessing the UserProfile Without Context
This is kind of possible, but you can't do this easily or out-of-the-box. There are some options that you can use, however (mostly in order from least difficult to most):
Pass the context to whatever method/function you need to use it in. Just about every bot method you'd use has some kind of context that you can pass into another method. This is definitely your best option.
Create a separate class that you use to store variables in bot memory
Either Write Directly to Storage or Implement Custom Storage that you use to track the UserProfile. Note, you'd have to pass around your Storage object, so you may as well just pass around the context, instead.
Use the new Adaptive Dialogs, since they do state management differently. I highly recommend against this, though, as these are "experimental", meaning that there's still bugs and we barely use this internally. I'm adding this as an option more for posterity and users that want to play with new stuff.

Wait for Even type Activity in a waterfallstep dialog (bot framework 4.0)

It's possible to wait and receive an Event type activity in a waterfall step dialog. I use directline 3.0 and inside a dialog flow I send an event from the bot to the client. After i would like to send an event from the client to the bot as an answer to previous send. If i use prompt await dc.Prompt("waitEvent",activity) where waitEvent is a textprompt and i answer with a message it works fine but I would like to answer to an event with an event. I was thinking that i could write a custom prompt but i didn't find documentation and obviously I could manage the conversation flow but I prefer use Dialogs where possible
You can use the ActivityPrompt abstract class to build an "EventActivityPrompt" class.
There aren't any BotFramework samples of this usage yet, but there are new tests written by the BotFramework team that you can use as an example.
To create your own EventActivityPrompt, you just need to implement the ActivityPrompt like so:
public class EventActivityPrompt : ActivityPrompt
{
public EventActivityPrompt(string dialogId, PromptValidator<Activity> validator)
: base(dialogId, validator)
{
}
}
The core difference between an ActivityPrompt and other Prompts (besides its abstract status) is that ActivityPrompts require a PromptValidator<Activity>, in order to validate the user input.
The next step is to create your validator. Here is the example:
async Task<bool> _validator(PromptValidatorContext<Activity> promptContext, CancellationToken cancellationToken)
{
var activity = promptContext.Recognized.Value;
if (activity.Type == ActivityTypes.Event)
{
if ((int)activity.Value == 2)
{
promptContext.Recognized.Value = MessageFactory.Text(activity.Value.ToString());
return true;
}
}
else
{
await promptContext.Context.SendActivityAsync("Please send an 'event'-type Activity with a value of 2.");
}
return false;
}

How to remove tooManyAttempts message in Prompt.Choice? How to accept text in Prompt.Choice that is not in list of options? C#

I'm using bot-Framework SDK3 C#.
I want to allow user input anything which is not in "PromptDialog.Choice"'s options. Any better ways to recommend?
This is my code.
private async Task SelectCategory(IDialogContext context)
{
List<string> options = new List<string>();
options = category.Keys.ToList();
options.Add("Category1");
options.Add("Category2");
options.Add("Category3");
PromptOptions<string> promptOptions = new PromptOptions<string>(
prompt: "which one do you prefer?",
tooManyAttempts: "",
options: options,
attempts: 0);
PromptDialog.Choice(context: context, resume: ResumeAfterSelectCategory, promptOptions: promptOptions);
await Task.FromResult<object>(null);
}
private async Task ResumeAfterSelectCategory(IDialogContext context, IAwaitable<string> result)
{
try
{
selected = await result;
}
catch (Exception)
{
// if the user's input is not in the select options, it will come here
}
}
But the problem is it always send the message "tooManyAttempts". If I set it to empty, I will send me "0".
I suppose you are using NodeJS. You can use the simple builder.Prompts.choice with maxRetries set to 0. Here is a sample snippet. It asks user to choose some option from a list, or they can enter something which is not in the list.
If you are using C# SDK, you can find some similar option for the list.
bot.dialog("optionalList", [
function(session){
builder.Prompts.choice(
session,
"Click any button or type something",
["option1", "option2", "option3"],
{maxRetries: 0} // setting maxRetries to zero causes no implicit checking
)
},
function(session, result){
// something from the list has been clicked
if(result.response && result.response.entity){
console.log(result.response.entity); // use the clicked button
} else {
console.log(session.message.text) // if user entered something which is not in the list
}
}
]);
EDIT 1:
Hi, Saw that you are using C# SDK. I am not that proficient with that but I can give you some suggestion.
The list which you generate in the async task SelectCategory you can generate in some other place, which is also accessible to the second async task ResumeAfterSelectCategory, (like making it a class variable or getting from database).
Now that the list is accessible in the 2nd task, you can compare what user has typed against the list to determine if the message is from the list or not.
If message is something from the list, then take action accordingly, otherwise user has entered something which is not in the list, and then take action accordingly.
Your 2nd problem is
And if user typed, it will show a message "you tried to many times"
What is meant by that? Does bot sends "you tried to many times" to the bot visitor. In which case it could be the behavior of library. You will be able to control that only if library provides some option. Else I don't know. Hope, that helps
EDIT 2:
I came across this SO question Can I add custom logic to a Bot Framework PromptDialog for handling invalid answers?
You can use that questions answer. Basically extending PromptDialog.PromptChoice<T>.Here is an example.
Override TryParse method like this
protected override bool TryParse(IMessageActivity message, out T result)
{
bool fromList = base.TryParse(message, out result);
if (fromList)
{
return true;
} else {
// do something here
return true; // signal that parsing was correct
}
}
I used node.js and to get message which user entered. use this code snippet.
(session, args) => {
builder.Prompts.text(session, "Please Enter your name.");
},
(session, args) => {
session.dialogData.username = args.response;
session.send(`Your user name is `${session.dialogData.username}`);
}

How to cancel a running async function in xamarin forms

I called async function in my code , which call rest service and populate a data structure. But somehow i need to cancel that function before its completion , how can i achieve this.
getAdDetails(ad.id,ad.campaign_type);
private async void getAdDetails(int campaign_id, string campaign_type) {
// some code here
}
There is something called "CancelationToken" which is supposed to be for such stuff.
Another way to do so is by throwing an exception when you want to cancel the process .
Another way is by having a flag which can be named "ShouldExecute" , and in the method you keep monitoring it.
I also tend to ignore the results which come from the method when they are not needed and let the thread executes in peace but yet ignored when it comes back.
Assuming you have some background logic in your function:
CancellationTokenSource _cancellation;
public void SomeFunctionToStartDataRefresh(){
_cancellation = new CancellationTokenSource();
try{
getAdDetails(id, type, cancellation.Token);
}catch(OperationCanceledException ex){
//Operation is cancelled
}
}
private async Task getAdDetails(ad.id,ad.campaign_type);
private async void getAdDetails(int campaign_id, string campaign_type, CancellationToken token) {
var data = await fetchDatafromServer()
token.ThrowIfCancellationRequested();
await DosomethingWithData();
token.ThrowIfCancellationRequested();
await DoSomethingElseWithData();
}

Resources