Skip to content
This repository was archived by the owner on Jan 15, 2025. It is now read-only.

Commit 2d315f0

Browse files
committed
Added support to timeout for the DTMF dialogs
1 parent 7e60182 commit 2d315f0

9 files changed

+183
-20
lines changed

packages/Telephony/Actions/BatchFixedLengthInput.cs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ public class BatchFixedLengthInput : BatchRegexInput
2222
[JsonProperty("$kind")]
2323
public new const string Kind = "Microsoft.Telephony.BatchFixedLengthInput";
2424

25-
private const string _dtmfCharacterRegex = @"^[\d#\*]+$";
2625
private const string _interruptionMaskRegex = @"^[\d]+$";
2726
private int _batchLength;
2827

@@ -68,15 +67,7 @@ public int BatchLength
6867
/// <inheritdoc/>
6968
public override Task<DialogTurnResult> ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = default)
7069
{
71-
if ((dc.Context.Activity.Type == ActivityTypes.Message) &&
72-
(Regex.Match(dc.Context.Activity.Text, _dtmfCharacterRegex).Success || dc.State.GetValue(TurnPath.Interrupted, () => false)))
73-
{
74-
return base.ContinueDialogAsync(dc, cancellationToken);
75-
}
76-
else
77-
{
78-
return Task.FromResult(new DialogTurnResult(DialogTurnStatus.Waiting));
79-
}
70+
return base.ContinueDialogAsync(dc, cancellationToken);
8071
}
8172
}
8273
}

packages/Telephony/Actions/BatchRegexInput.cs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Runtime.CompilerServices;
7+
using System.Security.Claims;
78
using System.Text.RegularExpressions;
89
using System.Threading;
910
using System.Threading.Tasks;
1011
using AdaptiveExpressions.Properties;
12+
using Microsoft.Bot.Builder;
1113
using Microsoft.Bot.Builder.Dialogs;
1214
using Microsoft.Bot.Builder.Dialogs.Adaptive;
15+
using Microsoft.Bot.Components.Telephony.Common;
1316
using Microsoft.Bot.Schema;
1417
using Newtonsoft.Json;
1518

@@ -23,6 +26,7 @@ public class BatchRegexInput : Dialog
2326
[JsonProperty("$kind")]
2427
public const string Kind = "Microsoft.Telephony.BatchRegexInput";
2528
protected const string AggregationDialogMemory = "this.aggregation";
29+
private static IStateMatrix stateMatrix = new LatchingStateMatrix();
2630

2731
/// <summary>
2832
/// Initializes a new instance of the <see cref="BatchRegexInput"/> class.
@@ -85,9 +89,24 @@ public BatchRegexInput([CallerFilePath] string sourceFilePath = "", [CallerLineN
8589
[JsonProperty("interruptionMask")]
8690
public StringExpression InterruptionMask { get; set; }
8791

92+
/// <summary>
93+
/// Gets or sets a value indicating how long to wait for before timing out and using the default value.
94+
/// </summary>
95+
[JsonProperty("timeOutInMilliseconds")]
96+
public IntExpression TimeOutInMilliseconds { get; set; }
97+
98+
/// <summary>
99+
/// Gets or sets a default value when failing after a time out.
100+
/// </summary>
101+
[JsonProperty("defaultValue")]
102+
public StringExpression DefaultValue { get; set; }
103+
88104
/// <inheritdoc/>
89105
public override async Task<DialogTurnResult> BeginDialogAsync(DialogContext dc, object options = null, CancellationToken cancellationToken = default(CancellationToken))
90106
{
107+
//start a timer that will continue this conversation
108+
await InitTimeoutTimerAsync(dc, cancellationToken).ConfigureAwait(false);
109+
91110
return await PromptUserAsync(dc, cancellationToken).ConfigureAwait(false);
92111
}
93112

@@ -101,6 +120,14 @@ public override async Task<DialogTurnResult> ContinueDialogAsync(DialogContext d
101120
return await PromptUserAsync(dc, cancellationToken).ConfigureAwait(false);
102121
}
103122

123+
//Handle case where we timed out
124+
var activity = dc.Context.Activity;
125+
if (!wasInterrupted && activity.Type != ActivityTypes.Message && activity.Name == ActivityEventNames.ContinueConversation)
126+
{
127+
await SendActivityAsync(dc, cancellationToken).ConfigureAwait(false);
128+
return await dc.EndDialogAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
129+
}
130+
104131
//Get value of termination string from expression
105132
string regexPattern = this.TerminationConditionRegexPattern?.GetValue(dc.State);
106133

@@ -120,6 +147,19 @@ public override async Task<DialogTurnResult> ContinueDialogAsync(DialogContext d
120147
}
121148
else
122149
{
150+
//If we didn't timeout then we have to manage our timer somehow.
151+
//For starters, complete our existing timer.
152+
var timerId = dc.State.GetValue<string>("this.TimerId");
153+
154+
//Should never happen but if it does, it shouldn't be fatal.
155+
if (timerId != null)
156+
{
157+
await stateMatrix.CompleteAsync(timerId).ConfigureAwait(false);
158+
}
159+
160+
// Restart the timeout timer
161+
await InitTimeoutTimerAsync(dc, cancellationToken).ConfigureAwait(false);
162+
123163
//else, save the updated aggregation and end the turn
124164
dc.State.SetValue(AggregationDialogMemory, existingAggregation);
125165
return new DialogTurnResult(DialogTurnStatus.Waiting);
@@ -198,5 +238,59 @@ protected override async Task<bool> OnPreBubbleEventAsync(DialogContext dc, Dial
198238

199239
return new DialogTurnResult(DialogTurnStatus.Waiting);
200240
}
241+
242+
private async Task<DialogTurnResult> SendActivityAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken))
243+
{
244+
var defaultValue = this.DefaultValue ?? throw new InvalidOperationException($"Value for DefaultValue property is not defined.");
245+
var msg = new Activity()
246+
{
247+
From = new ChannelAccount()
248+
{
249+
Role = "bot"
250+
},
251+
Type = "message",
252+
Text = defaultValue.GetValue(dc.State)
253+
};
254+
await dc.Context.SendActivityAsync(msg, cancellationToken).ConfigureAwait(false);
255+
256+
return new DialogTurnResult(DialogTurnStatus.Complete);
257+
}
258+
259+
private void CreateTimerForConversation(DialogContext dc, string timerId, CancellationToken cancellationToken)
260+
{
261+
BotAdapter adapter = dc.Context.Adapter;
262+
var identity = dc.Context.TurnState.Get<ClaimsIdentity>("BotIdentity");
263+
264+
ConversationReference conversationReference = dc.Context.Activity.GetConversationReference();
265+
var timeoutExpression = this.TimeOutInMilliseconds ?? throw new InvalidOperationException($"Value for TimeOutInMilliseconds property is not defined.");
266+
int timeout = timeoutExpression.GetValue(dc.State);
267+
268+
var audience = dc.Context.TurnState.Get<string>(BotAdapter.OAuthScopeKey);
269+
270+
//Question remaining to be answered: Will this task get garbage collected? If so, we need to maintain a handle for it.
271+
Task.Run(async () =>
272+
{
273+
await Task.Delay(timeout).ConfigureAwait(false);
274+
275+
//if we aren't already complete, go ahead and timeout
276+
await stateMatrix.RunForStatusAsync(timerId, StateStatus.Running, async () =>
277+
{
278+
await adapter.ContinueConversationAsync(
279+
identity,
280+
conversationReference,
281+
audience,
282+
BotWithLookup.OnTurn, //Leverage dirty hack to achieve Bot lookup from component
283+
cancellationToken).ConfigureAwait(false);
284+
}).ConfigureAwait(false);
285+
});
286+
}
287+
288+
private async Task InitTimeoutTimerAsync(DialogContext dc, CancellationToken cancellationToken)
289+
{
290+
var timerId = Guid.NewGuid().ToString();
291+
CreateTimerForConversation(dc, timerId, cancellationToken);
292+
await stateMatrix.StartAsync(timerId).ConfigureAwait(false);
293+
dc.State.SetValue("this.TimerId", timerId);
294+
}
201295
}
202296
}

packages/Telephony/Actions/BatchTerminationCharacterInput.cs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ public class BatchTerminationCharacterInput : BatchRegexInput
2222
[JsonProperty("$kind")]
2323
public new const string Kind = "Microsoft.Telephony.BatchTerminationCharacterInput";
2424

25-
private const string _dtmfCharacterRegex = @"^[\d#\*]+$";
2625
private const string _interruptionMaskRegex = @"^[\d]+$";
2726
private string _terminationCharacter;
2827

@@ -62,15 +61,7 @@ public string TerminationCharacter
6261
/// <inheritdoc/>
6362
public override Task<DialogTurnResult> ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = default)
6463
{
65-
if ((dc.Context.Activity.Type == ActivityTypes.Message) &&
66-
(Regex.Match(dc.Context.Activity.Text, _dtmfCharacterRegex).Success || dc.State.GetValue(TurnPath.Interrupted, () => false)))
67-
{
68-
return base.ContinueDialogAsync(dc, cancellationToken);
69-
}
70-
else
71-
{
72-
return Task.FromResult(new DialogTurnResult(DialogTurnStatus.Waiting));
73-
}
64+
return base.ContinueDialogAsync(dc, cancellationToken);
7465
}
7566
}
7667
}

packages/Telephony/Schemas/Microsoft.Telephony.BatchFixedLengthInput.schema

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,23 @@
4949
"$ref": "schema:#/definitions/booleanExpression",
5050
"title": "Always Prompt",
5151
"description": "When true, if batch is interrupted, it will attempt to restart the batch rather than abandon it."
52+
},
53+
"timeOutInMilliseconds": {
54+
"$ref": "schema:#/definitions/integerExpression",
55+
"title": "Timeout in milliseconds",
56+
"description": "After the specified amount of milliseconds the dialog will complete with its default value if the user doesn't respond.",
57+
"examples": [
58+
"10",
59+
"=conversation.xyz"
60+
]
61+
},
62+
"defaultValue": {
63+
"$ref": "schema:#/definitions/stringExpression",
64+
"title": "Default value",
65+
"description": "Value to be shown to the user when the timeout is reached.",
66+
"examples": [
67+
"Timeout error"
68+
]
5269
}
5370
},
5471
"$policies": {

packages/Telephony/Schemas/Microsoft.Telephony.BatchFixedLengthInput.uischema

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
"property",
1010
"allowInterruptions",
1111
"alwaysPrompt",
12+
"timeOutInMilliseconds",
13+
"defaultValue",
1214
"*"
1315
],
1416
"properties": {
@@ -31,6 +33,16 @@
3133
"intellisenseScopes": [
3234
"variable-scopes"
3335
]
36+
},
37+
"timeOutInMilliseconds": {
38+
"intellisenseScopes": [
39+
"variable-scopes"
40+
]
41+
},
42+
"defaultValue": {
43+
"intellisenseScopes": [
44+
"variable-scopes"
45+
]
3446
}
3547
}
3648
},

packages/Telephony/Schemas/Microsoft.Telephony.BatchRegexInput.schema

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,23 @@
5757
"$ref": "schema:#/definitions/booleanExpression",
5858
"title": "Always Prompt",
5959
"description": "When true, if batch is interrupted, it will attempt to restart the batch rather than abandon it."
60+
},
61+
"timeOutInMilliseconds": {
62+
"$ref": "schema:#/definitions/integerExpression",
63+
"title": "Timeout in milliseconds",
64+
"description": "After the specified amount of milliseconds the dialog will complete with its default value if the user doesn't respond.",
65+
"examples": [
66+
"10",
67+
"=conversation.xyz"
68+
]
69+
},
70+
"defaultValue": {
71+
"$ref": "schema:#/definitions/stringExpression",
72+
"title": "Default value",
73+
"description": "Value to be shown to the user when the timeout is reached.",
74+
"examples": [
75+
"Timeout error"
76+
]
6077
}
6178
},
6279
"$policies": {

packages/Telephony/Schemas/Microsoft.Telephony.BatchRegexInput.uischema

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
"property",
1010
"allowInterruptions",
1111
"alwaysPrompt",
12+
"timeOutInMilliseconds",
13+
"defaultValue",
1214
"*"
1315
],
1416
"properties": {
@@ -36,6 +38,16 @@
3638
"intellisenseScopes": [
3739
"variable-scopes"
3840
]
41+
},
42+
"timeOutInMilliseconds": {
43+
"intellisenseScopes": [
44+
"variable-scopes"
45+
]
46+
},
47+
"defaultValue": {
48+
"intellisenseScopes": [
49+
"variable-scopes"
50+
]
3951
}
4052
}
4153
},

packages/Telephony/Schemas/Microsoft.Telephony.BatchTerminationCharacterInput.schema

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,23 @@
4949
"$ref": "schema:#/definitions/booleanExpression",
5050
"title": "Always Prompt",
5151
"description": "When true, if batch is interrupted, it will attempt to restart the batch rather than abandon it."
52+
},
53+
"timeOutInMilliseconds": {
54+
"$ref": "schema:#/definitions/integerExpression",
55+
"title": "Timeout in milliseconds",
56+
"description": "After the specified amount of milliseconds the dialog will complete with its default value if the user doesn't respond.",
57+
"examples": [
58+
"10",
59+
"=conversation.xyz"
60+
]
61+
},
62+
"defaultValue": {
63+
"$ref": "schema:#/definitions/stringExpression",
64+
"title": "Default value",
65+
"description": "Value to be shown to the user when the timeout is reached.",
66+
"examples": [
67+
"Timeout error"
68+
]
5269
}
5370
},"$policies": {
5471
"interactive": true

packages/Telephony/Schemas/Microsoft.Telephony.BatchTerminationCharacterInput.uischema

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
"property",
1010
"allowInterruptions",
1111
"alwaysPrompt",
12+
"timeOutInMilliseconds",
13+
"defaultValue",
1214
"*"
1315
],
1416
"properties": {
@@ -31,6 +33,16 @@
3133
"intellisenseScopes": [
3234
"variable-scopes"
3335
]
36+
},
37+
"timeOutInMilliseconds": {
38+
"intellisenseScopes": [
39+
"variable-scopes"
40+
]
41+
},
42+
"defaultValue": {
43+
"intellisenseScopes": [
44+
"variable-scopes"
45+
]
3446
}
3547
}
3648
},

0 commit comments

Comments
 (0)