How does navigation work with LUIS subdialogs? - bots

I have a question... Unfortunately all the samples on the web are too shallow and don't really cover this well:
I have a RootDialog that extends the LuisDialog. This RootDialog is responsible for figuring out what the user wants to do. It could be multiple things, but one of them would be initiating a new order. For this, the RootDialog would forward the call to the NewOrderDialog, and the responsibility of the NewOrderDialog would be to figure out some basic details (what does the user want to order, which address does he like to use) and finally it will confirm the order and return back to the RootDialog.
The code for the RootDialog is very simple:
[Serializable]
public class RootDialog : LuisDialog<object>
{
public RootDialog() : base(new LuisService(new LuisModelAttribute(ConfigurationManager.AppSettings["LuisAppId"], ConfigurationManager.AppSettings["LuisAPIKey"], domain: "westus.api.cognitive.microsoft.com")))
{
}
[LuisIntent("Order.Place")]
public async Task PlaceOrderIntent(IDialogContext context, LuisResult result)
{
await context.Forward(new NewOrderDialog(), OnPlaceOrderIntentCompleted, context.Activity, CancellationToken.None);
context.Wait(MessageReceived);
}
private async Task OnPlaceOrderIntentCompleted(IDialogContext context, IAwaitable<object> result)
{
await context.PostAsync("Your order has been placed. Thank you for shopping with us.");
context.Wait(MessageReceived);
}
}
I also had some code in mind for the NewOrderDialog:
[Serializable]
public class NewOrderDialog : LuisDialog<object>
{
private string _product;
private string _address;
public NewOrderDialog() : base(new LuisService(new LuisModelAttribute(ConfigurationManager.AppSettings["LuisAppId"], ConfigurationManager.AppSettings["LuisAPIKey"], domain: "westus.api.cognitive.microsoft.com")))
{
}
[LuisIntent("Order.RequestedItem")]
public async Task RequestItemIntent(IDialogContext context, LuisResult result)
{
EntityRecommendation item;
if (result.TryFindEntity("Item", out item))
{
_product = item.Entity;
await context.PostAsync($"Okay, I understood you want to order: {_product}.");
}
else
{
await context.PostAsync("I couldn't understand what you would like to buy. Can you try it again?");
}
context.Wait(MessageReceived);
}
[LuisIntent("Order.AddedAddress")]
public async Task AddAddressIntent(IDialogContext context, LuisResult result)
{
EntityRecommendation item;
if (result.TryFindEntity("Address", out item))
{
_address = item.Entity;
await context.PostAsync($"Okay, I understood you want to ship the item to: {_address}.");
}
else
{
await context.PostAsync("I couldn't understand where you would like to ship the item. Can you try it again?");
}
context.Wait(MessageReceived);
}
}
The code as listed doesn't work. Upon entering the Order.Place intent, it immediately executes the 'success' callback, and then throws this exception:
Exception: IDialog method execution finished with multiple resume handlers specified through IDialogStack.
[File of type 'text/plain']
So I have a few questions:
How do I solve the error that I get?
How can I, upon entering the NewOrderDialog, check if we already know what the product and address is, and if not prompt them for the correct info?
Why does the NewOrderDialog get closed even though I don't call anything like context.Done()? I only want it to be closed when all the info has been gathered and the order has been confirmed.

So the first issue is that you are doing a context.Forward and a context.Wait in "Order.Place", which by definition is wrong. You need to choose: forward to a new dialog or wait in the current. Based on your post, you want to forward, so just remove the Wait call.
Besides that, you have 1 LUIS dialog and you are trying to forward to a new LUIS dialog... I have my doubts if that will work on not; I can imagine those are two different LUIS models otherwise it will be just wrong.
Based on your comment, I now understand what you are trying to do with the second dialog. The problem (and this is related to your second question) is that using LUIS in tha way might be confusing. Eg:
user: I want to place an order
bot => Forward to new dialog. Since it's a forward, the activity.Text will likely go to LUIS again (to the model of the second dialog) and nothing will be detected. The second dialog will be in Wait state, for user input.
Now, how the user will know that he needs to enter an address or a product? Where are you prompting the user for them? See the issue?
Your third question I suspect is a side effect of the error you are having in #1, which I already provided the solution for.
If you clarify a bit more I might be even more helpful. What you are trying to do with LUIS in the second dialog it doesn't look ok, but maybe with an explanation might have sense.
A usual scenario would be: I get the intent from LUIS ("Order.Place") and then I start a FormFlow or a set of Prompts to get the info to place the order (address, product, etc) or if you want to keep using LUIS you might want to check Luis Action Binding. You can read more on https://blog.botframework.com/2017/04/03/luis-action-binding-bot/.

Do you know about the Bing Location Control for Microsoft Bot Framework? It can be used to handle the 'which address does he like to use' part in obtaining and validating the user's address.
Here is some sample code:
[LuisModel("xxx", "yyy")]
[Serializable]
public class RootDialog : LuisDialog<object>
{
...
[LuisIntent("Find Location")]
public async Task FindLocationIntent(IDialogContext context, LuisResult result)
{
try
{
context.Call(new FindUserLocationDialog(), ResumeAfterLocationDialog);
}
catch (Exception e)
{
// handle exceptions
}
}
public async Task ResumeAfterLocationDialog(IDialogContext context, IAwaitable<object> result)
{
var resultLocation = await result;
await context.PostAsync($"Your location is {resultLocation}");
// do whatever you want
context.Wait(this.MessageReceived);
}
}
And for "FindUserLocationDialog()", you can follow the sample from Line 58 onwards.
Edit:
1) Can you try using:
[LuisIntent("Order.Place")]
public async Task PlaceOrderIntent(IDialogContext context, LuisResult result)
{
context.Forward(new NewOrderDialog(), OnPlaceOrderIntentCompleted, context.Activity, CancellationToken.None);
// or this
// context.Call(new NewOrderDialog(), OnPlaceOrderIntentCompleted);
}
2) I would say it depends on how you structured your intent. Does your "Order.Place" intent include entities? Meaning to say, if your user said "I want to make an order of Product X addressed to Address Y", does your intent already pick up those entities?
I would suggest that you check the product and address under your "Order.Place" intent. After you obtained and validated the product and address, you can forward it to another Dialog (non-LUIS) to handle the rest of the order processing.
[LuisIntent("Order.Place")]
public async Task PlaceOrderIntent(IDialogContext context, LuisResult result)
{
EntityRecommendation item;
if (result.TryFindEntity("Item", out item))
{
_product = item.Entity;
}
if (result.TryFindEntity("Address", out item))
{
_address = item.Entity;
}
if (_product == null)
{
PromptDialog.Text(context, this.MissingProduct, "Enter the product");
}
else if (_address == null)
{
PromptDialog.Text(context, this.MissingAddress, "Enter the address");
}
// both product and address present
context.Forward(new NewOrderDialog(), OnPlaceOrderIntentCompleted, **object with _product and _address**, CancellationToken.None);
}
private async Task MissingProduct(IDialogContext context, IAwaitable<String> result)
{
_product = await result;
// perform some validation
if (_address == null)
{
PromptDialog.Text(context, this.MissingAddress, "Enter the address");
}
else
{
// both product and address present
context.Forward(new NewOrderDialog(), OnPlaceOrderIntentCompleted, **object with _product and _address**, CancellationToken.None);
}
}
private async Task MissingAddress(IDialogContext context, IAwaitable<String> result)
{
_address = await result;
// perform some validation
// both product and address present
context.Forward(new NewOrderDialog(), OnPlaceOrderIntentCompleted, **object with _product and _address**, CancellationToken.None);
}
Something along these lines. Might need to include try/catch.
3) I think it has got to do with your 'context.Wait(MessageReceived)' in 'Order.Place' LUIS intent

Related

Release invoice on new screen

I need your help.
I have created a new screen, where I am calling all invoices pending release.
I have problems to release, I send a message where you request (you want to release).
It shows me the infinite message.
Only once should you ask me, then you should go out and follow the normal process.
public ProcessDocNew()
{
// Acuminator disable once PX1008 LongOperationDelegateSynchronousExecution [Justification]
Document.SetProcessDelegate(
delegate (List<ARInvoice> list)
{
List<ARRegister> newlist = new List<ARRegister>(list.Count);
foreach (ARInvoice doc in list)
{
newlist.Add(doc);
}
ProcessDoc(newlist, true);
}
);
Document.SetProcessCaption(ActionsMensje.Process);
Document.SetProcessAllCaption(ActionsMensje.ProcessAll);
}
public virtual void ProcessDoc(List<ARRegister> list, bool isMassProcess)
{
string title = "Test";
string sms = "¿Stamp?";
var Graph = PXGraph.CreateInstance<ARInvoiceEntry>();
ARInvoice document = Document.Current;
PEFEStampDocument timbrar = new PEFEStampDocument();/*This is a class where it is, all my method*/
if (isMassProcess == true)
{
Document.Ask(title, sms, MessageButtons.YesNo, MessageIcon.Question);
{
PXLongOperation.StartOperation(Graph, delegate
{
timbrar.Stamp(document, Graph); /*here I have my release method*/
});
}
}
}
public static class ActionsMensje
{
public const string Process = "Process";
public const string ProcessAll = "Process All";
}
I await your comments
Only once should you ask me, then you should go out and follow the
normal process.
That is not how the processing pattern works. The process delegate is called for each record and is therefore not a valid location to display a message that should be shown only once.
You would need to add a custom action to achieve that behavior. The scenario you're looking for should be implemented with a processing filter checkbox and processing filter to comply with best practices:
Documentation on processing screens implementation is available here:
https://help-2019r2.acumatica.com/Help?ScreenId=ShowWiki&pageid=a007b57b-af69-4c0f-9fd1-f5d98351035f

Azure Bot fails after a time [duplicate]

Good day everyone,
I'm creating a chatbot for my company and I started with the samples on github and the framework docs.
We decided to host it on Azure and added LUIS and Table Storage to it. The Bot runs fine locally in Botframework Emulator, but on Azure (WebChat, Telegram) it will only run for approximatly an hour to an hour and fifteen minutes, if no one tries to communicate with the bot. After this period of time, the bot will just run into an internal server error. When you ask the bot something, you can stretch this time window (For how long I don't know and why I don't know either, sorry).
In Azure "Always On" is set to true.
I'm really frustrated at this point, because I cannot find the problem and I'm pretty sure there must be something wrong with my code, because I don't properly understand the framework. I'm still a beginner with Azure, C# and Bot Framework.
Also I have already read everything on "internal server error's" on here and github. Also tried Debugging, even with extra Debbug options in VS. We have not tried Application Insights yet.
At the moment I'm doing everything with the LUIS Dialog which calls / Forwards to other IDialogs:
[LuisIntent(Intent_Existens)]
public async Task ExistensOf(IDialogContext context, IAwaitable<IMessageActivity> message, LuisResult result)
{
var existens = new ExistensDialog();
var messageToForward = await message;
if (result.Entities.Count == 1)
{
messageToForward.Value = result.Entities[0].Entity;
await context.Forward(existens, AfterDialog, messageToForward);
}
else
{
context.Wait(this.MessageReceived);
}
}
I know that "Value" is for CardActions, but I don't know how else I could pass Entities to the child dialog.
[Serializable]
public class ExistensDialog : IDialog<object>
{
public async Task StartAsync(IDialogContext context)
{
context.Wait(MessageReceivedAsync);
}
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> result)
{
var message = await result;
if (message.Text.Contains("specificWord"))
{
await context.Forward(new ExistensHU(), AfterDialog, message);
}
else
{
await context.Forward(new ExistensBin(), AfterDialog, message);
}
}
private async Task AfterDialog(IDialogContext context, IAwaitable<object> result)
{
context.Done<object>(null);
}
}
then:
[Serializable]
internal class ExistensHU : IDialog<object>
{
private Renamer renamer = new Renamer(); // Just for renaming
private ExternalConnection ec = new ExternalConnection(); //Just a HTTP connection to a WebApp to get data from it
public async Task StartAsync(IDialogContext context)
{
context.Wait(MessageReceivedAsync);
}
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> result)
{
const string apiCallURL = "some/API/"; // ExternalConnection...
var message = await result;
string nameHU = renamer.RemoveBlanks(message.Value.ToString());
StringBuilder answerBuilder = new StringBuilder();
var name = ec.CreateSingleAPIParameter("name", nameHU);
Dictionary<string, string> wms = await ec.APIResultAsDictionary(apiCallURL, name);
foreach (var item in wms)
{
if (item.Key.Equals("none") && item.Value.Equals("none"))
{
answerBuilder.AppendLine($"Wrong Answer");
}
else
{
answerBuilder.AppendLine($"Correct Answer");
}
}
await context.PostAsync(answerBuilder.ToString());
context.Done<object>(null);
}
}
That's basically every Dialog in my project.
Also I have an IDialog which looks like this:
[Serializable]
public class VerificationDialog : IDialog<object>
{
[NonSerializedAttribute]
private readonly LuisResult _luisResult;
public VerificationDialog(LuisResult luisResult)
{
_luisResult = luisResult;
}
public async Task StartAsync(IDialogContext context)
{
var message = _luisResult.Query;
if (!message.StartsWith("Wie viele"))
{
context.Call(new ByVerificationDialog(_luisResult), AfterDialog);
}
else
{
context.Call(new CountBinsByVerification(_luisResult), AfterDialog);
}
}
private async Task AfterDialog(IDialogContext context, IAwaitable<object> result)
{
context.Done<object>(null);
}
}
I don't know if I'm allowed to pass the luisResult like this from BasicLuisDialog. This could be the issue or one of the issues.
Basically that's it and I'm still getting used to the framework. I'm not expecting an absolute answer. Just hints/tips and advice how to make everything better.
Thanks in advance!
If you are using the .NET SDK version 3.14.0.7. There is currently a bug we are tracking in this version. There has been a number of reports and we are actively investigating. Please try downgrading to 3.13.1. This should fix the issue for you until we can release a new version.
for reference we are tracking the issue on these GitHub issues:
https://github.com/Microsoft/BotBuilder/issues/4322
https://github.com/Microsoft/BotBuilder/issues/4321
Update 3/21/2018:
We have pushed a new version of the SDK which includes a fix for this issue https://www.nuget.org/packages/Microsoft.Bot.Builder/3.14.1.1
Internal error usually means exceptions in .NET application.
Use AppDomain.CurrentDomain.UnhandledException to receive all unhandled exceptions and log them somewhere (consider using Application Insights).
After you investigate logged information fix that.

How to deal with Context.Done(R value)

I have a msbot chat dialog that I want to have the following behaviour:
user -> get me some info about GARY
bot -> which gary, (prompt: choice options)
user -> gary peskett
bot -> sure, (hero card with gary's contact details)
I have this code
public class CustomerRepository
{
private IList<Customer> _customerList = new List<Customer>
{
new Customer
{
Name = "Gary Peskett"
},
new Customer
{
Name = "Gary Richards"
},
new Customer
{
Name = "Barry White"
}
};
public async Task<IEnumerable<Customer>> GetAll()
{
// usually calls a database (which is why async is on this method)
return _customerList;
}
}
public class XDialog : IDialog
{
private readonly IIntent _intent;
private readonly CustomerRepository _customerRepository;
public XDialog(IIntent intent, CustomerRepository customerRepository)
{
// An intent is decided before this point
_intent = intent;
_customerRepository = customerRepository;
}
public async Task StartAsync(IDialogContext context)
{
// // An intent can provide parameters
string name = _intent.Parameters["Name"] as string;
IEnumerable<Customer> customers = await _customerRepository.GetAll();
IList<Customer> limitedList = customers.Where(x => x.Name.Contains(name)).ToList();
if (limitedList.Any())
{
if (limitedList.Count > 1)
{
PromptDialog.Choice(context, LimitListAgain, limitedList,
"Can you specify which customer you wanted?");
}
else
{
Customer customer = limitedList.FirstOrDefault();
Finish(context, customer);
}
}
else
{
context.Done("No customers have been found");
}
}
private static async Task LimitListAgain(IDialogContext context, IAwaitable<Customer> result)
{
Customer customer = await result;
Finish(context, customer);
}
private static void Finish(IDialogContext context, Customer customer)
{
HeroCard heroCard = new HeroCard
{
Title = customer?.Name
};
context.Done(heroCard);
}
}
What i'm finding is that usually when I do context.Done(STRING) then that is output to the user, and this is really useful to end the dialog. As I want to end with a hero card, its outputing the typename
Microsoft.Bot.Connector.HeroCard
Can anyone help by either explaining a better way to use context.Done(R value) or help me return a hero card to end the dialog?
The dialog is being called with
Chain.PostToChain()
.Select(msg => Task.Run(() => _intentionService.Get(msg.ChannelId, msg.From.Id, msg.Text)).Result)
.Select(intent => _actionDialogFactory.Create(intent)) // returns IDialog based on intent
.Unwrap()
.PostToUser();
I think the problem is a side effect of using Chain.
As you may know, the context.Done doesn't post anything back to the user, it just ends the current dialog with the value provided.
The post to user is effectively happening in the .PostToUser() at the end of your Chain. Now, by looking into the PostToUser's code, I realized that at the end of the game, it's doing a context.PostAsync of item.ToString(), being item the payload provided in the context.Done in this case. See this.
One option (I haven't tested this), could be using .Do instead of .PostToUser() and manually perform what the PostToUserDialog does and finally perform a context.PostAsync() by creating a new IMessageActivity and adding the HeroCard as an attachment.

How to pass control from one LUIS method to another?

Is it possible to pass control from one LUIS method to another, or how to create a method that can be shared by several LUIS methods in the same way (something like a default reaction if the intent score is too low)?
You can pass control from one Luis Method to another:
[LuisIntent("IntentOne")]
public async Task IntentOneHandler(IDialogContext context, LuisResult result)
{
await IntentTwoHandler(context, result);
}
[LuisIntent("IntentTwo")]
public async Task IntentTwoHandler(IDialogContext context, LuisResult result)
{
await context.PostAsync("IntentTwoResponse");
context.Wait(MessageReceived);
}
And the "None" intent should fire when no good intent match is found:
[LuisIntent("None")]
public async Task NoneHandler(IDialogContext context, LuisResult result)
{
}

How to insert multiple objects in to Azure Mobile Services table controller [.Net backend]

I have an Azure Mobile service coded in .net Web API. I have a TableController. I want that table controller to be able to insert multiple persons, not just one person with from the client with InsertAsync(myPerson). I have the following code in the TableController:
[RequiresAuthorization(AuthorizationLevel.Admin)]
public async Task<bool> InsertPersons(List<Person> values)
{
try
{
foreach (var item in values)
{
var current = await InsertAsync(item);
}
return true;
}
catch (System.Exception)
{
return false;
}
}
The problem is in the client. Because it is strongly typed it only allows me to insert one item at a time. How must I call the server from the client? Do I have to write a Custom Api Controller and call it with mobileService.InvokeApiAsync? If so, how can I get access to my database from a Custom API Controller that doesn't inherit from TableController?
Thank you so much!
The helper methods in the TableController<T> base class assume that the insert operations apply to a single object - and the InsertAsync method in the client also assumes the same. So even though you can define in a table controller a method that takes an array (or list) of Person, you won't be able to call it via the client SDK (at least not without some heavy-lifting using a handler, for example).
You can, however, create a custom API which takes such a list. And to insert the multiple items from the API, you can access the context directly, without needing to go through the helper methods from the table:
public class PersonController : ApiController
{
test20140807Context context;
protected override void Initialize(HttpControllerContext controllerContext)
{
base.Initialize(controllerContext);
this.context = new test20140807Context();
}
[HttpPost]
public async Task<bool> InsertPersons(List<Person> values)
{
foreach (var value in values)
{
if (string.IsNullOrEmpty(value.Id))
{
value.Id = Guid.NewGuid().ToString();
}
}
try
{
this.context.People.AddRange(values);
await this.context.SaveChangesAsync();
return true;
}
catch (System.Exception ex)
{
Trace.WriteLine("Error: " + ex);
return false;
}
}
}
And on the client:
private async void btnTest_Click(object sender, RoutedEventArgs e)
{
var items = new Person[]
{
new Person { Name = "John Doe", Age = 33 },
new Person { Name = "Jane Roe", Age = 32 }
};
try
{
var response = await App.MobileService.InvokeApiAsync<Person[], bool>("person", items);
Debug.WriteLine("response: " + response);
}
catch (Exception ex)
{
var str = ex.ToString();
Debug.WriteLine(str);
}
}
From Carlos Figueira's post on inserting multiple items at once in azure mobile services, it looks like what you need to do is create another table called AllPersons. In your client, the AllPersons object would have a Persons array member. In your server side script for the AllPersons insert, you iterate through the AllPersons.Persons and insert into the table one by one.

Resources