3

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.
Leon Cullens
  • 12,276
  • 10
  • 51
  • 85
  • 2
    Possible duplicate of [How do I implement multiple LUIS dialogs on a single bot using the Bot Framework?](https://stackoverflow.com/questions/40399973/how-do-i-implement-multiple-luis-dialogs-on-a-single-bot-using-the-bot-framework) – D4RKCIDE Nov 02 '17 at 17:00
  • @JasonSowers no it's no. I don't see a solution for my question. – Leon Cullens Nov 03 '17 at 16:10

2 Answers2

3

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

Ezequiel Jadib
  • 14,767
  • 2
  • 38
  • 43
  • I don't understand what you mean by your second paragraph. Re your third paragraph: if a user wants to place an order, but didn't specify *what* he would like to order, the bot would have to ask for clarification, and understand the responses using LUIS. Maybe I have a wrong view of what this flow would normally look like. Could you provide an example? – Leon Cullens Nov 06 '17 at 12:19
  • OK, got it. Probably the two LUIS models are what adds complexity to what you are trying to achieve... I think what you are looking is LUIS Action Binding. Take a look to https://github.com/Microsoft/BotBuilder-Samples/tree/master/CSharp/Blog-LUISActionBinding I suspect it's what you want. – Ezequiel Jadib Nov 06 '17 at 12:23
  • I also edited my answer. I included the issue I'm seeing in your current flow. But again, what you want is Luis Action Binding :) – Ezequiel Jadib Nov 06 '17 at 12:28
  • I have a lot of reading to do to figure out how to apply this to my situation. The Action Binding documentation is quite long =) – Leon Cullens Nov 10 '17 at 18:43
  • :) Based on your sample it should apply ok. LMK. – Ezequiel Jadib Nov 10 '17 at 18:48
  • I read the whole page, but it goes very deep into writing a whole bunch of custom stuff. But I think my scenario should not be that complicated at all and is one of the most basic things you should be able to do? So I don't get why I would need a whole bunch of custom classes and code. Could you please provide me with some code examples on how to change my code to do what I want so I can understand what I'm doing wrong? – Leon Cullens Nov 13 '17 at 16:42
  • P.s., already given you the bounty because otherwise no-one else gets it :) – Leon Cullens Nov 13 '17 at 16:47
  • Thanks! Give me a day or two and I will try to see how we can update your sample without using Luis Action Binding. But believe, LAB is the way to go, before it was part of the platform but it was deprecated that's why you need to do it via code. – Ezequiel Jadib Nov 13 '17 at 18:13
  • thanks! Looking forward to it :) Since it's deprecated; is there a new way coming out to do this? – Leon Cullens Nov 13 '17 at 18:34
  • Yes, using the LAB code I shared with you. It's the replacement. – Ezequiel Jadib Nov 14 '17 at 02:43
  • Not yet, I will try to do it during the weekend. There at least 3 ways to do it: 1 - chaining prompts (too verbose) 2 - with formflow (luis detect intent and forwards to a formflow) 3 - with LAB (recommended) Not sure why you don't want to go with LAB though :) – Ezequiel Jadib Nov 17 '17 at 12:35
  • Sounds like LAB is the best. It's not that I don't want to use it, but I don't understand why I would need to use a deprecated library to do something that should be supported out of the box? Makes me think it's not the right way to do it. – Leon Cullens Nov 17 '17 at 12:56
  • 2
    No! The LAB library is not deprecated!! The library is the new way to do it. What it's deprecated is the support for this in the LUIS portal. Some time ago, from the LUIS portal you were able to define actions/prompts for missing entities; that was deprecated in favor of the code library I shared. – Ezequiel Jadib Nov 17 '17 at 13:00
-1

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

kc-
  • 29
  • 5
  • 1
    This does not help me with my problem and is not a solution to my question. – Leon Cullens Nov 03 '17 at 16:10
  • 1
    Did you perhaps mean to post this as an answer to a different question? This doesn't make any sense as an attempt to answer the question that was asked here. – Sam Hanley Nov 04 '17 at 02:10
  • @SamHanley Hmm, the question stated "I want to use LUIS to understand the user's answers, because there are for example many different ways a user can respond with his address" I thought of using the service (mentioned in my answer) as that helps with getting the user's address and validating it. That handles the 'where to send it' part. As for the 'what the user wants to order', maybe he can consider FormFlow or PromptDialog. Maybe I am deviating from the question a little, but just trying to suggest other ways of getting what he want – kc- Nov 04 '17 at 03:43
  • @kc-, my apologies, that context helps! I'd just suggest that you might want to edit a bit of that context into the answer itself, because without it, it's easy to miss the connection between the suggestion you provided and the fact that the user's address was mentioned in the question. Without making that connection, it reads as a bit of a non sequitur. – Sam Hanley Nov 05 '17 at 22:14