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 LuisService
s; 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 ExecutingAssembly
s, 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>&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.
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!
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.