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)
Related
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.
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.
I'm trying to build a Dialog using the Microsoft Bot Framework which helps users consult purchase order status (currently, just a mock). I am using a LuisDialog which, when it detects the "ConsultPO" intent, it's supposed to ask for the user's 'customer id' and wait a follow up message from the user. However, it keeps going back to the start of the Luis Dialog and processing the intent instead of resuming from the waited method. This is the intent's code, which runs correctly:
[LuisIntent("ConsultPO")]
public async Task POIntent(IDialogContext context, LuisResult result)
{
string PO = "";
foreach (var entity in result.Entities)
{
if (entity.Type == "purchaseOrder")
PO = entity.Entity;
}
if (PO.Length != 0)
{
po_query = PO;
}
await context.PostAsync("Ok, can you confirm your customer id and I'll check for you?");
context.Wait(confirmCustomer_getPO);
}
This is the code I would expect to be executed after the user responds with a follow up message:
public async Task confirmCustomer_getPO(IDialogContext context, IAwaitable<object> argument)
{
await context.PostAsync("DEBUG TEST");
IMessageActivity activity = (IMessageActivity)await argument;
customer_query = activity.Text;
if (po_query.Length > 0)
{
PurchaseOrder po = POservice.findPO(po_query, customer_query);
await buildSendResponse(po, context);
//more non relevant code
When I answer to the bot's inquiry after context.Wait(confirmCustomer_getPO) is executed, it just goes into LUIS then runs the code respective to "None" intent. The message "DEBUG TEST" is never sent.
Why is "confirmCustomer_getPO" never getting called?
EDIT:
I added a debug message in the StartAsync method. I'm not sure whether this is supposed to happen but it pops up every time I send a message to the bot, which makes me believe the Dialog is simply restarting every time I message the bot:
public class EchoDialog : LuisDialog<object>
{
public EchoDialog() : base(new LuisService(new LuisModelAttribute(
ConfigurationManager.AppSettings["LuisAppId"],
ConfigurationManager.AppSettings["LuisAPIKey"],
domain: ConfigurationManager.AppSettings["LuisAPIHostName"])))
{
}
public override Task StartAsync(IDialogContext context)
{
context.PostAsync("I'm in startAsync");
return base.StartAsync(context);
}
Local debugging shows no exceptions are occurring and that any breakpoint in the waited method is never reached, although the context.Wait call does happen.
I figured out the issue myself after fighting with it for a while. The issue was with the bot store. I was using an InMemoryDataStore which was not working - switching to TableBotDataStore fixed the problem. The issue with the DataStore meant that states weren't being saved so my "waits" and "forwards" were not being saved into the dialog stack - any new incoming message was sent to the RootDialog.
Broken - not working while this was in global.asax.cs:
Conversation.UpdateContainer(
builder =>
{
builder.RegisterModule(new AzureModule(Assembly.GetExecutingAssembly()));
var store = new InMemoryDataStore(); // volatile in-memory store
builder.Register(c => store)
.Keyed<IBotDataStore<BotData>>(AzureModule.Key_DataStore)
.AsSelf()
.SingleInstance();
});
GlobalConfiguration.Configure(WebApiConfig.Register);
As soon as I updated store to:
var store = new TableBotDataStore(ConfigurationManager.AppSettings["AzureWebJobsStorage"]);
Having a valid "AzureWebJobsStorage" setting in web.config from my application settings in Azure, the problem was fixed without any other changes in the code.
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.
I have a subdialog in a bot built using MS bot framework that starts as follows - the standard way:
public async Task StartAsync(IDialogContext context)
{
var msg = "Let's find your flights! Tell me the flight number, city or airline.";
var reply = context.MakeMessage();
reply.Text = msg;
//add quick replies here
await context.PostAsync(reply);
context.Wait(UserInputReceived);
}
This dialog is called using two different ways, depending on whether in the previous screen the user tapped a button that says "Flights" or immediately entered a flight number. Here is the code from the parent dialog:
else if (response.Text == MainOptions[2]) //user tapped a button
{
context.Call(new FlightsDialog(), ChildDialogComplete);
}
else //user entered some text instead of tapping a button
{
await context.Forward(new FlightsDialog(), ChildDialogComplete,
activity, CancellationToken.None);
}
Question: how can I know (from within the FlightsDialog) whether that dialog was called using context.Call() or context.Forward()? This is because in the case of context.Forward(), StartAsync() shouldn't output the prompt asking the user to enter the flight number - they already did this.
The best idea I have is to save a flag in the ConversationData or user data, as below, and access it from the IDialog, but I thought there could be a better way?
public static void SetUserDataProperty(Activity activity, string PropertyName, string ValueToSet)
{
StateClient client = activity.GetStateClient();
BotData userData = client.BotState.GetUserData(activity.ChannelId, activity.From.Id);
userData.SetProperty<string>(PropertyName, ValueToSet);
client.BotState.SetUserDataAsync(activity.ChannelId, activity.From.Id, userData);
}
Unfortunately Forward actually calls Call (and then does some other stuff afterwards), so your Dialog wouldn't be able to differentiate.
void IDialogStack.Call<R>(IDialog<R> child, ResumeAfter<R> resume)
{
var callRest = ToRest(child.StartAsync);
if (resume != null)
{
var doneRest = ToRest(resume);
this.wait = this.fiber.Call<DialogTask, object, R>(callRest, null, doneRest);
}
else
{
this.wait = this.fiber.Call<DialogTask, object>(callRest, null);
}
}
async Task IDialogStack.Forward<R, T>(IDialog<R> child, ResumeAfter<R> resume, T item, CancellationToken token)
{
IDialogStack stack = this;
stack.Call(child, resume);
await stack.PollAsync(token);
IPostToBot postToBot = this;
await postToBot.PostAsync(item, token);
}
From https://github.com/Microsoft/BotBuilder/blob/10893730134135dd4af4250277de8e1b980f81c9/CSharp/Library/Dialogs/DialogTask.cs#L196