I am busy writing a bot that interacts with both anonymous and authenticated users. It stores user data in a custom object and persists that object to the UserState Store.
When the bot starts and the user joins the conversation it creates the custom object and the IStatePropertyAccessor for the custom object. the bot then determines if this is a authenticated or anonymous user. If its authenticated, it loads the required information from the backend system and we are able to use this data in all dialogs without issue.
If it is an anonymous user we direct them to a basic dialog that gets their name, phone number, and email. The last step in this dialog is to pull the above custom object and update it with the information collected so we can attach it when saving requests to the backend system.
The problem is that the updated information is saved to the store (I am able to view the raw data in the cosmosDB), but when getting the custom object from the store in other dialogs it always returns an empty object. If I trigger the onboarding dialog again, it pulls the correclty populated custom object fine.
Why is that this one dialog can see the data it saved to the store but other dialogs see it as an empty object?
Below is my code for the final step in the onboarding WaterfallStep dialog:
public async Task<DialogTurnResult> ProcessOnBoardingAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
_form = await _accessor.GetAsync(stepContext.Context);
_form.Firstname = (string)stepContext.Result;
UserProfile profile = await _userAccessor.GetAsync(stepContext.Context);
profile.FullName = String.Format("{0} {1}", _form.Firstname, _form.Lastname);
await _userAccessor.SetAsync(stepContext.Context, profile);
MainResponses view = new MainResponses();
await view.ReplyWith(stepContext.Context, MainResponses.Menu);
return await stepContext.EndDialogAsync();
}
After this step, the raw data is correct, the Fullname is set correctly. It can be viewed in the raw data stored in CosmosDB.
the next dialog's constructor is as follows and the IStatePropertyAccessor<UserProfile> userAccessor that is passed in to this constructor is the same one passed into the Onboarding Dialog constructor:
public LeadDialog(BotServices botServices, IStatePropertyAccessor<LeadForm> accessor, IStatePropertyAccessor<UserProfile> userAccessor)
: base(botServices, nameof(LeadDialog))
{
_accessor = accessor;
_userAccessor = userAccessor;
InitialDialogId = nameof(LeadDialog);
var lead = new WaterfallStep[]
{
LeadPromptForTitleAsync,
LeadPromptForDescriptionAcync,
LeadProcessFormAsync
};
AddDialog(new WaterfallDialog(InitialDialogId, lead));
AddDialog(new TextPrompt("LeadTopic"));
AddDialog(new TextPrompt("LeadDescription"));
}
and the code that is trying to use the accessor is:
public async Task<DialogTurnResult> LeadProcessFormAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
_form = await _accessor.GetAsync(stepContext.Context);
_form.Description = (string)stepContext.Result;
await _responder.ReplyWith(stepContext.Context, LeadResponses.LeadFinish);
await _responder.ReplyWith(stepContext.Context, LeadResponses.LeadSummary, new { _form.Topic, _form.Description });
UserProfile profile = await _userAccessor.GetAsync(stepContext.Context);
var LeadDetail = new CRMLead
{
ks_chatid = profile.Chatid,
parentcontactid =profile.ContactId,
topic = _form.Topic,
description = _form.Description
};
}
In this last bit of code, the returned UserProfile is an empty object with default values, but would have expected to at minimum pulled the Fullname that is correctly stored in the CosmosDB.
It turns out that a fellow developer had made the set properties on the User Profile class as internal so the accessor could not set the properties when reading the store.
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.
I have a bot written in C# that is using LUIS to determine intents. I have a method that makes a call to the LUIS service and then looks for an 'Open_Case' intent. The model has a CaseNumber entity defined which may or may not be included in the response from the LUIS service.
If the response doesn't have a case number entity I start a dialog to ask the user for the case number.
Once I have a case number I then want to return a card with case information.
Here's the code I have:-
/// <summary>
/// Dispatches the turn to the requested LUIS model.
/// </summary>
private async Task DispatchToLuisModelAsync(ITurnContext context, string appName, DialogContext dc, CancellationToken cancellationToken =
default (CancellationToken)) {
var result = await botServices.LuisServices[appName].RecognizeAsync(context, cancellationToken);
var intent = result.Intents ? .FirstOrDefault();
string caseNumber = null;
if (intent ? .Key == "Open_Case") {
if (!result.Entities.ContainsKey("Case_CaseNumber")) {
var dialogResult = await dc.BeginDialogAsync(CaseNumberDialogId, null, cancellationToken);
} else {
caseNumber = (string)((Newtonsoft.Json.Linq.JValue) result.Entities["Case_CaseNumber"].First).Value;
var cardAttachment = botServices.CaseInfoServices.LookupCase(caseNumber);
var reply = context.Activity.CreateReply();
reply.Attachments = new List < Attachment > () {
cardAttachment
};
await context.SendActivityAsync(reply, cancellationToken);
}
}
}
What I'm struggling with is where the code send the card response should sit.
In the code I currently have I send the card if the number was returned in the LUIS response, but if there was no number and I start the dialog then I only get access to the number either in the final step of the dialog or in the dialog result in the root turn handler. I've currently duplicated the reply inside the final step in the dialog, but it feels wrong and inelegant.
I'm sure there must be a way that I can collect the number from LUIS or the dialog and THEN send the response from a single place instead of duplicating code.
Any suggestions gratefully received...
I came to the conclusion that I need to put the code that displays the card into a method on the bot class, then call it from the else in code snippet and also from the turn handler when the dialogTurnStatus is equal to Complete
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.
I am using this tutorial in order to connect a xamarin.forms app with easy tables. I cannot add data to the database in Azure as i get
System.InvalidOperationException
The error message is the following
An insert operation on the item is already in the queue.
The exception happends in the following line of code.
await usersTable.InsertAsync(data);
In order to add a user
var user = new User { Username = "username", Password = "password" };
bool x = await AddUser(user);
AddUser
public async Task<bool> AddUser(User user)
{
try
{
await usersTable.InsertAsync(user);
await SyncUsers();
return true;
}
catch (Exception x)
{
await new MessageDialog(x.Message.ToString()).ShowAsync();
return false;
}
}
SyncUsers()
public async Task SyncUsers()
{
await usersTable.PullAsync("users", usersTable.CreateQuery());
await client.SyncContext.PushAsync();
}
where
IMobileServiceSyncTable<User> usersTable;
MobileServiceClient client = new MobileServiceClient("url");
Initialize
var path = Path.Combine(MobileServiceClient.DefaultDatabasePath, "DBNAME.db");
var store = new MobileServiceSQLiteStore(path);
store.DefineTable<User>();
await client.SyncContext.InitializeAsync(store, new MobileServiceSyncHandler());
usersTable = client.GetSyncTable<User>();
Please check your table. You probably have added the item already. Also, I would suggest that you don't set the Id property for your entity, because you might be inserting a same ID that's already existing in your table. It's probably the reason why the exception is appearing.
Hope it helps!
Some debugging you can do:
1) Turn on diagnostic logging in the backend and debug the backend: https://adrianhall.github.io/develop-mobile-apps-with-csharp-and-azure/chapter8/developing/#debugging-your-cloud-mobile-backend
2) Add a logging delegating handler in your MobileServiceClient setup: https://adrianhall.github.io/develop-mobile-apps-with-csharp-and-azure/chapter3/server/#turning-on-diagnostic-logs
The MobileServicePushFailedException contains an inner exception that contains the actual error. Normally, it is one of the 409/412 HTTP errors, which indicates a conflict. However, it can also be a 404 (which means there is a mismatch between what your client is asking for and the table name in Easy Tables) or 500 (which means the server crashed, in which case the server-side diagnostic logs indicate why).
Easy Tables is just a Node.js service underneath the covers.
I'm trying to pass state between different dialogs, and seem to be either a) not calling dialogs correctly or b) not using botstate correctly (or both).
Can anyone tell me what I am losing when I open the second dialog? Its opened using context.forward();
In messagescontroller I am easily able to set state;
StateClient stateClient = activity.GetStateClient();
BotData userData = await stateClient.BotState.GetUserDataAsync(activity.ChannelId, activity.From.Id);
userData.SetProperty<bool>("SessionActive", true);
await stateClient.BotState.SetUserDataAsync(activity.ChannelId, activity.From.Id, userData);
I then open a dialog which controls access to other dialogs;
await Conversation.SendAsync(activity, () => new DialogController());
This is a separate class;
public class DialogController : IDialog<object>
Within this dialog I can access the value I set as 'true' - this works;
StateClient stateClient = new StateClient(new Uri(message.ServiceUrl));
BotData userData = await stateClient.BotState.GetUserDataAsync(message.ChannelId, message.From.Id);
userData.GetProperty<bool>("SessionActive")
However, within this dialog i then go on to open a second dialog dependant on state;
await context.Forward(new SubDialog(), ThrowOutTask, message, cts.Token);
This is also a separate class;
public class SubDialog: IDialog<object>
However, when I try to retrieve the 'SessionActive' state, in exactly the same way as before, the value is false (i.e. its instantiated for the first time)..?
When you are requesting the state from an Dialog class, you should do it using the context.
bool sessionActive = context.UserData.Get<bool>("SessionActive");