testing: introduce ILuisService to make it possible to mock LUIS

This commit is contained in:
Will Portnoy 2016-03-26 21:22:55 -07:00
Родитель 1267f9df20
Коммит 200d76e7e6
5 изменённых файлов: 107 добавлений и 67 удалений

Просмотреть файл

@ -33,29 +33,14 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Runtime.Serialization;
using System.Threading.Tasks;
using Microsoft.Bot.Connector;
using Newtonsoft.Json;
namespace Microsoft.Bot.Builder
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class LuisModel : Attribute
{
public readonly string luisModelUrl;
public LuisModel(string luisModelUrl)
{
Field.SetNotNull(out this.luisModelUrl, nameof(luisModelUrl), luisModelUrl);
}
}
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class LuisIntent : Attribute
{
@ -67,55 +52,32 @@ namespace Microsoft.Bot.Builder
}
}
public class LuisResult
{
public Models.IntentRecommendation[] Intents { get; set; }
public Models.EntityRecommendation[] Entities { get; set; }
}
public static partial class Extensions
{
public static bool TryFindEntity(this LuisResult result, string type, out Models.EntityRecommendation entity)
{
entity = result.Entities?.FirstOrDefault(e => e.Type == type);
return entity != null;
}
}
public delegate Task IntentHandler(IDialogContext context, LuisResult luisResult);
[Serializable]
public class LuisDialog : IDialog
{
public readonly string subscriptionKey;
public readonly string modelID;
public readonly string luisUrl;
private readonly ILuisService service;
[NonSerialized]
protected Dictionary<string, IntentHandler> handlerByIntent;
public LuisDialog()
{
var luisModel = ((LuisModel)this.GetType().GetCustomAttributes(typeof(LuisModel), true).FirstOrDefault())?.luisModelUrl;
if (!string.IsNullOrEmpty(luisModel))
{
this.luisUrl = luisModel;
}
else
var type = this.GetType();
var luisModel = type.GetCustomAttribute<LuisModel>(inherit: true);
if (luisModel == null)
{
throw new Exception("Luis model attribute is not set for the class");
}
this.AddAttributeBasedHandlers();
this.service = new LuisService(luisModel);
}
public LuisDialog(string subscriptionKey, string modelID)
public LuisDialog(ILuisService service)
{
Field.SetNotNull(out this.subscriptionKey, nameof(subscriptionKey), subscriptionKey);
Field.SetNotNull(out this.modelID, nameof(modelID), modelID);
this.luisUrl = string.Format("https://api.projectoxford.ai/luis/v1/application?id={0}&subscription-key={1}&q=", this.modelID, this.subscriptionKey);
Field.SetNotNull(out this.service, nameof(service), service);
}
public virtual async Task StartAsync(IDialogContext context)
@ -123,31 +85,19 @@ namespace Microsoft.Bot.Builder
context.Wait(MessageReceived);
}
protected virtual async Task<LuisResult> GetLuisResult(string luisUrl, string text)
{
var url = luisUrl + Uri.EscapeDataString(text);
string json;
using (HttpClient client = new HttpClient())
{
json = await client.GetStringAsync(url);
}
Debug.WriteLine(json);
var response = JsonConvert.DeserializeObject<LuisResult>(json);
return response;
}
protected async Task MessageReceived(IDialogContext context, IAwaitable<Message> item)
{
var message = await item;
var luisRes = await GetLuisResult(this.luisUrl, message.Text);
var intent = luisRes.Intents.FirstOrDefault(i => i.Score == luisRes.Intents.Select(t => t.Score).Max());
if (this.handlerByIntent == null)
{
this.AddAttributeBasedHandlers();
}
var message = await item;
var luisRes = await this.service.QueryAsync(message.Text);
var maximum = luisRes.Intents.Max(t => t.Score);
var intent = luisRes.Intents.FirstOrDefault(i => i.Score == maximum);
IntentHandler handler;
if (intent == null || !this.handlerByIntent.TryGetValue(intent.Intent, out handler))
{
@ -160,7 +110,7 @@ namespace Microsoft.Bot.Builder
}
else
{
var text = $"LuisModel[{this.modelID}] no default intent handler.";
var text = $"No default intent handler found.";
throw new Exception(text);
}
}

Просмотреть файл

@ -0,0 +1,89 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web;
using Newtonsoft.Json;
namespace Microsoft.Bot.Builder
{
public class LuisResult
{
public Models.IntentRecommendation[] Intents { get; set; }
public Models.EntityRecommendation[] Entities { get; set; }
}
public interface ILuisService
{
Uri BuildUri(string text);
Task<LuisResult> QueryAsync(Uri uri);
}
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
[Serializable]
public class LuisModel : Attribute
{
public readonly string ModelID;
public readonly string SubscriptionKey;
public LuisModel(string modelID, string subscriptionKey)
{
Field.SetNotNull(out this.ModelID, nameof(modelID), modelID);
Field.SetNotNull(out this.SubscriptionKey, nameof(subscriptionKey), subscriptionKey);
}
}
[Serializable]
public sealed class LuisService : ILuisService
{
private readonly LuisModel model;
public LuisService(LuisModel model)
{
Field.SetNotNull(out this.model, nameof(model), model);
}
public static readonly Uri UriBase = new Uri("https://api.projectoxford.ai/luis/v1/application");
Uri ILuisService.BuildUri(string text)
{
var queryString = HttpUtility.ParseQueryString(string.Empty);
queryString["id"] = this.model.ModelID;
queryString["subscription-key"] = this.model.SubscriptionKey;
queryString["q"] = text;
var builder = new UriBuilder(UriBase);
builder.Query = queryString.ToString();
return builder.Uri;
}
async Task<LuisResult> ILuisService.QueryAsync(Uri uri)
{
string json;
using (HttpClient client = new HttpClient())
{
json = await client.GetStringAsync(uri);
}
var result = JsonConvert.DeserializeObject<LuisResult>(json);
return result;
}
}
public static partial class Extensions
{
public static async Task<LuisResult> QueryAsync(this ILuisService service, string text)
{
var uri = service.BuildUri(text);
return await service.QueryAsync(uri);
}
public static bool TryFindEntity(this LuisResult result, string type, out Models.EntityRecommendation entity)
{
entity = result.Entities?.FirstOrDefault(e => e.Type == type);
return entity != null;
}
}
}

Просмотреть файл

@ -91,6 +91,7 @@
<Compile Include="Dialogs\IDialog.cs" />
<Compile Include="Dialogs\IDialogContext.cs" />
<Compile Include="Dialogs\LuisDialog.cs" />
<Compile Include="Dialogs\LuisService.cs" />
<Compile Include="Dialogs\PromptDialog.cs" />
<Compile Include="Extensions\Extensions.cs" />
<Compile Include="Fibers\Awaitable.cs" />
@ -140,7 +141,7 @@
</PropertyGroup>
<Error Condition="!Exists('..\packages\Microsoft.Net.Compilers.1.1.1\build\Microsoft.Net.Compilers.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Net.Compilers.1.1.1\build\Microsoft.Net.Compilers.props'))" />
</Target>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>

Просмотреть файл

@ -11,7 +11,7 @@ using System.Web;
namespace Microsoft.Bot.Sample.PizzaBot
{
[LuisModel("https://api.projectoxford.ai/luis/v1/application?id=a19f7eee-0280-4a9a-b5e5-73c16b32c43d&subscription-key=fe054e042fd14754a83f0a205f6552a5&q=")]
[LuisModel("a19f7eee-0280-4a9a-b5e5-73c16b32c43d", "fe054e042fd14754a83f0a205f6552a5")]
[Serializable]
public class PizzaOrderDialog : LuisDialog
{

Просмотреть файл

@ -10,7 +10,7 @@ using Newtonsoft.Json.Linq;
namespace Microsoft.Bot.Sample.SimpleAlarmBot
{
[LuisModel("https://api.projectoxford.ai/luis/v1/application?id=c413b2ef-382c-45bd-8ff0-f76d60e2a821&subscription-key=fe054e042fd14754a83f0a205f6552a5&q=")]
[LuisModel("c413b2ef-382c-45bd-8ff0-f76d60e2a821", "fe054e042fd14754a83f0a205f6552a5")]
[Serializable]
public class SimpleAlarmBot : LuisDialog, ISerializable
{