How does navigation work with LUIS subdialogs?

家住魔仙堡 提交于 2019-12-10 19:22:47

问题


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:

  1. How do I solve the error that I get?
  2. 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?
  3. 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.

回答1:


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/.




回答2:


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



来源:https://stackoverflow.com/questions/47077488/how-does-navigation-work-with-luis-subdialogs

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!