-
Notifications
You must be signed in to change notification settings - Fork 0
/
PictureBot.cs
372 lines (339 loc) · 19.6 KB
/
PictureBot.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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Schema;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Extensions.Logging;
using System.Linq;
using PictureBot.Models;
using PictureBot.Responses;
using Microsoft.Bot.Builder.AI.Luis;
using Microsoft.Azure.Search;
using Microsoft.Azure.Search.Models;
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Luis;
using Microsoft.Extensions.Options;
namespace Microsoft.PictureBot
{
/// <summary>
/// Represents a bot that processes incoming activities.
/// For each user interaction, an instance of this class is created and the OnTurnAsync method 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.
/// For example, the <see cref="MemoryStorage"/> object and associated
/// <see cref="IStatePropertyAccessor{T}"/> object are created with a singleton lifetime.
/// </summary>
/// <seealso cref="https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-2.1"/>
/// <summary>Contains the set of dialogs and prompts for the picture bot.</summary>
public class PictureBot : IBot
{
/// <summary>
/// Services configured from the ".bot" file.
/// </summary>
private readonly BotServices _services;
/// <summary>
/// Key in the bot config (.bot file) for the LUIS instance.
/// In the .bot file, multiple instances of LUIS can be configured.
/// </summary>
public static readonly string LuisPictureBot = "PictureBot";
private readonly PictureBotAccessors _accessors;
private readonly ILogger _logger;
private DialogSet _dialogs;
/// <summary>
/// Initializes a new instance of the <see cref="PictureBot"/> class.
/// </summary>
/// <param name="accessors">A class containing <see cref="IStatePropertyAccessor{T}"/> used to manage state.</param>
/// <param name="loggerFactory">A <see cref="ILoggerFactory"/> that is hooked to the Azure App Service provider.</param>
/// <seealso cref="https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-2.1#windows-eventlog-provider"/>
public PictureBot(PictureBotAccessors accessors, ILoggerFactory loggerFactory, BotServices services)
{
_services = services ?? throw new System.ArgumentNullException(nameof(services));
// Make sure we have our required LUIS models
if (!_services.LuisServices.ContainsKey(LuisPictureBot))
{
throw new System.ArgumentException($"Invalid configuration. Please check your '.bot' file for a LUIS service named '{LuisPictureBot}'.");
}
if (loggerFactory == null)
{
throw new System.ArgumentNullException(nameof(loggerFactory));
}
_logger = loggerFactory.CreateLogger<PictureBot>();
_logger.LogTrace("PictureBot turn start.");
_accessors = accessors ?? throw new System.ArgumentNullException(nameof(accessors));
// The DialogSet needs a DialogState accessor, it will call it when it has a turn context.
_dialogs = new DialogSet(_accessors.DialogStateAccessor);
// This array defines how the Waterfall will execute.
// We can define the different dialogs and their steps here
// allowing for overlap as needed. In this case, it's fairly simple
// but in more complex scenarios, you may want to separate out the different
// dialogs into different files.
var main_waterfallsteps = new WaterfallStep[]
{
GreetingAsync,
MainMenuAsync,
};
var search_waterfallsteps = new WaterfallStep[]
{
// Add SearchDialog water fall steps
SearchRequestAsync,
SearchAsync
};
// Add named dialogs to the DialogSet. These names are saved in the dialog state.
_dialogs.Add(new WaterfallDialog("mainDialog", main_waterfallsteps));
_dialogs.Add(new WaterfallDialog("searchDialog", search_waterfallsteps));
// The following line allows us to use a prompt within the dialogs
_dialogs.Add(new TextPrompt("searchPrompt"));
}
/// <summary>
/// Every conversation turn for our PictureBot will call this method.
/// There are no dialogs used, since it's "single turn" processing, meaning a single
/// request and response. Later, when we add Dialogs, we'll have to navigate through this method.
/// </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>
/// <seealso cref="BotStateSet"/>
/// <seealso cref="ConversationState"/>
/// <seealso cref="IMiddleware"/>
public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
var activity = turnContext.Activity;
// Create a dialog context
var dc = await _dialogs.CreateContextAsync(turnContext);
if (activity.Type is "message")
{
// Continue any current dialog.
var results = await dc.ContinueDialogAsync(cancellationToken);
// Every turn sends a response, so if no response was sent,
// then there no dialog is currently active.
if (!turnContext.Responded)
{
// Start the main dialog
await dc.BeginDialogAsync("mainDialog", null, cancellationToken);
}
}
else if (activity.Type == ActivityTypes.ConversationUpdate)
{
if (turnContext.Activity.MembersAdded != null)
{
// Iterate over all new members added to the conversation.
foreach (var member in turnContext.Activity.MembersAdded)
{
// Greet anyone that was not the target (recipient) of this message.
if (member.Id != turnContext.Activity.Recipient.Id)
{
// Start the main dialog
await dc.BeginDialogAsync("mainDialog", null, cancellationToken);
}
}
}
}
}
// Add MainDialog-related tasks
// If we haven't greeted a user yet, we want to do that first, but for the rest of the
// conversation we want to remember that we've already greeted them.
private async Task<DialogTurnResult> GreetingAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
// Get the state for the current step in the conversation
var state = await _accessors.PictureState.GetAsync(stepContext.Context, () => new PictureState());
// If we haven't greeted the user
if (state.Greeted == "not greeted")
{
// Greet the user
await MainResponses.ReplyWithGreeting(stepContext.Context);
// Update the GreetedState to greeted
state.Greeted = "greeted";
// Save the new greeted state into the conversation state
// This is to ensure in future turns we do not greet the user again
await _accessors.ConversationState.SaveChangesAsync(stepContext.Context);
// Ask the user what they want to do next
await MainResponses.ReplyWithHelp(stepContext.Context);
// Since we aren't explicitly prompting the user in this step, we'll end the dialog
// When the user replies, since state is maintained, the else clause will move them
// to the next waterfall step
return await stepContext.EndDialogAsync();
}
else // We've already greeted the user
{
// Move to the next waterfall step, which is MainMenuAsync
return await stepContext.NextAsync();
}
}
// This step routes the user to different dialogs
// In this case, there's only one other dialog, so it is more simple,
// but in more complex scenarios you can go off to other dialogs in a similar
public async Task<DialogTurnResult> MainMenuAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
// Check if we are currently processing a user's search
var state = await _accessors.PictureState.GetAsync(stepContext.Context);
// If Regex picks up on anything, store it
var recognizedIntents = stepContext.Context.TurnState.Get<IRecognizedIntents>();
// Based on the recognized intent, direct the conversation
switch (recognizedIntents.TopIntent?.Name)
{
case "search":
// switch to the search dialog
return await stepContext.BeginDialogAsync("searchDialog", null, cancellationToken);
case "share":
// respond that you're sharing the photo
await MainResponses.ReplyWithShareConfirmation(stepContext.Context);
return await stepContext.EndDialogAsync();
case "order":
// respond that you're ordering
await MainResponses.ReplyWithOrderConfirmation(stepContext.Context);
return await stepContext.EndDialogAsync();
case "help":
// show help
await MainResponses.ReplyWithHelp(stepContext.Context);
return await stepContext.EndDialogAsync();
default:
{
// Call LUIS recognizer
var recognizerResult = await _services.LuisServices[LuisPictureBot].RecognizeAsync<LUISPicture>(stepContext.Context, cancellationToken);
//var result = await _recognizer.RecognizeAsync(stepContext.Context, cancellationToken);
// Get the top intent from the results
//var topIntent = recognizerResult?.GetTopScoringIntent();
var topIntent = recognizerResult.TopIntent().intent;
var topIntentScore = recognizerResult.TopIntent().score;
// Based on the intent, switch the conversation, similar concept as with Regex above
switch (topIntent)
{
case LUISPicture.Intent.None:
await MainResponses.ReplyWithConfused(stepContext.Context);
// with each statement, we're adding the LuisScore, purely to test, so we know whether LUIS was called or not
await MainResponses.ReplyWithLuisScore(stepContext.Context, topIntent.ToString(), topIntentScore);
break;
case LUISPicture.Intent.Greeting:
await MainResponses.ReplyWithGreeting(stepContext.Context);
await MainResponses.ReplyWithHelp(stepContext.Context);
await MainResponses.ReplyWithLuisScore(stepContext.Context, topIntent.ToString(), topIntentScore);
break;
case LUISPicture.Intent.OrderPic:
await MainResponses.ReplyWithOrderConfirmation(stepContext.Context);
await MainResponses.ReplyWithLuisScore(stepContext.Context, topIntent.ToString(), topIntentScore);
break;
case LUISPicture.Intent.SharePic:
await MainResponses.ReplyWithShareConfirmation(stepContext.Context);
await MainResponses.ReplyWithLuisScore(stepContext.Context, topIntent.ToString(), topIntentScore);
break;
case LUISPicture.Intent.SearchPics:
// Check if LUIS has identified the search term that we should look for.
// Note: you should have stored the search term as "facet", but if you did not,
// you will need to update.
var entity = recognizerResult?.Entities;
var obj = JObject.Parse(JsonConvert.SerializeObject(entity)).SelectToken("facet");
// If entities are picked up on by LUIS, store them in state.Search
// Also, update state.Searching to "yes", so you don't ask the user
// what they want to search for, they've already told you
if (obj != null)
{
// format "facet", update state, and save save
state.Search = obj.ToString().Replace("\"", "").Trim(']', '[').Trim();
state.Searching = "yes";
await _accessors.ConversationState.SaveChangesAsync(stepContext.Context);
}
// Begin the search dialog
await MainResponses.ReplyWithLuisScore(stepContext.Context, topIntent.ToString(), topIntentScore);
return await stepContext.BeginDialogAsync("searchDialog", null, cancellationToken);
default:
await MainResponses.ReplyWithConfused(stepContext.Context);
break;
}
return await stepContext.EndDialogAsync();
}
}
}
// Add SearchDialog-related tasks
private async Task<DialogTurnResult> SearchRequestAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
// Check if a user has already started searching, and if you know what to search for
var state = await _accessors.PictureState.GetAsync(stepContext.Context);
// If they're just starting to search for photos
if (state.Searching == "no")
{
// Update the searching state
state.Searching = "yes";
// Save the new state into the conversation state.
await _accessors.ConversationState.SaveChangesAsync(stepContext.Context);
// Prompt the user for what they want to search for.
// Instead of using SearchResponses.ReplyWithSearchRequest,
// we're experimenting with using text prompts
return await stepContext.PromptAsync("searchPrompt", new PromptOptions { Prompt = MessageFactory.Text("What would you like to search for?") }, cancellationToken);
}
else // This means they just told us what they want to search for
// Go to the next step in the dialog, which is "SearchAsync"
return await stepContext.NextAsync();
}
private async Task<DialogTurnResult> SearchAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
// Add state so we can update it throughout the turn
var state = await _accessors.PictureState.GetAsync(stepContext.Context);
// If we haven't stored what they want to search for
if (state.Search == "")
{
// Store it and update the ConversationState
state.Search = (string)stepContext.Result;
await _accessors.ConversationState.SaveChangesAsync(stepContext.Context);
}
var searchText = state.Search;
// Confirm with the user what you're searching for
await SearchResponses.ReplyWithSearchConfirmation(stepContext.Context, searchText);
// Process the search request and send the results to the user
await StartAsync(stepContext.Context, searchText);
// Clear out Search or future searches, set the searching state to no,
// update the conversation state
state.Search = "";
state.Searching = "no";
await _accessors.ConversationState.SaveChangesAsync(stepContext.Context);
return await stepContext.EndDialogAsync();
}
// Lab 2.2.2 Add search related tasks
public async Task StartAsync(ITurnContext context, string searchText)
{
ISearchIndexClient indexClientForQueries = CreateSearchIndexClient();
// For more examples of calling search with SearchParameters, see
// https://github.com/Azure-Samples/search-dotnet-getting-started/blob/master/DotNetHowTo/DotNetHowTo/Program.cs.
// Call the search service and store the results
DocumentSearchResult results = await indexClientForQueries.Documents.SearchAsync(searchText);
await SendResultsAsync(context, searchText, results);
}
public async Task SendResultsAsync(ITurnContext context, string searchText, DocumentSearchResult results)
{
IMessageActivity activity = context.Activity.CreateReply();
// if the search returns no results
if (results.Results.Count == 0)
{
await SearchResponses.ReplyWithNoResults(context, searchText);
}
else // this means there was at least one hit for the search
{
// create the response with the result(s) and send to the user
SearchHitStyler searchHitStyler = new SearchHitStyler();
searchHitStyler.Apply(
ref activity,
"Here are the results that I found:",
results.Results.Select(r => ImageMapper.ToSearchHit(r)).ToList().AsReadOnly());
await context.SendActivityAsync(activity);
}
}
public ISearchIndexClient CreateSearchIndexClient()
{
// Configure the search service and establish a connection, call it in StartAsync()
// replace "YourSearchServiceName" and "YourSearchServiceKey" with your search service values
string searchServiceName = "xxxxx";
string queryApiKey = "xxxxxxx";
// if you named your index "images" as instructed, you do not need to change this value
string indexName = "images";
SearchIndexClient indexClient = new SearchIndexClient(searchServiceName, indexName, new SearchCredentials(queryApiKey));
return indexClient;
}
}
}