Search code examples
f#dsltype-providersazure-language-understanding

What's a good way to write a F# Type Provider for Microsoft's Luis?


Having a play with Microsoft's Luis + bot framework, my "this would make a good type provider" sense started tingling. Unfortunately type providers can't output discriminated unions. I was hoping to do something like the following, but it isn't possible:

type Luis = LuisProvider<@"LuisId",@"LuisPasskey">
let IntentMatcher Intent =
    match intent with
    | Luis.Intents.Greeting -> GreetingHandler()
    | Luis.Intents.SetAlarm title startDate startTime -> AlarmHandler title startDate startTime
    | _ -> CouldNotUnderstand()

The Luis intents and their parameters are all available via Apis making them great candidates for typeProviderization

For reference here is a handler from an example C# bot (which I think could be cleaner, and more type safe in F#):

public const string Entity_Alarm_Title = "builtin.alarm.title";
public const string Entity_Alarm_Start_Time = "builtin.alarm.start_time";
public const string Entity_Alarm_Start_Date = "builtin.alarm.start_date";
public const string DefaultAlarmWhat = "default";

[LuisIntent("builtin.intent.alarm.set_alarm")]
public async Task SetAlarm(IDialogContext context, LuisResult result)
{
        EntityRecommendation title;
        if (!result.TryFindEntity(Entity_Alarm_Title, out title))
        {
            title = new EntityRecommendation(type: Entity_Alarm_Title) { Entity = DefaultAlarmWhat };
        }
        EntityRecommendation date;
        if (!result.TryFindEntity(Entity_Alarm_Start_Date, out date))
        {
            date = new EntityRecommendation(type: Entity_Alarm_Start_Date) { Entity = string.Empty };
        }
        EntityRecommendation time;
        if (!result.TryFindEntity(Entity_Alarm_Start_Time, out time))
        {
            time = new EntityRecommendation(type: Entity_Alarm_Start_Time) { Entity = string.Empty };
        }
        var parser = new Chronic.Parser();
        var span = parser.Parse(date.Entity + " " + time.Entity);
        if (span != null)
        {
            var when = span.Start ?? span.End;
            var alarm = new Alarm() { What = title.Entity, When = when.Value };
            this.alarmByWhat[alarm.What] = alarm;
            string reply = $"alarm {alarm} created";
            await context.PostAsync(reply);
        }
        else
        {
            await context.PostAsync("could not find time for alarm");
        }
        context.Wait(MessageReceived);
}

Anyway the question is: does anyone with more experience building type providers have any good ideas on how I can structure a readable dsl that is actually feasible to build?


Solution

  • I'm not particularly familiar with the bot framework, but I can comment on discriminated unions - we face the similar problem in F# data.

    If you have <One name="string" /><Two id="42" />, it would be nice to provide discriminated union with cases One of string and Two of int. What we do instead is that we provide a type:

    type OneOrTwo =
      member One : option<string>
      member Two : option<int>
    

    You could follow the same pattern and expose API that looks something like this:

    type Luis = LuisProvider<"LuisId", "LuisPasskey">
    
    let intentMatcher (intent:Luis.Intents) =
      match intent.Greetings, intent.SetAlarm with
      | Some(), _ -> greetingHandler()
      | _, Some(title, startDate, startTime) -> alarmHandler title startDate startTime
      | _ -> couldNotUnderstand()
    
    Luis.Connect().OnIntent
    |> Observable.subscribe intentMatcher
    

    It is not quite as elegant as discriminated unions, but it should be technically doable.

    I suppose that another alternative would be to expose handlers for the individual actions as separate events and then you could write something like this:

    type Luis = LuisProvider<"LuisId", "LuisPasskey">
    
    let luis = Luis.Connect()
    
    luis.BuiltIn.Greetings 
    |> Observable.add greetingHandler
    
    luis.BuiltIn.SetAlarm 
    |> Observable.add (fun (title, startDate, startTime) -> 
         alarmHandler title startDate startTime)
    

    Now that I think about it, this would probably be nicer, but it depends on what kind of uses are typical for the bot framework.