This repository was archived by the owner on Jan 15, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 84
/
Copy pathBatchRegexInput.cs
308 lines (264 loc) · 13.5 KB
/
BatchRegexInput.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
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Security.Claims;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using AdaptiveExpressions.Properties;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.Dialogs.Adaptive;
using Microsoft.Bot.Components.Telephony.Common;
using Microsoft.Bot.Schema;
using Newtonsoft.Json;
namespace Microsoft.Bot.Components.Telephony.Actions
{
/// <summary>
/// Aggregates input until it matches a regex pattern and then stores the result in an output property.
/// </summary>
public class BatchRegexInput : Dialog
{
[JsonProperty("$kind")]
public const string Kind = "Microsoft.Telephony.BatchRegexInput";
protected const string AggregationDialogMemory = "this.aggregation";
private const string TimerId = "this.TimerId";
private static IStateMatrix stateMatrix = new LatchingStateMatrix();
/// <summary>
/// Initializes a new instance of the <see cref="BatchRegexInput"/> class.
/// </summary>
/// <param name="sourceFilePath">Optional, source file full path.</param>
/// <param name="sourceLineNumber">Optional, line number in source file.</param>
[JsonConstructor]
public BatchRegexInput([CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
: base()
{
// enable instances of this command as debug break point
this.RegisterSourceLocation(sourceFilePath, sourceLineNumber);
}
/// <summary>
/// Gets or sets interruption policy.
/// </summary>
/// <value>
/// Bool or expression which evalutes to bool.
/// </value>
[JsonProperty("allowInterruptions")]
public BoolExpression AllowInterruptions { get; set; } = false;
/// <summary>
/// Gets or sets the regex pattern to use to decide when the dialog has aggregated the whole message.
/// </summary>
[JsonProperty("terminationConditionRegexPattern")]
public StringExpression TerminationConditionRegexPattern { get; set; }
/// <summary>
/// Gets or sets the property to assign the result to.
/// </summary>
[JsonProperty("property")]
public StringExpression Property { get; set; }
/// <summary>
/// Gets or sets the activity to send to the user.
/// </summary>
/// <value>
/// An activity template.
/// </value>
[JsonProperty("prompt")]
public ITemplate<Activity> Prompt { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the input should always prompt the user regardless of there being a value or not.
/// </summary>
/// <value>
/// Bool or expression which evaluates to bool.
/// </value>
[JsonProperty("alwaysPrompt")]
public BoolExpression AlwaysPrompt { get; set; }
/// <summary>
/// Gets or sets a regex describing input that should be flagged as handled and not bubble.
/// </summary>
/// <value>
/// String or expression which evaluates to string.
/// </value>
[JsonProperty("interruptionMask")]
public StringExpression InterruptionMask { get; set; }
/// <summary>
/// Gets or sets a value indicating how long to wait for before timing out and using the default value.
/// </summary>
[JsonProperty("timeOutInMilliseconds")]
public IntExpression TimeOutInMilliseconds { get; set; }
/// <summary>
/// Gets or sets the default value for the input dialog when a Timeout is reached.
/// </summary>
/// <value>
/// Value or expression which evaluates to a value.
/// </value>
[JsonProperty("defaultValue")]
public ValueExpression DefaultValue { get; set; }
/// <summary>
/// Gets or sets the activity template to send when a Timeout is reached and the default value is used.
/// </summary>
/// <value>
/// An activity template.
/// </value>
[JsonProperty("defaultValueResponse")]
public ITemplate<Activity> DefaultValueResponse { get; set; }
/// <inheritdoc/>
public override async Task<DialogTurnResult> BeginDialogAsync(DialogContext dc, object options = null, CancellationToken cancellationToken = default(CancellationToken))
{
//start a timer that will continue this conversation
await InitTimeoutTimerAsync(dc, cancellationToken).ConfigureAwait(false);
return await PromptUserAsync(dc, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc/>
public override async Task<DialogTurnResult> ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = default)
{
//Check if we were interrupted. If we were, follow our logic for when we get interrupted.
bool wasInterrupted = dc.State.GetValue(TurnPath.Interrupted, () => false);
if (wasInterrupted)
{
return await PromptUserAsync(dc, cancellationToken).ConfigureAwait(false);
}
//Get value of termination string from expression
string regexPattern = this.TerminationConditionRegexPattern?.GetValue(dc.State);
//append the message to the aggregation memory state
var existingAggregation = dc.State.GetValue(AggregationDialogMemory, () => string.Empty);
existingAggregation += dc.Context.Activity.Text;
//Is the current aggregated message the termination string?
if (Regex.Match(existingAggregation, regexPattern).Success)
{
//If so, save it to the output property and end the dialog
//Get property from expression
string property = this.Property?.GetValue(dc.State);
dc.State.SetValue(property, existingAggregation);
return await dc.EndDialogAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
}
else
{
//If we didn't timeout then we have to manage our timer somehow.
//For starters, complete our existing timer.
string timerId = dc.State.GetValue<string>(TimerId);
if (timerId != null)
{
await stateMatrix.CompleteAsync(timerId).ConfigureAwait(false);
// Restart the timeout timer
await InitTimeoutTimerAsync(dc, cancellationToken).ConfigureAwait(false);
}
//else, save the updated aggregation and end the turn
dc.State.SetValue(AggregationDialogMemory, existingAggregation);
return new DialogTurnResult(DialogTurnStatus.Waiting);
}
}
/// <inheritdoc/>
protected override async Task<bool> OnPreBubbleEventAsync(DialogContext dc, DialogEvent e, CancellationToken cancellationToken)
{
if (e.Name == DialogEvents.ActivityReceived && dc.Context.Activity.Type == ActivityTypes.Message)
{
//Get interruption mask pattern from expression
string regexPattern = this.InterruptionMask?.GetValue(dc.State);
// Return true( already handled ) if input matches our regex interruption mask
if (!string.IsNullOrEmpty(regexPattern) && Regex.Match(dc.Context.Activity.Text, regexPattern).Success)
{
return true;
}
// Ask parent to perform recognition
await dc.Parent.EmitEventAsync(AdaptiveEvents.RecognizeUtterance, value: dc.Context.Activity, bubble: false, cancellationToken: cancellationToken).ConfigureAwait(false);
// Should we allow interruptions
var canInterrupt = true;
if (this.AllowInterruptions != null)
{
var (allowInterruptions, error) = this.AllowInterruptions.TryGetValue(dc.State);
canInterrupt = error == null && allowInterruptions;
}
// Stop bubbling if interruptions ar NOT allowed
return !canInterrupt;
}
return false;
}
protected async Task<DialogTurnResult> EndDialogAsync(DialogContext dc, CancellationToken cancellationToken)
{
// Set the default value to the output property and send the default value response to the user
if (this.DefaultValue != null)
{
var (value, error) = this.DefaultValue.TryGetValue(dc.State);
if (this.DefaultValueResponse != null)
{
var response = await this.DefaultValueResponse.BindAsync(dc, cancellationToken: cancellationToken).ConfigureAwait(false);
if (response != null)
{
await dc.Context.SendActivityAsync(response, cancellationToken).ConfigureAwait(false);
}
}
// Set output property
dc.State.SetValue(this.Property.GetValue(dc.State), value);
return await dc.EndDialogAsync(value, cancellationToken).ConfigureAwait(false);
}
return await dc.EndDialogAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
}
private async Task<DialogTurnResult> PromptUserAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken))
{
//Do we already have a value stored? This would happen in the interruption case, a case in which we are looping over ourselves, or maybe we had a fatal error and had to restart the dialog tree
var existingAggregation = dc.State.GetValue(AggregationDialogMemory, () => string.Empty);
if (!string.IsNullOrEmpty(existingAggregation))
{
var alwaysPrompt = this.AlwaysPrompt?.GetValue(dc.State) ?? false;
//Are we set to always prompt?
if (alwaysPrompt)
{
//If so then we should actually clear the users input and prompt again.
dc.State.SetValue(AggregationDialogMemory, string.Empty);
}
else
{
//Otherwise we just want to leave.
return await dc.EndDialogAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
}
}
ITemplate<Activity> template = this.Prompt ?? throw new InvalidOperationException($"InputDialog is missing Prompt.");
IMessageActivity msg = await this.Prompt.BindAsync(dc, cancellationToken: cancellationToken).ConfigureAwait(false);
if (msg != null && string.IsNullOrEmpty(msg.InputHint))
{
msg.InputHint = InputHints.ExpectingInput;
}
var properties = new Dictionary<string, string>()
{
{ "template", JsonConvert.SerializeObject(template) },
{ "result", msg == null ? string.Empty : JsonConvert.SerializeObject(msg, new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore, MaxDepth = null }) },
};
TelemetryClient.TrackEvent("GeneratorResult", properties);
await dc.Context.SendActivityAsync(msg, cancellationToken).ConfigureAwait(false);
return new DialogTurnResult(DialogTurnStatus.Waiting);
}
private void CreateTimerForConversation(DialogContext dc, int timeout, string timerId, CancellationToken cancellationToken)
{
var adapter = dc.Context.Adapter;
var conversationReference = dc.Context.Activity.GetConversationReference();
var identity = dc.Context.TurnState.Get<ClaimsIdentity>("BotIdentity");
var audience = dc.Context.TurnState.Get<string>(BotAdapter.OAuthScopeKey);
//Question remaining to be answered: Will this task get garbage collected? If so, we need to maintain a handle for it.
Task.Run(async () =>
{
await Task.Delay(timeout).ConfigureAwait(false);
//if we aren't already complete, go ahead and timeout
await stateMatrix.RunForStatusAsync(timerId, StateStatus.Running, async () =>
{
await adapter.ContinueConversationAsync(
identity,
conversationReference,
audience,
BotWithLookup.OnTurn, //Leverage dirty hack to achieve Bot lookup from component
cancellationToken).ConfigureAwait(false);
}).ConfigureAwait(false);
});
}
private async Task InitTimeoutTimerAsync(DialogContext dc, CancellationToken cancellationToken)
{
var timeout = this.TimeOutInMilliseconds?.GetValue(dc.State) ?? 0;
if (timeout > 0)
{
var timerId = Guid.NewGuid().ToString();
CreateTimerForConversation(dc, timeout, timerId, cancellationToken);
await stateMatrix.StartAsync(timerId).ConfigureAwait(false);
dc.State.SetValue(TimerId, timerId);
}
}
}
}