c# chat bot | RetryPrompt message dynamically - c#

c# chat bot : is there any way that we can control choice prompt's RetryPrompt message dynamically? I am using bot framework 4.0.

There's a couple of different ways to do this, depending on how I'm interpreting your question.
The easiest is to just add a separate RetryPrompt. For example, if we want to do this to the Multi-Turn-Prompt sample, we just add the RetryPrompt property:
private static async Task<DialogTurnResult> TransportStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
// WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog.
// Running a prompt here means the next WaterfallStep will be run when the users response is received.
return await stepContext.PromptAsync(nameof(ChoicePrompt),
new PromptOptions
{
Prompt = MessageFactory.Text("Please enter your mode of transport."),
Choices = ChoiceFactory.ToChoices(new List<string> { "Car", "Bus", "Bicycle" }),
RetryPrompt = MessageFactory.Text("That wasn't a valid option. Try again.")
}, cancellationToken);
}
This produces:
The other alternative would be to do something like what #pkr2000 said (although a little different), and use a custom validator to dynamically add the RetryPrompt. Something like:
AddDialog(new ChoicePrompt(nameof(ChoicePrompt), ValidateChoicesAsync));
[...]
private static Task<bool> ValidateChoicesAsync(PromptValidatorContext<FoundChoice> promptContext, CancellationToken cancellationToken)
{
if (!promptContext.Recognized.Succeeded)
{
promptContext.Options.RetryPrompt = MessageFactory.Text($"You said \"{ promptContext.Context.Activity.Text},\" which is invalid. Please try again.");
return Task.FromResult(false);
}
return Task.FromResult(true);
}
This produces:
You can do just about anything you want within the validator. Instead of using MessageFactory.Text(), you can pass in a completely different Activity like an Adaptive Card or something. You could also not set a RetryPrompt, instead changing the Prompt to whatever text/activity you want, return false, and then the user gets re-prompted with the new Prompt. It's really pretty limitless what you can do with a custom validator.

Related

How to create and run Dialog from Middleware with Microsoft BotFramework

Question
How can I conditionally create and run a Dialog from
middleware without breaking the bot?
Context
I'm using the dotnetcore/13.core-bot sample.
I have a setup to run a custom spellchecking Middleware. I am trying to create a dialog from Middleware so that after the user types some misspelled input and ONLY when two or more spellcheck suggestions are found, the user gets the possible sentence interpretations and chooses from a HeroCard or similar.
From my middleware SpellcheckMiddleware.cs, myDialog.RunAsync(...) runs a dialog, however, after the middleware exits onTurnAsync(), I get a fatal error: "An item with the same key has already been added". That error occurs when the bot tries to continue MainDialog from MainDialog.cs which is the dialog that was setup in Startup.cs.
Bot emulator visual
Error capture within Visual Studio
("An item with the same key has already been added")
---
My code
The only thing I have changed from the sample code is creating these two files, one defines the dialog that resolves a spellcheck with multiple suggestions, and one that is middleware that should run the spellcheck dialog.
SpellcheckMiddleware.cs:
public class SpellCheckMiddleware : IMiddleware
{
private readonly ConversationState conversationState;
public SpellCheckMiddleware(
IConfiguration configuration,
ConversationState conversationState)
{
this.conversationState = conversationState;
}
public async Task OnTurnAsync(
ITurnContext turnContext,
NextDelegate next,
CancellationToken cancellationToken = new CancellationToken())
{
# Fake suggestions
List<List<String>> suggestions = new List<List<String>>{
new List<String>{'Info', 'Olympics'},
new List<String>{'Info', 'Olympia'},
};
SpellcheckSuggestionsDialog myDialog = new SpellcheckSuggestionsDialog(suggestions);
await myDialog.RunAsync(
turnContext,
conversationState.CreateProperty<DialogState>(nameof(DialogState)),
cancellationToken);
await next(cancellationToken);
}
}
SpellcheckSuggestionsDialog.cs:
class SpellcheckSuggestionsDialog : ComponentDialog
{
// Create a prompt that uses the default choice recognizer which allows exact matching, or number matching.
public ChoicePrompt SpellcheckPrompt { get; set; }
public WaterfallDialog WaterfallDialog { get; set; }
List<string> Choices { get; set; }
internal SpellcheckSuggestionsDialog(
IEnumerable<IEnumerable<string>> correctedSentenceParts)
{
SpellcheckPrompt = new ChoicePrompt(
nameof(ChoicePrompt),
validator: null,
defaultLocale: null);
WaterfallDialog = new WaterfallDialog(
nameof(WaterfallDialog),
new WaterfallStep[]{
SpellingSuggestionsCartesianChoiceAsync,
EndSpellingDialogAsync
});
AddDialog(SpellcheckPrompt);
AddDialog(WaterfallDialog);
InitialDialogId = nameof(WaterfallDialog);
// Get all possible combinations of the elements in the list of list. Works as expected.
var possibleUtterances = correctedSentenceParts.CartesianProduct();
// Generate a choices array with the flattened list
Choices = new();
foreach (var item in possibleUtterances) {
System.Console.WriteLine(item.JoinStrings(" "));
Choices.Add(item.JoinStrings(" "));
}
}
private async Task<DialogTurnResult> SpellingSuggestionsCartesianChoiceAsync(
WaterfallStepContext stepContext,
CancellationToken cancellationToken)
{
return await stepContext.PromptAsync(
SpellcheckPrompt.Id,
new PromptOptions()
{
Choices = ChoiceFactory.ToChoices(Choices),
RetryPrompt = MessageFactory.Text("Did you mean...?"),
Prompt = MessageFactory.Text("Did you mean...?"),
Style = ListStyle.HeroCard
});
}
private async Task<DialogTurnResult> EndSpellingDialogAsync(
WaterfallStepContext stepContext,
CancellationToken cancellationToken)
{
// Overriding text sent using the choosen correction.
stepContext.Context.TurnState.Add("CorrectionChoice", stepContext.Result);
var choosen_correction = stepContext.Context.TurnState.Get<string>("CorrectionChoice");
stepContext.Context.Activity.Text = choosen_correction;
return await stepContext.EndDialogAsync(null, cancellationToken);
}
}
As of 2022, there is no sample from Microsoft showing dialogs being spawned from middleware, so that is likely not the intended way to use the framework. It may be possible, but then you're in a sense going against the framework, which is never advisable.
In order to have a dialog that provides spellcheck suggestions when the user types with a typo, I suggest you make that dialog part of logic specified in the WaterFall Dialog steps in MainDialog.cs.
AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[]
{
IntroStepAsync,
SpellcheckStep, //new step to force user to choose a spellcheck suggestions
ActStepAsync,
FinalStepAsync,
}));
This comes with the drawback that if you need to spellcheck multiple user inputs in the conversation, then you will need to add multiple spellcheck steps, each customized to handle the input expected at the matching point in the conversation steps.

Is it possible to get the current dialog step name?

I am writing a bot and I want to create a step by step help system. I setup a dictionary that allow each dialog to overwrite the help text for each step in it:
public abstract class BaseDialog : ComponentDialog
{
private static Dictionary<string, string> StepHelp = new Dictionary<string, string>();
protected static void AddStepHelp(string function, string text)
{
StepHelp.Add(function, text);
}
private async Task<DialogTurnResult> InterruptAsync(DialogContext innerDc, CancellationToken cancellationToken)
{
Activity helpMessage;
string curStepName = ""; //???
if (userText == "help" && StepHelp.ContainsKey(curStepName))
{
helpMessage = MessageFactory.Text(StepHelp[curStepName], StepHelp[curStepName], InputHints.ExpectingInput);
}
await innerDc.Context.SendActivityAsync(helpMessage, cancellationToken);
}
}
Then I add the text in the chidl dialog:
public class MyChildDialog: BaseDialog
{
static MyChildDialog()
{
AddStepHelp(nameof(Step1), "Help text for step1");
}
public MyChildDialog()
{
AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[]
{
Step1
}));
InitialDialogId = nameof(WaterfallDialog);
}
private async Task<DialogTurnResult> Step1(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
}
}
You can see in the InterruptAsync function above that I call the dictionary to get the help message in the base dialog, but how do I get the current step name?
You can get the current step index as well as the dialog name, so you should be able to create a unique identifier from that information (as long as you didn't name all the dialogs "waterfallDialog" from the example like I did!). I'm using node but I'm assuming getting the data from your stepContext is similar or the same.
The dialog name can be found at stepContext.stack[stepContext.stack.length-1].id.
The step index can be found at stepContext.stack[stepContext.stack.length-1].state.stepIndex.
I can't recall if you can end up with nested dialogs inside a waterfall dialog. I know your main/outer context will have the whole stack, but I think you'll always just have the one element inside your particular waterfall. That said, the current dialog should be the last one the stack, so accessing it as stepContext.stack.length-1 should work in either case. In the event the current dialog is at element 0, obviously you could just access it as such.
So long as your waterfall dialog names are unique, you'd end up with identifiers like waterfallDialog0, waterfallDialog1, etc. that you could then map in your dictionary to help texts.
It occurred to me you might be trying to access this from outside the waterfall dialog. In that case you should still be able to get that from your outer dialog context. You would likely have to use a recursive function to get it, something like
getInnermostActiveDialog(dc) {
var child = dc.child;
return child ? this.getInnermostActiveDialog(child) : dc.activeDialog;
}
where dc is your outer dialog context. I haven't gone this deep into things but you should then be able to get the same id and stepIndex values.
While it's easy to get the step index as billoverton explained, getting the step name is difficult. The _steps field is private and so is the method that returns the step name so you won't be able to access the steps even if your class derives from WaterfallDialog. The step names are only exposed through the telemetry client so you might consider writing a custom telemetry client that somehow exposes the information to your bot but at that point it's probably easier just to use reflection to access the private members.
Since you only really want the step name to use as a key for your dictionary, just using the step index instead is a much better option. You could use a Dictionary<int, string> but it makes sense to use a List<string> if you intend to have a help string for every step in the waterfall.

Botframework v4: How to simplify this waterfall dialog?

I have this code but but i think its over-complicated and can be simplified.
Also is there a way to go back to a spefici waterfall step if ever the user type "back" without restarting the whole dialog? I am new to this and it's hard to find a guide or online course on botframework v4 since it is new. Any help would be appreciated thanks!
public GetNameAndAgeDialog(string dialogId, IEnumerable<WaterfallStep> steps = null) : base(dialogId, steps)
{
var name = "";
var age = "";
AddStep(async (stepContext, cancellationToken) =>
{
return await stepContext.PromptAsync("textPrompt",
new PromptOptions
{
Prompt = stepContext.Context.Activity.CreateReply("What's your name?")
});
});
AddStep(async (stepContext, cancellationToken) =>
{
name = stepContext.Result.ToString();
return await stepContext.PromptAsync("numberPrompt",
new PromptOptions
{
Prompt = stepContext.Context.Activity.CreateReply($"Hi {name}, How old are you ?")
});
});
AddStep(async (stepContext, cancellationToken) =>
{
age= stepContext.Result.ToString();
return await stepContext.PromptAsync("confirmPrompt",
new PromptOptions
{
Prompt = stepContext.Context.Activity.CreateReply($"Got it you're {name}, age {age}. {Environment.NewLine}Is this correct?"),
Choices = new[] {new Choice {Value = "Yes"},
new Choice {Value = "No"},
}.ToList()
});
});
AddStep(async (stepContext, cancellationToken) =>
{
var result = (stepContext.Result as FoundChoice).Value;
if(result == "Yes" || result == "yes" || result == "Yeah" || result == "Correct" || result == "correct")
{
var state = await (stepContext.Context.TurnState["FPBotAccessors"] as FPBotAccessors).FPBotStateAccessor.GetAsync(stepContext.Context);
state.Name = name;
state.Age = int.Parse(age);
return await stepContext.BeginDialogAsync(MainDialog.Id, cancellationToken);
}
else
{
//restart the dialog
return await stepContext.ReplaceDialogAsync(GetNameAndAgeDialog.Id);
}
});
}
public static string Id => "GetNameAndAgeDialog";
public static GetNameAndAgeDialog Instance { get; } = new GetNameAndAgeDialog(Id);
}
And this is my accessors code:
public class FPBotAccessors
{
public FPBotAccessors(ConversationState conversationState)
{
ConversationState = conversationState ?? throw new ArgumentNullException(nameof(conversationState));
}
public static string FPBotAccessorName { get; } = $"{nameof(FPBotAccessors)}.FPBotState";
public IStatePropertyAccessor<FPBotState> FPBotStateAccessor { get; internal set; }
public static string DialogStateAccessorName { get; } = $"{nameof(FPBotAccessors)}.DialogState";
public IStatePropertyAccessor<DialogState> DialogStateAccessor { get; internal set; }
public ConversationState ConversationState { get; }
//
public static string ConversationFlowName { get; } = "ConversationFlow";
public IStatePropertyAccessor<ConversationFlow> ConversationFlowAccessor { get; set; }
}
So, there are a couple issues with your code and things you can do to make it better.
State within the dialog
First, let's start with the fact that you're closing over local variables in your constructor and accessing those from the closures that represent your steps. This "works" right now but is ultimately flawed. The initial flaw is different depending on the approach you've taking with instancing your GetNameAndAgeDialog dialog.
If you're using it as a singleton, that means all active conversations between users and your bot would be going through that one instance and you would have a concurrency issue where two users talking to the bot at the same time would be storing their values into the same memory (those variables) and stepping on each other's data.
It's also possible, depending on which samples you're following, that you're instead instantiating your GetNameAndAgeDialog on every turn. This would mean that those variables are then initialized to an empty string on every turn of the conversation and you'd lose track of the original values.
Ultimately, regardless of the instancing used, the approach ends up being flawed no matter what when it comes to scale out because at best your state would be pegged to a single server instance and if one turn of the conversation took place on ServerA and the next turn of the conversation took place on ServerM then ServerM would not have the values from the previous turn.
Alright, so clearly you need to store them with some kind of proper state management mechanism. You're clearly somewhat familiar with using BotState (be it conversation or user scope) already being that you're already using the state property accessors, but it's probably premature to store values you're collecting throughout a multi-turn prompt into someplace more permanent until you're at the end of the collection process. Luckily, dialogs themselves are stored into state, which you may have figured out when you set up a state property accessor for DialogState, and therefore offer a temporarily persistence mechanism that is tied to each dialog's lifetime on the dialog stack. Using this state is not obvious or documented well (yet), but WaterfallDialog actually goes a step further and exposes a first class Values collection via its WaterfallStepContext companion class which is fed into each step. This means that each step of your waterfall flow can add values into the Values collection and access values that previous steps may have put into there. There is a pretty good sample of this in the documentation page titled Create advanced conversation flow using branches and loops.
Not Making The Best Use of Prompts
You're using a TextPrompt for name which is perfect and you'll get a string from it and be all set. Though you might want to consider throwing a validator on there to make sure you get something that looks like a name instead of just allowing any old value.
You appear to be using a NumberPrompt<T> for the age (judging by the name "numberPrompt" at least), but then you .ToString() the step.Result and ultimately do an int.Parse in the final step. Using a NumberPrompt<int> would guarantee you get an int and you can/should just use that value as is rather than turning it back into a string and then parsing it yourself again later.
You've got a prompt named "confirmPrompt", but it does not appear to be an actual ConfirmPrompt because you're doing all the Choice work and positive value detection (e.g. checking for variations of "Yes") yourself. If you actually use a ConfirmPrompt it will do this all of this for you and its result will be a bool which you can then just easily test in your logic.
Minor stuff
Currently you're using stepContext.Context.Activity.CreateReply to create activities. This is fine, but long winded and unecessary. I would highly recommend just using the MessageFactory APIs.
I would always make sure to pass the CancellationToken through to all the XXXAsync APIs that take it... it's just good practice.
Your final step either restarts the GetNameAndAgeDialog if they don't confirm the details or starts the MainDialog if they do confirm the details. Restarting with ReplaceDialogAsync is awesome, that's the right way to do it! I just wanted to point out that by using BeginDialogAsync to start the MainDialog means that you're effectively leaving the GetNameAndAgeDialog at the bottom of the stack for the remainder of the conversation's lifetime. It's not a huge deal, but considering you'll likely never pop the stack back to there I would instead suggest using ReplaceDialogAsync for starting up the MainDialog as well.
Refactored Code
Here is the code rewritten using all of the advice above:
public GetNameAndAgeDialog(string dialogId, IEnumerable<WaterfallStep> steps = null) : base(dialogId, steps)
{
AddStep(async (stepContext, cancellationToken) =>
{
return await stepContext.PromptAsync("textPrompt",
new PromptOptions
{
Prompt = MessageFactory.Text("What's your name?"),
},
cancellationToken: cancellationToken);
});
AddStep(async (stepContext, cancellationToken) =>
{
var name = (string)stepContext.Result;
stepContext.Values["name"] = name;
return await stepContext.PromptAsync("numberPrompt",
new PromptOptions
{
Prompt = MessageFactory.Text($"Hi {name}, How old are you ?"),
},
cancellationToken: cancellationToken);
});
AddStep(async (stepContext, cancellationToken) =>
{
var age = (int)stepContext.Result;
stepContext.Values["age"] = age;
return await stepContext.PromptAsync("confirmPrompt",
new PromptOptions
{
Prompt = MessageFactory.Text($"Got it you're {name}, age {age}.{Environment.NewLine}Is this correct?"),
},
cancellationToken: cancellationToken);
});
AddStep(async (stepContext, cancellationToken) =>
{
var result = (bool)stepContext.Result;
if(result)
{
var state = await (stepContext.Context.TurnState["FPBotAccessors"] as FPBotAccessors).FPBotStateAccessor.GetAsync(stepContext.Context);
state.Name = stepContext.Values["name"] as string;
state.Age = stepContext.Values["age"] as int;
return await stepContext.ReplaceDialogAsync(MainDialog.Id, cancellationToken: cancellationToken);
}
else
{
//restart the dialog
return await stepContext.ReplaceDialogAsync(GetNameAndAgeDialog.Id, cancellationToken: cancellationToken);
}
});
}
Also is there a way to go back to a spefici waterfall step if ever the user type "back" without restarting the whole dialog?
No, there is not a way to do this today. The topic has come up in internal discussions with the team, but nothing has been decided yet. If you think this is a feature that would be useful, please submit an issue over on GitHub and we can see if it gains enough momentum to get the feature added.

Prompting a user for a string C# botframe work

I'm currently busy designing a bot, that receives a project name and returns the status of it, however I can't get the prompting for the name to work.
Currently this is the method i'm using to prompt the user
[LuisIntent("ProjectInfo")]
public async Task projectInfo(IDialogContext context, LuisResult result, IAwaitable<string> Userresult)
{
await context.PostAsync($"Enter your project name");
var Promt = await Userresult;
string projectName = Promt.ToString().ToLower();
if(projectName != null)
{
TestInfo MI = new TestInfo();
if(MI.FindProject(projectName.ToString()) == 0)
{
await context.PostAsync($"Project Found. What do you want to know ?");
}
else
{
await context.PostAsync($"Project Not Found.");
}
}
context.Wait(MessageReceived);
}
With this current code I'm receiving a Exception: ProjectInfo [File of type 'text/plain'].
I have tried using a prompt dialog but that didn't seem to work ether. My end goal for this is to loop and prompt the user for a new project name until "Project Found" is displayed.
I'm not sure if I'm going about this the right way, if not any suggestions are welcome.
Unfortunately I've not come across your version yet but I can give you an example of a different approach.
Usually I prompt for simple texts something like this:
PromptDialog.Text(context, AfterPromptMethod, "Prompt text", attempts: 100);
Signature of AfterPromptMethod:
async Task AfterPromptMethod(IDialogContext context, IAwaitable<string> userInput)
With this you could do your logic in the AfterPromptMethod and loop back to the prompt in messageReceived.

PromptChoice in bot framework displays 'too many attempts' exception

I need to prompt user to make a choice and keep the chosen value in a variable and use it at the end of conversation session.
var dialog = new PromptDialog.PromptChoice<string>(
new string[] {"A new request", "Current Request" },
"Which one would you like?",
"Sorry, that wans't a valid option", 1);
context.Call(dialog, ChoiceReceivedAsync);
context.Wait(this.MessageReceivedAsync);
}
private async Task ChoiceReceivedAsync(IDialogContext context, IAwaitable<object> result)
{
context.Wait(MessageReceivedAsync1);
return;
}
The above code displays choices but gives too manyattempts exception. Also I used the below code in MessageReceivedAsync1
var UserChose = await result;
but still result does not store the value.
Remove the context.Wait(this.MessageReceivedAsync); line that you have after the context.Call
context.Call is launching a new dialog (PromptChoice) and so you cannot do both (launch a new dialog and wait in the current dialog)

Categories

Resources