LuisDialog.cs
1 //
2 // Copyright (c) Microsoft. All rights reserved.
3 // Licensed under the MIT license.
4 //
5 // Microsoft Bot Framework: http://botframework.com
6 //
7 // Bot Builder SDK GitHub:
8 // https://github.com/Microsoft/BotBuilder
9 //
10 // Copyright (c) Microsoft Corporation
11 // All rights reserved.
12 //
13 // MIT License:
14 // Permission is hereby granted, free of charge, to any person obtaining
15 // a copy of this software and associated documentation files (the
16 // "Software"), to deal in the Software without restriction, including
17 // without limitation the rights to use, copy, modify, merge, publish,
18 // distribute, sublicense, and/or sell copies of the Software, and to
19 // permit persons to whom the Software is furnished to do so, subject to
20 // the following conditions:
21 //
22 // The above copyright notice and this permission notice shall be
23 // included in all copies or substantial portions of the Software.
24 //
25 // THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
26 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
27 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
28 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
29 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
30 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
31 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
32 //
33 
34 using System;
35 using System.Collections.Generic;
36 using System.Linq;
37 using System.Reflection;
38 using System.Runtime.Serialization;
39 using System.Threading;
40 using System.Threading.Tasks;
41 
42 using Microsoft.Bot.Connector;
48 
49 namespace Microsoft.Bot.Builder.Dialogs
50 {
54  [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
55  [Serializable]
57  {
61  public readonly string IntentName;
62 
67  public LuisIntentAttribute(string intentName)
68  {
69  SetField.NotNull(out this.IntentName, nameof(intentName), intentName);
70  }
71 
72  protected override string Text
73  {
74  get
75  {
76  return this.IntentName;
77  }
78  }
79  }
80 
87  public delegate Task IntentHandler(IDialogContext context, LuisResult luisResult);
88 
96  public delegate Task IntentActivityHandler(IDialogContext context, IAwaitable<IMessageActivity> message, LuisResult luisResult);
97 
101  [Serializable]
102  public sealed class InvalidIntentHandlerException : InvalidOperationException
103  {
104  public readonly MethodInfo Method;
105 
106  public InvalidIntentHandlerException(string message, MethodInfo method)
107  : base(message)
108  {
109  SetField.NotNull(out this.Method, nameof(method), method);
110  }
111 
112  private InvalidIntentHandlerException(SerializationInfo info, StreamingContext context)
113  : base(info, context)
114  {
115  }
116  }
117 
122  public class LuisServiceResult
123  {
125  {
126  this.Result = result;
127  this.BestIntent = intent;
128  this.LuisService = service;
129  }
130 
131  public LuisResult Result { get; }
132 
133  public IntentRecommendation BestIntent { get; }
134 
135  public ILuisService LuisService { get; }
136  }
137 
142  [Serializable]
143  public class LuisDialog<TResult> : IDialog<TResult>
144  {
145  protected readonly IReadOnlyList<ILuisService> services;
146 
148  [NonSerialized]
149  protected Dictionary<string, IntentActivityHandler> handlerByIntent;
150 
152  {
153  var type = this.GetType();
154  var luisModels = type.GetCustomAttributes<LuisModelAttribute>(inherit: true);
155  return luisModels.Select(m => new LuisService(m)).Cast<ILuisService>().ToArray();
156  }
157 
162  public LuisDialog(params ILuisService[] services)
163  {
164  if (services.Length == 0)
165  {
166  services = MakeServicesFromAttributes();
167  }
168 
169  SetField.NotNull(out this.services, nameof(services), services);
170  }
171 
172  public virtual async Task StartAsync(IDialogContext context)
173  {
174  context.Wait(MessageReceived);
175  }
176 
183  {
184  return result.TopScoringIntent ?? result.Intents?.MaxBy(i => i.Score ?? 0d);
185  }
186 
194  protected virtual LuisServiceResult BestResultFrom(IEnumerable<LuisServiceResult> results)
195  {
196  return results.MaxBy(i => i.BestIntent.Score ?? 0d);
197  }
198 
199  protected virtual async Task MessageReceived(IDialogContext context, IAwaitable<IMessageActivity> item)
200  {
201  var message = await item;
202  var messageText = await GetLuisQueryTextAsync(context, message);
203 
204  var tasks = this.services.Select(s => s.QueryAsync(messageText, context.CancellationToken)).ToArray();
205  var results = await Task.WhenAll(tasks);
206 
207  var winners = from result in results.Select((value, index) => new {value, index} )
208  let resultWinner = BestIntentFrom(result.value)
209  where resultWinner != null
210  select new LuisServiceResult(result.value, resultWinner, this.services[result.index]);
211 
212  var winner = this.BestResultFrom(winners);
213 
214  if (winner == null)
215  {
216  throw new InvalidOperationException("No winning intent selected from Luis results.");
217  }
218 
219  if (winner.Result.Dialog?.Status == DialogResponse.DialogStatus.Question)
220  {
221  var childDialog = await MakeLuisActionDialog(winner.LuisService,
222  winner.Result.Dialog.ContextId,
223  winner.Result.Dialog.Prompt);
224  context.Call(childDialog, LuisActionDialogFinished);
225  }
226  else
227  {
228  await DispatchToIntentHandler(context, item, winner.BestIntent, winner.Result);
229  }
230  }
231 
232  protected virtual async Task DispatchToIntentHandler(IDialogContext context,
234  IntentRecommendation bestInent,
235  LuisResult result)
236  {
237  if (this.handlerByIntent == null)
238  {
239  this.handlerByIntent = new Dictionary<string, IntentActivityHandler>(GetHandlersByIntent());
240  }
241 
242  IntentActivityHandler handler = null;
243  if (result == null || !this.handlerByIntent.TryGetValue(bestInent.Intent, out handler))
244  {
245  handler = this.handlerByIntent[string.Empty];
246  }
247 
248  if (handler != null)
249  {
250  await handler(context, item, result);
251  }
252  else
253  {
254  var text = $"No default intent handler found.";
255  throw new Exception(text);
256  }
257  }
258 
259  protected virtual Task<string> GetLuisQueryTextAsync(IDialogContext context, IMessageActivity message)
260  {
261  return Task.FromResult(message.Text);
262  }
263 
264  protected virtual IDictionary<string, IntentActivityHandler> GetHandlersByIntent()
265  {
266  return LuisDialog.EnumerateHandlers(this).ToDictionary(kv => kv.Key, kv => kv.Value);
267  }
268 
269  protected virtual async Task<IDialog<LuisResult>> MakeLuisActionDialog(ILuisService luisService, string contextId, string prompt)
270  {
271  return new LuisActionDialog(luisService, contextId, prompt);
272  }
273 
274  protected virtual async Task LuisActionDialogFinished(IDialogContext context, IAwaitable<LuisResult> item)
275  {
276  var result = await item;
277  var messageActivity = (IMessageActivity) context.Activity;
278  await DispatchToIntentHandler(context, Awaitable.FromItem(messageActivity), BestIntentFrom(result), result);
279  }
280  }
281 
285  [Serializable]
286  public class LuisActionDialog : IDialog<LuisResult>
287  {
288  private readonly ILuisService luisService;
289  private string contextId;
290  private string prompt;
291 
298  public LuisActionDialog(ILuisService luisService, string contextId, string prompt)
299  {
300  SetField.NotNull(out this.luisService, nameof(luisService), luisService);
301  SetField.NotNull(out this.contextId, nameof(contextId), contextId);
302  this.prompt = prompt;
303  }
304 
305 
306  public virtual async Task StartAsync(IDialogContext context)
307  {
308  await context.PostAsync(this.prompt);
309  context.Wait(MessageReceivedAsync);
310  }
311 
312  protected virtual async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> item)
313  {
314  var message = await item;
315  var luisRequest = new LuisRequest(query: message.Text, contextId: this.contextId);
316  var result = await luisService.QueryAsync(luisService.BuildUri(luisRequest), context.CancellationToken);
317  if (result.Dialog.Status != DialogResponse.DialogStatus.Finished)
318  {
319  this.contextId = result.Dialog.ContextId;
320  this.prompt = result.Dialog.Prompt;
321  await context.PostAsync(this.prompt);
322  context.Wait(MessageReceivedAsync);
323  }
324  else
325  {
326  context.Done(result);
327  }
328  }
329  }
330 
331  internal static class LuisDialog
332  {
338  public static IEnumerable<KeyValuePair<string, IntentActivityHandler>> EnumerateHandlers(object dialog)
339  {
340  var type = dialog.GetType();
341  var methods = type.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
342  foreach (var method in methods)
343  {
344  var intents = method.GetCustomAttributes<LuisIntentAttribute>(inherit: true).ToArray();
345  IntentActivityHandler intentHandler = null;
346 
347  try
348  {
349  intentHandler = (IntentActivityHandler)Delegate.CreateDelegate(typeof(IntentActivityHandler), dialog, method, throwOnBindFailure: false);
350  }
351  catch (ArgumentException)
352  {
353  // "Cannot bind to the target method because its signature or security transparency is not compatible with that of the delegate type."
354  // https://github.com/Microsoft/BotBuilder/issues/634
355  // https://github.com/Microsoft/BotBuilder/issues/435
356  }
357 
358  // fall back for compatibility
359  if (intentHandler == null)
360  {
361  try
362  {
363  var handler = (IntentHandler)Delegate.CreateDelegate(typeof(IntentHandler), dialog, method, throwOnBindFailure: false);
364 
365  if (handler != null)
366  {
367  // thunk from new to old delegate type
368  intentHandler = (context, message, result) => handler(context, result);
369  }
370  }
371  catch (ArgumentException)
372  {
373  // "Cannot bind to the target method because its signature or security transparency is not compatible with that of the delegate type."
374  // https://github.com/Microsoft/BotBuilder/issues/634
375  // https://github.com/Microsoft/BotBuilder/issues/435
376  }
377  }
378 
379  if (intentHandler != null)
380  {
381  var intentNames = intents.Select(i => i.IntentName).DefaultIfEmpty(method.Name);
382 
383  foreach (var intentName in intentNames)
384  {
385  var key = string.IsNullOrWhiteSpace(intentName) ? string.Empty : intentName;
386  yield return new KeyValuePair<string, IntentActivityHandler>(intentName, intentHandler);
387  }
388  }
389  else
390  {
391  if (intents.Length > 0)
392  {
393  throw new InvalidIntentHandlerException(string.Join(";", intents.Select(i => i.IntentName)), method);
394  }
395  }
396  }
397  }
398  }
399 }
A mockable interface for the LUIS service.
Definition: LuisService.cs:169
virtual LuisServiceResult BestResultFrom(IEnumerable< LuisServiceResult > results)
Calculates the best scored LuisServiceResult across multiple LuisServiceResult returned by different ...
Definition: LuisDialog.cs:194
The context for the execution of a dialog&#39;s conversational process.
const string Question
Send the prompt in DialogResponse.Prompt
virtual async Task DispatchToIntentHandler(IDialogContext context, IAwaitable< IMessageActivity > item, IntentRecommendation bestInent, LuisResult result)
Definition: LuisDialog.cs:232
virtual IntentRecommendation BestIntentFrom(LuisResult result)
Calculates the best scored IntentRecommendation from a LuisResult.
Definition: LuisDialog.cs:182
delegate Task IntentActivityHandler(IDialogContext context, IAwaitable< IMessageActivity > message, LuisResult luisResult)
The handler for a LUIS intent.
Namespace for the Microsoft Bot Connector SDK.
Namespace for models generated from the http://luis.ai REST API.
Definition: FormDialog.cs:837
string Text
Content for the message
Explicit interface to support the compiling of async/await.
Definition: Awaitable.cs:55
Namespace for internal scorable implementation that is not useful for most developers and may change ...
virtual async Task StartAsync(IDialogContext context)
Definition: LuisDialog.cs:172
Namespace for scorable interfaces, classes and compositions.
Namespace for internal machinery that is not useful for most developers and may change in the future...
A dialog specialized to handle intents and entities from LUIS.
Definition: LuisDialog.cs:143
A IDialog<TResult> is a suspendable conversational process that produces a result of type TResult ...
Definition: IDialog.cs:47
virtual async Task< IDialog< LuisResult > > MakeLuisActionDialog(ILuisService luisService, string contextId, string prompt)
Definition: LuisDialog.cs:269
LuisDialog(params ILuisService[] services)
Construct the LUIS dialog.
Definition: LuisDialog.cs:162
readonly IReadOnlyList< ILuisService > services
Definition: LuisDialog.cs:145
virtual Task< string > GetLuisQueryTextAsync(IDialogContext context, IMessageActivity message)
Definition: LuisDialog.cs:259
string Intent
The LUIS intent detected by LUIS service in response to a query.
Task PostAsync(IMessageActivity message, CancellationToken cancellationToken=default(CancellationToken))
Post a message to be sent to the user.
virtual async Task MessageReceivedAsync(IDialogContext context, IAwaitable< IMessageActivity > item)
Definition: LuisDialog.cs:312
The dialog wrapping Luis dialog feature.
Definition: LuisDialog.cs:286
LUIS intent recommendation. Look at https://www.luis.ai/Help for more information.
Definition: FormDialog.cs:845
The LUIS model information.
Definition: LuisModel.cs:81
Namespace for internal Dialogs machinery that is not useful for most developers and may change in the...
Standard implementation of ILuisService against actual LUIS service.
Definition: LuisService.cs:191
LuisIntentAttribute(string intentName)
Construct the association between the LUIS intent and a dialog method.
Definition: LuisDialog.cs:67
static IEnumerable< KeyValuePair< string, IntentActivityHandler > > EnumerateHandlers(object dialog)
Enumerate the handlers based on the attributes on the dialog instance.
Definition: LuisDialog.cs:338
Matches a LuisResult object with the best scored IntentRecommendation of the LuisResult and correspon...
Definition: LuisDialog.cs:122
Associate a LUIS intent with a dialog method.
Definition: LuisDialog.cs:56
readonly string IntentName
The LUIS intent name.
Definition: LuisDialog.cs:61
IActivity Activity
The activity posted to bot.
LuisActionDialog(ILuisService luisService, string contextId, string prompt)
Creates an instance of LuisActionDialog.
Definition: LuisDialog.cs:298
virtual async Task MessageReceived(IDialogContext context, IAwaitable< IMessageActivity > item)
Definition: LuisDialog.cs:199
virtual async Task LuisActionDialogFinished(IDialogContext context, IAwaitable< LuisResult > item)
Definition: LuisDialog.cs:274
An exception for invalid intent handlers.
Definition: LuisDialog.cs:102
IList< IntentRecommendation > Intents
The intents found in the query text.
Definition: LuisResult.cs:54
Namespace for the internal fibers machinery that is not useful for most developers and may change in ...
Core namespace for Dialogs and associated infrastructure.
virtual IDictionary< string, IntentActivityHandler > GetHandlersByIntent()
Definition: LuisDialog.cs:264
Namespace for the machinery needed to talk to http://luis.ai.
delegate Task IntentHandler(IDialogContext context, LuisResult luisResult)
The handler for a LUIS intent.
LuisServiceResult(LuisResult result, IntentRecommendation intent, ILuisService service)
Definition: LuisDialog.cs:124
Object that contains all the possible parameters to build Luis request.
Definition: LuisService.cs:49
virtual async Task StartAsync(IDialogContext context)
Definition: LuisDialog.cs:306
Root namespace for the Microsoft Bot Builder SDK.
Dictionary< string, IntentActivityHandler > handlerByIntent
Mapping from intent string to the appropriate handler.
Definition: LuisDialog.cs:149
InvalidIntentHandlerException(string message, MethodInfo method)
Definition: LuisDialog.cs:106