Bot Framework - Botstate between Dialogs loses values - c#

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");

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.

Custom State not loading data that is saved to data Store

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.

Bot Framework Context Wait not waiting for next message

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.

Multi-language bot using LUIS

I'm trying to create a multilanguage bot by detecting the language and selecting the proper set of LUIS keys and strings. My problem is, that my LuisDialog serializes itself and the MakeRoot method is not being called anymore.
My code (roughly):
public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
if (activity.Type == ActivityTypes.Message)
{
var languageData = DetectLanguage(activity); // here I have the keys, strings etc.
await Conversation.SendAsync(activity, () => new Dialogs.RootDialog(languageData));
}
else
{
HandleSystemMessage(activity);
}
var response = Request.CreateResponse(HttpStatusCode.OK);
return response;
}
I've tried with the intermediate dialog, which selects the language and context.Forward everything to the LuisDialog, but I'm struggling with managing that. If this is a good strategy, I can share more code. I'm also considering scoreables.
If your need is about switching the language of your LUIS Dialog at its start
You have to make a method to get each LUIS parameter by language and as you know the language with your DetectLanguage, choose the right ones.
Then pass them to your LuisDialog like the following:
public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
if (activity.Type == ActivityTypes.Message)
{
var languageData = DetectLanguage(activity); // here I have the keys, strings etc.
var luisService = new LuisService(new LuisModelAttribute("yourLuisAppIdGivenTheLanguageData", "yourLuisAppKeyGivenTheLanguageData", domain: "yourLuisDomainGivenTheLanguageData"));
await Conversation.SendAsync(activity, () => new Dialogs.RootDialog(luisService));
}
else
{
HandleSystemMessage(activity);
}
var response = Request.CreateResponse(HttpStatusCode.OK);
return response;
}
And your RootDialog shoud look like this:
public class RootDialog : LuisDialog<object>
{
public RootDialog(params ILuisService[] services) : base(services)
{
}
If you have a more complex project
We implemented a complex project allowing language switch at any time. Due to this possibility, activity's locale field could not be fully trusted even if you override it on the beginning of its processing.
The strategy is the following:
Detect user's language in MessagesController's Post method, for every incoming message
Store the user language in bot state (userData) when it's changing
Then:
When displaying a text, get the value in the right language using userData
When using a language specific tool like LUIS, get the right parameter using userData
You will need an intermediate RootDialog to handle this LUIS language switch, and you have to complete your LuisDialog after every detection (or to check the language before every MessageReceived on your LuisDialog).

Easy tables with Xamarin Forms - InvalidOperationException

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.

Categories

Resources