Custom BotFramework Intent Service Implementation

Developing a chatbot with language understanding capabilities is a huge leap from basic pattern recognition on the input to match to specific commands.

If you have a botframework chatbot, you’re currently limited to using LUIS as your NLP (Natural Language Processing) provider via the various Luis classes: LuisDialog, LuisModelAttribute, and LuisService.

If you’re trying to compare alternative NLP services, such as kitt.ai or wit.ai or even Alexa, then implementing support for another NLP service in Botframework for this can be a bit tricky.

In this article I’ll show you one approach to decoupling your botframework bot from a specific NLP solution.

LUIS

Before we create our own implementation, let’s look at the one that already exists within BotFramework: the LuisDialog and it’s associated henchmen – LuisModelAttribute and LuisService.

If you’re not already familiar with LUIS, check out these articles first

LuisModel Attribute

The class-level LuisModel attribute defines the configured “LUIS App” this class has intents mapped to.

[LuisModel("modelId","subscriptionKey")]

By adding that at the top of your dialog class, a message received to that dialog will end up being passed on to the LUIS app defined by the “subscription key” and “model Id” values, and the resulting intent in the response will be used to determine which method to fire, thanks to the LuisIntent method-level attribute:

[LuisIntent("intent name")]

For example, the class GetDetailsDialog could be configured like this:

// Values that match my LUIS app's subscription key and model ID
[LuisModel("<luis model id>","<luis subscription key>")]
public class GetDetailsDialog : LuisDialog<object>
{
    [LuisIntent("Details")]
    public async Task Details(IDialogContext context, 
        LuisResult result)
    {
        ...
    }

}

Let’s look at those building blocks in more detail.

LuisDialog

The LuisDialog type replaces IDialog in your class definition:

public class GetDetailsDialog : IDialog

becomes

public class GetDetailsDialog : LuisDialog<object>

This class has a MakeServicesFromAttributes method which is quite self-explanatory; using the LuisModel attributes on the class that extends LuisDialog it creates some instances of a LuisService class:

public ILuisService[] MakeServicesFromAttributes()
{
    var type = this.GetType();
    var luisModels = type.GetCustomAttributes<LuisModelAttribute>(inherit: true);
    return luisModels.Select(m => new LuisService(m)).Cast<ILuisService>().ToArray();
}

LuisDialog has a base MessageReceived method which queries these generated LuisServices; i.e., makes a call to the LUIS endpoint with the appropriate subscription key and model id:

...

var tasks = this.services.Select(s => 
    s.QueryAsync(messageText, context.CancellationToken)).ToArray();

...

So let’s see what a LuisService actually does, shall we? …

LuisService

The LuisService is a class intended to make the necessary call(s) to your LUIS App and map the response to a LuisResult:

async Task<LuisResult> ILuisService.QueryAsync(Uri uri, CancellationToken token)
{
    string json;
    using (var client = new HttpClient())
    using (var response = 
        await client.GetAsync(uri, 
                    HttpCompletionOption.ResponseContentRead, 
                    token))
    {
        response.EnsureSuccessStatusCode();
        json = await response.Content.ReadAsStringAsync();
    }

    try
    {
        var result = JsonConvert.DeserializeObject<LuisResult>(json);
        // snip ...
        return result;
    }
    catch (JsonException ex)
    {
        throw new ArgumentException("Unable to deserialize the LUIS response.", ex);
    }
}

That seems easy enough, doesn’t it? Make a request using an HttpClient to the configured LUIS App endpoint, then map the response and return it as a LuisResult. Nice.

LuisResult

As we saw at the start, the LuisResult is made available to the method you’ve assigned a LuisIntent attribute to:

[LuisIntent("Details")]
public async Task Details(IDialogContext context, 
    LuisResult result)
{
    ...
}

The LuisResult just wraps up the response from LUIS for ease of use.

Decoupling the LuisDialog

Now let’s try to implement something similar and even have a LUIS default implementation to show how it could work.

Instead of having cascading dialogs for mapping to multiple LUIS apps, I’m just assuming any dialog can potentially respond to the incoming message; i.e., no hierarchy. I’m sure you can build this in yourself if you wanted to though.

Dialog

Let’s assume you’d like to be able to mark up any IDialog with an attribute which would allow it to be mapped to an NLP intent. I would want something like this to work:

[IntentMatch("GetDetails")]
public class GetDetailDialog : IDialog
{
    ...
}

Custom IntentMatch Attribute

Given that requirement, let’s create a simple custom attribute to expose the intent which that dialog can handle:

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class IntentMatchAttribute : Attribute
{
    public IntentMatchAttribute(string intentName)
    {
        Intent = intentName;
    }
    public string Intent { get; set; }
}

This will allow us to use it as expected:

[IntentMatch("GetDetails")]
public class GetDetailDialog : IDialog

Custom Intent Matching Service

Let’s create an interface that looks like this:

public interface IIntentService
{
    Task<IntentResponse> GetIntent(string message);
}

Seems reasonable. How about that IntentResponse? This could be an acceptable structure for a base “IntentResponse” class:

public class IntentResponse
{
    public string query { get; set; }
    public Intent topScoringIntent { get; set; }
    public Intent[] intents { get; set; }
    public Entity[] entities { get; set; }

    public class Intent
    {
        public string intent { get; set; }
        public float score { get; set; }
    }

    public class Entity
    {
        public string entity { get; set; }
        public string type { get; set; }
        public float score { get; set; }
    }
}

Now let’s look at how you’d use it in an example dialog:

[Serializable]
public class IntentExampleDialog : IDialog
{
    // Using the hacky service factory approach to resolve implementations:
    // http://robinosborne.co.uk/2016/11/07/botframework-avoiding-have-to-make-everything-serializable/
    private static readonly IIntentService IntentService = 
                        ServiceResolver.Get<IIntentService>();

    // sneaky trick to get all implementations of an interface
    // (details further down..)
    private static readonly IntentDialogs = 
                ReflectionService.GetImplementers<IDialog>();                       

    public async Task StartAsync(IDialogContext context)
    {
        context.Wait(MessageReceivedAsync);
    }

    private async Task MessageReceivedAsync(IDialogContext context, 
                                    IAwaitable<IMessageActivity> result)
    {
        // get the posted message
        var message = await result;

        // call the intent service to get an intent for the message
        var intentResponse = await IntentService.GetIntent(message.Text);

        // find a matching dialog for that intent
        var dialog = 
            GetDialogWithMatchingIntent(intentResponse, IntentDialogs);

        // I'll assume we got a match..
        await context.Forward(dialog, ResumeAsync, 
                await result, new CancellationToken());
    }

    private async Task ResumeAsync(IDialogContext context, 
                                IAwaitable<object> awaitable)
    {
        context.Wait(MessageReceivedAsync);
    }

Nothing too scary so far, right? Let’s dig into that GetDialogWithMatchingIntent method to see what it might do:

private static IDialog GetDialogWithMatchingIntent(
        IntentResponse intentResponse,
        IEnumerable<IDialog> dialogs)
{

    var matchingDialogs = 

        // from all dialogs
        from d in dialogs

        // get each one's IntentMatchAttribute
        let attr = 
                (IntentMatchAttribute) 
                Attribute.GetCustomAttribute(d.GetType(), 
                    typeof(IntentMatchAttribute))

        // get the passed in top intent's value
        let intent = intentResponse.topScoringIntent?.intent

        // return the first dialog with a matching attribute
        where intent != null && attr != null && intent.Contains(attr.Intent)
        select d;

    return matchingDialogs.FirstOrDefault();
}

The key bit here is where I get the value of the IntentMatch attributes of each implementation of IDialog:

let attr = 
    (IntentMatchAttribute) 
    Attribute.GetCustomAttribute
    (d.GetType(), typeof(IntentMatchAttribute))

Aside – ReflectionService

Before I move on, let’s quickly cover the ReflectionService I mentioned back there – ReflectionService.GetImplementers<IDialog>();

The ReflectionService is a favourite little hack of mine to find all implementations of a given interface in the AppDomain or sometimes the ExecutingAssemblys, and it looks like this:

public class ReflectionService
{
    public static IEnumerable<T> GetImplementers<T>()
    {
        var type = typeof(T);

        // load all available types
        var implementers = AppDomain.CurrentDomain.GetAssemblies()
            .SelectMany(x => x.GetTypes())

            // which implement the interface
            .Where(x => type.IsAssignableFrom(x) 
                     && !x.IsInterface 
                     && !x.IsAbstract)

            // create an instance of each of them
            .Select(x => (T)Activator.CreateInstance(x));

        return implementers;
    }
}

Depending on you current project type and project structure, you might want to use the Assembly reflection to get the current, executing, entry, or specifically named assembly to load the implementations:

public static IEnumerable<T> GetImplementers<T>()
{
    var type = typeof(T);

    var implementers = 
        Assembly.GetExecutingAssembly().GetTypes()
        .Where(x => type.IsAssignableFrom(x) 
                    && !x.IsInterface 
                    && !x.IsAbstract)
        .Select(x => (T)Activator.CreateInstance(x));

    return implementers;
}

Basic “Default” IIntentService Implementation (LUIS)

Now that you can see how the Intent Service could be wired up to find the dialog with the matching intent attribute value, let’s create an example implementation for LUIS.

First up – the LUIS implementation for IIntentService:

public class LuisService : IIntentService
{
    private const Endpoint = 
        "https://api.projectoxford.ai/luis/v2.0/apps/<modelId>?subscription-key=<subscriptionKey>&amp;q={0}";

    public async Task<IntentResponse> GetIntent(string message)
    {
        using (var client = new WebClient())
        {
            // Build the endpoint
            var query = string.Format(Endpoint, message);

            // Get the response as json
            var json = await client.DownloadStringTaskAsync(new Uri(query));

            // deserialize to a dynamic object
            dynamic response = JsonConvert.DeserializeObject(json);

            // Map the dynamic object to an IntentResponse
            // (The MapToIntentResponse method has been ignored for brevity)
            return MapToIntentResponse(response);
        }
    }
}

Pretty simple implementation, right? Theoretically you’d need to implement your own MapToIntentResponse method to take the LUIS service’s json response and map it to the IntentResponse data structure – however the structure actually maps exactly to the IntentResponse I’ve used here, so no mapping is actually needed.

Summary

With the example implementations here, you can see it’s not too hard to create your own intent service that doesn’t rely on the default LuisDialog; a new attribute, a new intent service, and a bit of wiring up, and we’re almost there.

Let me know how you’re approached this problem yourself; I’d love to hear alternative solutions.

2 thoughts on “Custom BotFramework Intent Service Implementation

  1. Hi!
    Great work – it’s just what I’m looking for 🙂 I tried implementing your code, but I have some errors. Is it possible to get the project-code? I think my errors are caused by how i implement the code in a project and I dont’t know if I still need a messages controller. Hope you can help!

  2. Is there any way we can handle generic LUISIntent attribute on only one method and redirect the LUIS app response to different methods as we have to write different methods based on different intents.

Leave a Reply

Your email address will not be published. Required fields are marked *