microsoft / botframework-sdk

Bot Framework provides the most comprehensive experience for building conversation applications.
MIT License
7.49k stars 2.44k forks source link

how do I accomplish this ? #501

Closed amoldeshpande closed 8 years ago

amoldeshpande commented 8 years ago

consider a scenario where I am inviting a group of people to a catered party where they have the choice of food of various cuisines.

My main form might look like this: --- various options --- Please pick the cuisine you'd like for your meal ---- continue picking options ---

i.e., in addition to choosing various other options (seating arrangements, for example), I'd like to kick them into a form for picking specifics for their meal.

e.g., Italian -> would they like Pasta, Salad, Pizza, etc. Thai -> Green Curry, Red Curry, etc.

In the simplest implementation, I suppose what I want is to be able to do something like this

context.Call(CuisineTypeBase.GetFormByName(cuisineName),cuisineInputComplete);

I don't see a way in the current framework to launch dialogs via factory methods like this example.

Is there maybe a better way of doing what I need ?

I apologize for not posting this on stack overflow, but I see very few answered questions on there.

jeffreyhyun commented 8 years ago

You could have a first route called '/cuisine' which would contain a waterfall. The first step would be a prompt for the cuisine. The second closure will have logic to call a new route, each new route being the cuisine type. In each of those routes, you will have a prompt for the menu items.

amoldeshpande commented 8 years ago

my question is more of what's a good pattern to launch one form from a family of forms that's related by inheritance (or generics). If interfaces like IDialog were non-generics and instead returned objects implementing another (possibly empty) interface, this would be a lot easier.

jeffreyhyun commented 8 years ago

Ah, sorry. I completely misunderstood the question and still think I'm missing something, but last try... If it's by inheritance, couldn't you just end the child dialog which would send the response to the parent(s) and have the logic there in a third waterfall step to launch the new form? The first step would be the prompt, the second the logic to direct to the proper dialog in the "family of forms". That way regardless of the form you were in from the family, it will always go back to the parent dialog with the args you passed so it will always then go to that single final form.

chrimc62 commented 8 years ago

Have you looked at the chaining stuff? In particular Chain.Switch? You can have your front-end dialog select and then use switch to dispatch to the appropriate form child dialog. Here is an example from the EchoBot sample.

    public static readonly IDialog<string> dialog = Chain.PostToChain()
        .Select(msg => msg.Text)
        .Switch(
            new Case<string, IDialog<string>>(text =>
                {
                    var regex = new Regex("^reset");
                    return regex.Match(text).Success;
                }, (context, txt) =>
                {
                    return Chain.From(() => new PromptDialog.PromptConfirm("Are you sure you want to reset the count?",
                    "Didn't get that!", 3)).ContinueWith<bool, string>(async (ctx, res) =>
                    {
                        string reply;
                        if (await res)
                        {
                            ctx.UserData.SetValue("count", 0);
                            reply = "Reset count.";
                        }
                        else
                        {
                            reply = "Did not reset count.";
                        }
                        return Chain.Return(reply);
                    });
                }),
            new RegexCase<IDialog<string>>(new Regex("^help", RegexOptions.IgnoreCase), (context, txt) =>
                {
                    return Chain.Return("I am a simple echo dialog with a counter! Reset my counter by typing \"reset\"!");
                }),
            new DefaultCase<string, IDialog<string>>((context, txt) =>
                {
                    int count;
                    context.UserData.TryGetValue("count", out count);
                    context.UserData.SetValue("count", ++count);
                    string reply = string.Format("{0}: You said {1}", count, txt);
                    return Chain.Return(reply);
                }))
        .Unwrap()
        .PostToUser();
amoldeshpande commented 8 years ago

I'm not sure how I would use Chain.Switch<T,R> here. Doesn't the signature constrain the cases to return a particular type ?

In the echo example, IDialog is being returned in each "Case". I'd want to return a different IDialog for each one.

Thanks for the suggestions. I'm going to close this issue as it doesn't really seem appropriate for discussing in a bug database.

chrimc62 commented 8 years ago

We do most of our support here, so I've reopened. Switch can return object--you do not have to be strongly typed especially if you don't care about the value that is returned because you have processed it somehow.

Here is another example from the FormTest test code in the GitHub.

        var callDebug =
            Chain
            .From(() => new PromptDialog.PromptString("Locale?", null, 1))
            .ContinueWith<string, Choices>(async (ctx, locale) =>
                {
                    Locale = await locale;
                    CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo(Locale);
                    CultureInfo.CurrentCulture = CultureInfo.CurrentUICulture;
                    return FormDialog.FromType<Choices>(FormOptions.PromptInStart);
                })
            .ContinueWith<Choices, object>(async (context, result) =>
            {
                Choices choices;
                try
                {
                    choices = await result;
                }
                catch (Exception error)
                {
                    await context.PostAsync(error.ToString());
                    throw;
                }

                switch (choices.Choice)
                {
                    case DebugOptions.AnnotationsAndNumbers:
                        return MakeForm(() => PizzaOrder.BuildForm(noNumbers: false));
                    case DebugOptions.AnnotationsAndNoNumbers:
                        return MakeForm(() => PizzaOrder.BuildForm(noNumbers: true));
                    case DebugOptions.AnnotationsAndText:
                        return MakeForm(() => PizzaOrder.BuildForm(style: ChoiceStyleOptions.AutoText));
                    case DebugOptions.NoAnnotations:
                        return MakeForm(() => PizzaOrder.BuildForm(noNumbers: true, ignoreAnnotations: true));
                    case DebugOptions.NoFieldOrder:
                        return MakeForm(() => new FormBuilder<PizzaOrder>().Build());
                    case DebugOptions.WithState:
                        return new FormDialog<PizzaOrder>(new PizzaOrder()
                        { Size = SizeOptions.Large, Kind = PizzaOptions.BYOPizza },
                        () => PizzaOrder.BuildForm(noNumbers: false),
                        options: FormOptions.PromptInStart,
                        entities: new Luis.Models.EntityRecommendation[] {
                            new Luis.Models.EntityRecommendation("Address", "abc", "DeliveryAddress"),
                            new Luis.Models.EntityRecommendation("Toppings", "onions", "BYO.Toppings"),
                            new Luis.Models.EntityRecommendation("Toppings", "peppers", "BYO.Toppings"),
                            new Luis.Models.EntityRecommendation("Toppings", "ice", "BYO.Toppings"),
                            new Luis.Models.EntityRecommendation("NotFound", "OK", "Notfound")
                        }
                        );
                    case DebugOptions.Localized:
                        {
                            var form = PizzaOrder.BuildForm(false, false);
                            using (var stream = new FileStream("pizza.resx", FileMode.Create))
                            using (var writer = new ResXResourceWriter(stream))
                            {
                                form.SaveResources(writer);
                            }
                            Process.Start(new ProcessStartInfo(@"RView.exe", "pizza.resx -c " + Locale) { UseShellExecute = false, CreateNoWindow = true }).WaitForExit();
                            return MakeForm(() => PizzaOrder.BuildForm(false, false, true));
                        }
                    case DebugOptions.SimpleSandwichBot:
                        return MakeForm(() => SimpleSandwichOrder.BuildForm());
                    case DebugOptions.AnnotatedSandwichBot:
                        return MakeForm(() => AnnotatedSandwichOrder.BuildLocalizedForm());
                    case DebugOptions.JSONSandwichBot:
                        return MakeForm(() => AnnotatedSandwichOrder.BuildJsonForm());
                    default:
                        throw new NotImplementedException();
                }
            })
            .Do(async (context, result) =>
            {
                try
                {
                    var item = await result;
                    Debug.WriteLine(item);
                }
                catch (FormCanceledException e)
                {
                    if (e.InnerException == null)
                    {
                        await context.PostAsync($"Quit on {e.Last} step.");
                    }
                    else
                    {
                        await context.PostAsync($"Exception {e.Message} on step {e.Last}.");
                    }
                }
            })
            .DefaultIfException()
            .Loop();
amoldeshpande commented 8 years ago

ok, thanks. First, I should try to express more clearly what I'm trying to do.

Example code: `[LuisIntent("SelectCuisine")] public async Task SelectCuisine(IDialogContext context, LuisResult result) { String cuisine = findEntitiesOfType(kCuisineEntity, result);

context.Call(CuisineFormFactory.GetFormForCuisine(cuisine),cuisineSelectionComplete); }

public class CuisineFormFactory { public static IDialog GetFormForCuisine(String cuisine) { switch(cuisine) { case "thai": return MakeForm(()=>ThaiCuisine.BuildForm()); } } }

public abstract class BaseCuisineType { public String CuisineName; }

public class ThaiCuisineForm : BaseCuisineType { // fields here }`

This is slightly different from my original problem statement, but the underlying issue is the same.

I have a LUIS intent which wants to launch some form based on an entity extracted. However, context.Call needs a strongly typed IDialog, which the intent would not know about in advance.

I'm aware that there are many ways to skin this, but mainly I'm trying to isolate my LUIS intents from having to have a deep knowledge of the forms they are invoking.

I could potentially have hundreds of cuisine types and I'd like to encapsulate that knowledge somewhere else, while still having the ability to continue with other forms in the main intent once the cuisine selection is done.

ankitbko commented 8 years ago

Can you try the following. Build out IDialog using FormDialog.

public static IDialog GetFormForCuisine(String cuisine)
{
  switch(cuisine)
  {
    case "thai":
      return new FormDialog<object>(new ThaiCuisineForm(), ThaiCuisine.BuildForm(), FormOptions.PromptInStart) ;
  }
}
amoldeshpande commented 8 years ago

Unfortunately, since ThaiCusiine.BuildForm() is strongly typed (it returns FormBuilder<ThaiCusine)() ), this does not compile.

chrimc62 commented 8 years ago

Adding @willportnoy to comment.

willportnoy commented 8 years ago

IDialog is declared to have a covariant generic type argument, so you should be able to assign an instance of IDialog to IDialog - it's still type checked, though only as an object.

msft-shahins commented 8 years ago

Closing this, assuming that the issue is resolved. Feel free to open it again, if you have more follow up questions.