Microsoft Bot Framework - Multi turn context is lost after the first interaction - frameworks

I recently moved my code from SDK v3 to v4 and I am trying to take advantage of the multi-turn features.
I have looked over the samples from GitHub. The samples work well for multi-turn but one issue I noticed is that it recognizes the context only if the prompt is clicked immediately after the initial answer (with prompts) is shown.
I would like to be able to identify, at any given time that a prompt is clicked. I am storing all the previous prompts in the state object (dialogInstance.State) already. I have a custom host, which sends the replytoid and using that I can get the appropriate state.
The problem is though, I am not able to get to a point where I can use the dialoginstance.State.
The sample code uses the DialogExtensions class. The "DialogExtensions" class tries to gather the previous context by checking if the result from the ContinueDialogAsync method returns null or not.
DialogExtensions class with multi-turn
When there is no previous context (no previous answer with prompts), then the call to the ContinueDialogAsync returns a result with Empty Status.
I am thinking this where I need to check the dialogstate and if the new message refers to any of the old messages at any given point, it can then start to continue the old conversation.
I am not sure if that is even possible.
Any help/pointers would be appreciated.
thanks,

I eventually ended up implementing something that will work for custom bot host/direct channel bot client.
The whole point is that the call to the qnamaker api should happen with the old context object, whenever an option is chosen, even if it is out-of-context.
First let me explain how it works in the current version of the code.
The way the bot code was trying to solve multi turn dialog, was by storing the current answer if it had prompts/options and returning the conversation state in "waiting" mode. When the next question is received, it would automatically assume that the new question is part of the prompts/options of the old question. It would then pass the oldstate along to the QnAMaker.
What I noticed is that, even if the question in the second turn is not part of the prompts/options (something the user has typed manually and is a completely different question), it would still send the oldstate object to the QnAMaker.
The QnAMaker api call seem to ignore oldstate if the new question is not part of the prompts/options of the oldstate. It will work correctly by fetching the answer for the new question that was typed manually.
This was the key. If we can focus on what gets to the qnamaker then we can solve our original problem.
I realized that having the bot return a waiting state is only a mechanism to create a condition to extract the oldstate in the next turn. However, if I can rebuild the oldstate anytime when there is an option chosen, then the call to the qnamaker would work equally well.
This is what I have done now.
In my custom bot host code (which is a direct line client), I am sending the ReplyToID field populated with the original question whenever a prompt is clicked. Then in the bot code, I have changed it so that if there is a replytoid present, then build a new oldstate object with the data from the reply to id. Below is the QnABotState class that represents the oldstate. its a very simple class containing previous qna question id and the question text.
public int PreviousQnaId { get; set; }
public string PreviousUserQuery { get; set; }
QnABoState class
Now, the problem was the Activity object contains ReplyToId but does not contain ReplyToQuery (or something like that). Activity object is used to send data from bot client to the bot. So, either I would have to use a different field or send the PreviousUserQuery as an empty string. I had a hunch that it would work with just the previousqnaid.
//starting the process to get the old context (create an object that will hold the Process function's current state from the dialog state)
//if there is replyToId field is present, then it is a direct channel request who is replying to an old context
//get the reply to id from summary field
var curReplyToId = "";
curReplyToId = dialogContext.Context.Activity.ReplyToId;
var curReplyToQuery = "";
var oldState = GetPersistedState(dialogContext.ActiveDialog);
//if oldstate is null also check if there is replytoid populated, if it is then it maybe a new conversation but it is actually an "out of turn option" selection.
if (oldState == null)
{
if (!string.IsNullOrEmpty(curReplyToId))
{
//curReplyToId is not empty. this is an option that was selected out-of-context
int prevQnaId = -1;
int.TryParse(curReplyToId, out prevQnaId);
oldState = new QnABotState() { PreviousQnaId = prevQnaId, PreviousUserQuery = curReplyToQuery };
}
}
With that in place, my call to the qnamaker api would receive an oldstate object even if it is called out-of-context.
I tried the code and it worked. Not having the previous qna query did not make a difference. It worked with just the PreviousQnaId field being populated.
However, please note, this will not work for other channels. It would work for channels where you can set the ReplyToId field, such as the Direct Channel Client.
here is the code from my bot host:
// to create a new message
Activity userMessage = new Activity
{
From = new ChannelAccount(User.Identity.Name),
Text = questionToBot,
Type = ActivityTypes.Message,
Value = paramChatCode,// + "|" + "ShahID-" + DateTime.Now.TimeOfDay,
Id = "ShahID-" + DateTime.Now.TimeOfDay,
ChannelData = botHostId//this will be added as the bot host identifier
};
//userMessage.Type = "imBack";
if (paramPreviousChatId > 0)
{
//we have a valid replytoid (as a part of dialog)
userMessage.ReplyToId = paramPreviousChatId.ToString();
}

Related

Cannot pass data across intents in Dialogflow Fulfillment

In Dialogflow Fulfillment I simply want to pass data from the Welcome Intent to the help Intent using conv.user.storage as seen in the code below. I can add it in the welcome intent but when I try to retrieve it in the help intent it is always undefined meaning data is NOT passed to the help intent. I have spent several hours on something I thought was straight forward and played around without any success. I would really appreciate a real world example on how to fix it and understand what I'm doing wrong.
function welcome(agent) {
agent.add(request.body.queryResult.fulfillmentMessages[0].text.text[0]);
var entity = 'media_getreq?message=volume';
getData(entity).then(result => {
let conv = agent.conv();
conv.user.storage["devicedata"] = result;
console.log(conv.user.storage["devicedata"]); //WORKS
});
}
function help(agent) {
agent.add(request.body.queryResult.fulfillmentMessages[0].text.text[0]);
let conv = agent.conv();
console.log(conv.user.storage["devicedata"]); //ALWAYS EMPTY
}
You have missed out Contexts a critical componet required to link intents.
Contexts represent the current state of a user's
request and allow your agent to transport conversation information from one intent to another.
You can use combinations of input and output contexts to control the conversational path the user traverses through your dialog sequence.
In summary, The intent that collects your Welcome input uses an output context to "remember" what you said.
The same context is used as the input context to the next intent, which collects input into the HelpIntent.
You need to update your code accordingly. Please see for details:
https://dialogflow.com/docs/contexts
https://dialogflow.com/docs/contexts/contexts-api

Remove submitted state in submission handler

I am trying to reset a form so that it appears to Drupal 8 that it hasn't been submitted. So far I have been unable to do this, as I cannot find any available methods (setSubmitted() hardcodes it to TRUE without a FALSE option). The reason is this isn't a full submit, but a submit of one field after which I would like the user to be redirected to another page that has another form, and I would like this secondary form to use the value obtained in the first step.
In the submit handler for the first part I use this to redirect:
$form_state->setRedirect('my.route', [], []);
And this works, but when the form reaches the second form (it seems) that the second form thinks it is a submission. As a result any submit buttons I add to the second form seem to make it auto-submit, and this breaks my user journey.
In the submit for the first part I have tried:
$form_state->setRebuild(TRUE);
$form_state = new FormState();
unset($form_state);
Tried the above in various configurations to no avail. They all prevent/ignore the setRedirect call that I make afterwards. The reason I want/need to do it this way is I want to preserve the POST method used.
Do you want to obtain something similar to what core search module does? It has simple SearchBlockForm that sends data to more complex SearchPageForm.
SearchBlockForm uses GET method (though you may use POST):
$form['#method'] = 'get';
and has no id and token fields:
function search_form_search_block_form_alter(&$form, FormStateInterface $form_state) {
$form['form_build_id']['#access'] = FALSE;
$form['form_token']['#access'] = FALSE;
$form['form_id']['#access'] = FALSE;
}
BTW, the last change allows you to avoid running submit callbacks.
Hope this helps.

CQ5 / AEM5.6 Workflow: Access workflow instance properties from inside OR Split

TL;DR version:
In CQ workflows, is there a difference between what's available to the OR Split compared to the Process Step?
Is it possible to access the /history/ nodes of a workflow instance from within an OR Split?
How?!
The whole story:
I'm working on a workflow in CQ5 / AEM5.6.
In this workflow I have a custom dialog, which stores a couple of properties on the workflow instance.
The path to the property I'm having trouble with is: /workflow/instances/[this instance]/history/[workItem id]/workItem/metaData and I've called the property "reject-or-approve".
The dialog sets the property fine (via a dropdown that lets you set it to "reject" or "approve"), and I can access other properties on this node via a process step (in ecma script) using:
var actionReason;
var history = workflowSession.getHistory(workItem.getWorkflow());
// loop backwards through workItems
// and as soon as we find a Action Reason that is not empty
// store that as 'actionReason' and break.
for (var index = history.size() - 1; index >= 0; index--) {
var previous = history.get(index);
var tempActionReason = previous.getWorkItem().getMetaDataMap().get('action-message');
if ((tempActionReason != '')&&(tempActionReason != null)) {
actionReason = tempActionReason;
break;
}
}
The process step is not the problem though. Where I'm having trouble is when I try to do the same thing from inside an OR Split.
When I try the same workflowSession.getHistory(workItem.getWorkflow()) in an OR Split, it throws an error saying workItem is not defined.
I've tried storing this property on the payload instead (i.e. storing it under the page's jcr:content), and in that case the property does seem to be available to the OR Split, but my problems with that are:
This reject-or-approve property is only relevant to the current workflow instance, so storing it on the page's jcr:content doesn't really make sense. jcr:content properties will persist after the workflow is closed, and will be accessible to future workflow instances. I could work around this (i.e. don't let workflows do anything based on the property unless I'm sure this instance has written to the property already), but this doesn't feel right and is probably error-prone.
For some reason, when running through the custom dialog in my workflow, only the Admin user group seems to be able to write to the jcr:content property. When I use the dialog as any other user group (which I need to do for this workflow design), the dialog looks as though it's working, but never actually writes to the jcr:content property.
So for a couple of different reasons I'd rather keep this property local to the workflow instance instead of storing it on the page's jcr:content -- however, if anyone can think of a reason why my dialog isn't setting the property on the jcr:content when I use any group other than admin, that would give me a workaround even if it's not exactly the solution I'm looking for.
Thanks in advance if anyone can help! I know this is kind of obscure, but I've been stuck on it for ages.
a couple of days ago i ran into the same issue. The issue here is that you don't have the workItem object, because you don't really have an existing workItem. Imagine the following: you are going through the workflow, you got a couple of workItems, with means, either process step, either inbox item. When you are in an or split, you don't have existing workItems, you can ensure by visiting the /workItems node of the workflow instance. Your workaround seems to be the only way to go through this "issue".
I've solved it. It's not all that elegant looking, but it seems to be a pretty solid solution.
Here's some background:
Dialogs seem to reliably let you store properties either on:
the payload's jcr:content node (which wasn't practical for me, because the payload is locked during the workflow, and doesn't let non-admins write to its jcr:content)
the workItem/metaData for the current workflow step
However, Split steps don't have access to workItem. I found a fairly un-helpful confirmation of that here: http://blogs.adobe.com/dmcmahon/2013/03/26/cq5-failure-running-script-etcworkflowscriptscaworkitem-ecma-referenceerror-workitem-is-not-defined/
So basically the issue was, the Dialog step could store the property, but the OR Split couldn't access it.
My workaround was to add a Process step straight after the Dialog in my workflow. Process steps do have access to workItem, so they can read the property set by the Dialog. I never particularly wanted to store this data on the payload's jcr:content, so I looked for another location. It turns out the workflow metaData (at the top level of the workflow instance node, rather than workItem/metaData, which is inside the /history sub-node) is accessible to both the Process step and the OR Split. So, my Process step now reads the workItem's approveReject property (set by the Dialog), and then writes it to the workflow's metaData node. Then, the OR Split reads the property from its new location, and does its magic.
The way you access the workflow metaData from the Process step and the OR Split is not consistent, but you can get there from both.
Here's some code: (complete with comments. You're welcome)
In the dialog where you choose to approve or reject, the name of the field is set to rejectApprove. There's no ./ or anything before it. This tells it to store the property on the workItem/metaData node for the current workflow step under /history/.
Straight after the dialog, a Process step runs this:
var rejectApprove;
var history = workflowSession.getHistory(workItem.getWorkflow());
// loop backwards through workItems
// and as soon as we find a rejectApprove that is not empty
// store that as 'rejectApprove' and break.
for (var index = history.size() - 1; index >= 0; index--) {
var previous = history.get(index);
var tempRejectApprove = previous.getWorkItem().getMetaDataMap().get('rejectApprove');
if ((tempRejectApprove != '')&&(tempRejectApprove != null)) {
rejectApprove = tempRejectApprove;
break;
}
}
// steps up from the workflow step into the workflow metaData,
// and stores the rejectApprove property there
// (where it can be accessed by an OR Split)
workItem.getWorkflowData().getMetaData().put('rejectApprove', rejectApprove);
Then after the Process step, the OR Split has the following in its tabs:
function check() {
var match = 'approve';
if (workflowData.getMetaData().get('rejectApprove') == match) {
return true;
} else {
return false;
}
}
Note: use this for the tab for the "approve" path, then copy it and replace var match = 'approve' with var match = 'reject'
So the key here is that from a Process step:
workItem.getWorkflowData().getMetaData().put('rejectApprove', rejectApprove);
writes to the same property that:
workflowData.getMetaData().get('rejectApprove') reads from when you execute it in an OR Split.
To suit our business requirements, there's more to the workflow I've implemented than just this, but the method above seems to be a pretty reliable way to get values that are entered in a dialog, and access them from within an OR Split.
It seems pretty silly that the OR Split can't access the workItem directly, and I'd be interested to know if there's a less roundabout way of doing this, but for now this has solved my problem.
I really hope someone else has this same problem, and finds this useful, because it took me waaay to long to figure out, to only apply it once!

Clear posted values before returning the view

I implemented a feature to automatically load the next record after finishing the current one. On the server, I can get the next record and load it into the model fine. The problem is, when I return the view, MVC favors the posted values from the previous record over the model values from the current record.
Public Function Update(model As UpdateModel) As ActionResult
'... save changes to the model
If model.LoadNext Then
Dim nextRecord As UpdateModel = GetNextRecord()
Return View(nextRecord)
Else
Return RedirectToAction("Index")
End If
End Function
I've confirmed in the debugger that I am passing the nextRecord properly. The view is reading the posted values (from the Request I guess?) instead of using the model.
Is there a way to avoid this behavior. Request.Form, Request.Params, and Request.QueryString are all read only so I cannot clear them.
You should call ModelState.Clear() before returning the view. Keep in mind Post-Redirect-Get is also an option if you don't want to break user navigation.

GWT EditorDriver Proxy Object Modification

This is an edited version of a question that I had previously asked (and that tbroyer answered) about why the isDirty() method didn't return true after attempting to modify the Editor version of an entity. I think that my understanding of the RequestFactory/EditorDriver handling of entities is the issue, and that the isDirty() question was a red herring. I've left my original question at the end of this question, but my new question is:
How can the entity (proxy) that is being edited by an EditorDriver be modified in code? Obviously values will be changed as a result of changes in the user interface; but I don't know how to change values 'behind the scenes'. My understanding is that the call to EditorDriver.edit() will create a copy of the proxy object, and that subsequently any changes to that copy will be applied to the original object using EditorDriver.flush(). But EditorDriver.edit() does not return a reference to the object that is being edited (unlike RequestContext.edit(), which does return a reference to the object being edited).
The original (ill-informed) question:
I don't understand why the EditorDriver.isDirty() method is not returning true in the following situation (the following onOrgSelectedEvent() method is invoked when a new Org has been selected from a listbox):
private IOrgProxy _org;
...
/**
* Loads the currently selected Org into the editor.
*/
#Override
public void onOrgSelectedEvent(final OrgSelectedEvent orgSelectedEvent) {
IOrgProxy org = _clientFactory.getCache().getOrgCache().getOrg(orgSelectedEvent.getOrgId());
_orgRequestContext = _clientFactory.getRequestFactory().newOrgRequestContext();
_org = _orgRequestContext.edit(org);
_orgEditorDriver.edit(_org, _orgRequestContext);
_org.setName(_org.getName() + " (edit)");
if (_orgEditorDriver.isDirty()) {
_org.setName(org.getName());
}
}
When I put breakpoints on the setName() calls I see that the first call changes the name in the editable Org object, but the second setName() call is never reached (i.e., _orgEditorDriver.isDirty() returns false).
Just as a side question, it seems strange to me that the EditorDriver.edit() method doesn't return the editable proxy object, and that I have to call RequestContext.edit(), but that's a very minor issue.
Why would isDirty be true just after edit? Clearly the user hasn't be given the time to do any change.
isDirty compares the current value of the subeditors to their original value, it doesn't care whether the object changed: if you lend the object to the editor, you implicitly gives it control over the edited object (for the edited properties).