forked from microsoft/BotBuilder-Samples
-
Notifications
You must be signed in to change notification settings - Fork 0
/
NlpDispatchBot.cs
211 lines (188 loc) · 9.83 KB
/
NlpDispatchBot.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.AI.Luis;
using Microsoft.Bot.Builder.AI.QnA;
using Microsoft.Bot.Schema;
namespace NLP_With_Dispatch_Bot
{
/// <summary>
/// Represents a bot that processes incoming activities.
/// For each interaction from the user, an instance of this class is called.
/// This is a Transient lifetime service. Transient lifetime services are created
/// each time they're requested. For each Activity received, a new instance of this
/// class is created. Objects that are expensive to construct, or have a lifetime
/// beyond the single Turn, should be carefully managed.
/// </summary>
public class NlpDispatchBot : IBot
{
private const string WelcomeText = "This bot will introduce you to Dispatch for QnA Maker and LUIS. Type a greeting, or a question about the weather to get started";
/// <summary>
/// Key in the Bot config (.bot file) for the Home Automation Luis instance.
/// </summary>
private const string HomeAutomationLuisKey = "Home Automation";
/// <summary>
/// Key in the Bot config (.bot file) for the Weather Luis instance.
/// </summary>
private const string WeatherLuisKey = "Weather";
/// <summary>
/// Key in the Bot config (.bot file) for the Dispatch.
/// </summary>
private const string DispatchKey = "nlp-with-dispatchDispatch";
/// <summary>
/// Key in the Bot config (.bot file) for the QnaMaker instance.
/// In the .bot file, multiple instances of QnaMaker can be configured.
/// </summary>
private const string QnAMakerKey = "sample-qna";
/// <summary>
/// Services configured from the ".bot" file.
/// </summary>
private readonly BotServices _services;
/// <summary>
/// Initializes a new instance of the <see cref="NlpDispatchBot"/> class.
/// </summary>
/// <param name="services">Services configured from the ".bot" file.</param>
public NlpDispatchBot(BotServices services)
{
_services = services ?? throw new System.ArgumentNullException(nameof(services));
if (!_services.QnAServices.ContainsKey(QnAMakerKey))
{
throw new System.ArgumentException($"Invalid configuration. Please check your '.bot' file for a QnA service named '{DispatchKey}'.");
}
if (!_services.LuisServices.ContainsKey(HomeAutomationLuisKey))
{
throw new System.ArgumentException($"Invalid configuration. Please check your '.bot' file for a Luis service named '{HomeAutomationLuisKey}'.");
}
if (!_services.LuisServices.ContainsKey(WeatherLuisKey))
{
throw new System.ArgumentException($"Invalid configuration. Please check your '.bot' file for a Luis service named '{WeatherLuisKey}'.");
}
}
/// <summary>
/// Every conversation turn for our NLP Dispatch Bot will call this method.
/// There are no dialogs used, since it's "single turn" processing, meaning a single
/// request and response, with no stateful conversation.
/// </summary>
/// <param name="turnContext">A <see cref="ITurnContext"/> containing all the data needed
/// for processing this conversation turn. </param>
/// <param name="cancellationToken">(Optional) A <see cref="CancellationToken"/> that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A <see cref="Task"/> that represents the work queued to execute.</returns>
public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
if (turnContext.Activity.Type == ActivityTypes.Message && !turnContext.Responded)
{
// Get the intent recognition result
var recognizerResult = await _services.LuisServices[DispatchKey].RecognizeAsync(turnContext, cancellationToken);
var topIntent = recognizerResult?.GetTopScoringIntent();
if (topIntent == null)
{
await turnContext.SendActivityAsync("Unable to get the top intent.");
}
else
{
await DispatchToTopIntentAsync(turnContext, topIntent, cancellationToken);
}
}
else if (turnContext.Activity.Type == ActivityTypes.ConversationUpdate)
{
// Send a welcome message to the user and tell them what actions they may perform to use this bot
if (turnContext.Activity.MembersAdded != null)
{
await SendWelcomeMessageAsync(turnContext, cancellationToken);
}
}
else
{
await turnContext.SendActivityAsync($"{turnContext.Activity.Type} event detected", cancellationToken: cancellationToken);
}
}
/// <summary>
/// On a conversation update activity sent to the bot, the bot will
/// send a message to the any new user(s) that were added.
/// </summary>
/// <param name="turnContext">Provides the <see cref="ITurnContext"/> for the turn of the bot.</param>
/// <param name="cancellationToken" >(Optional) A <see cref="CancellationToken"/> that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>>A <see cref="Task"/> representing the operation result of the Turn operation.</returns>
private static async Task SendWelcomeMessageAsync(ITurnContext turnContext, CancellationToken cancellationToken)
{
foreach (var member in turnContext.Activity.MembersAdded)
{
if (member.Id != turnContext.Activity.Recipient.Id)
{
await turnContext.SendActivityAsync(
$"Welcome to Dispatch bot {member.Name}. {WelcomeText}",
cancellationToken: cancellationToken);
}
}
}
/// <summary>
/// Depending on the intent from Dispatch, routes to the right LUIS model or QnA service.
/// </summary>
private async Task DispatchToTopIntentAsync(ITurnContext context, (string intent, double score)? topIntent, CancellationToken cancellationToken = default(CancellationToken))
{
const string homeAutomationDispatchKey = "l_Home_Automation";
const string weatherDispatchKey = "l_Weather";
const string noneDispatchKey = "None";
const string qnaDispatchKey = "q_sample-qna";
switch (topIntent.Value.intent)
{
case homeAutomationDispatchKey:
await DispatchToLuisModelAsync(context, HomeAutomationLuisKey);
// Here, you can add code for calling the hypothetical home automation service, passing in any entity information that you need
break;
case weatherDispatchKey:
await DispatchToLuisModelAsync(context, WeatherLuisKey);
// Here, you can add code for calling the hypothetical weather service,
// passing in any entity information that you need
break;
case noneDispatchKey:
// You can provide logic here to handle the known None intent (none of the above).
// In this example we fall through to the QnA intent.
case qnaDispatchKey:
await DispatchToQnAMakerAsync(context, QnAMakerKey);
break;
default:
// The intent didn't match any case, so just display the recognition results.
await context.SendActivityAsync($"Dispatch intent: {topIntent.Value.intent} ({topIntent.Value.score}).");
break;
}
}
/// <summary>
/// Dispatches the turn to the request QnAMaker app.
/// </summary>
private async Task DispatchToQnAMakerAsync(ITurnContext context, string appName, CancellationToken cancellationToken = default(CancellationToken))
{
if (!string.IsNullOrEmpty(context.Activity.Text))
{
var results = await _services.QnAServices[appName].GetAnswersAsync(context);
if (results.Any())
{
await context.SendActivityAsync(results.First().Answer, cancellationToken: cancellationToken);
}
else
{
await context.SendActivityAsync($"Couldn't find an answer in the {appName}.");
}
}
}
/// <summary>
/// Dispatches the turn to the requested LUIS model.
/// </summary>
private async Task DispatchToLuisModelAsync(ITurnContext context, string appName, CancellationToken cancellationToken = default(CancellationToken))
{
await context.SendActivityAsync($"Sending your request to the {appName} system ...");
var result = await _services.LuisServices[appName].RecognizeAsync(context, cancellationToken);
await context.SendActivityAsync($"Intents detected by the {appName} app:\n\n{string.Join("\n\n", result.Intents)}");
if (result.Entities.Count > 0)
{
await context.SendActivityAsync($"The following entities were found in the message:\n\n{string.Join("\n\n", result.Entities)}");
}
}
}
}