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

Commit ff0bbc5

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

10 files changed

+238
-13
lines changed

packages/Telephony/Actions/BatchFixedLengthInput.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,16 +66,23 @@ public int BatchLength
6666
}
6767

6868
/// <inheritdoc/>
69-
public override Task<DialogTurnResult> ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = default)
69+
public async override Task<DialogTurnResult> ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = default)
7070
{
7171
if ((dc.Context.Activity.Type == ActivityTypes.Message) &&
7272
(Regex.Match(dc.Context.Activity.Text, _dtmfCharacterRegex).Success || dc.State.GetValue(TurnPath.Interrupted, () => false)))
7373
{
74-
return base.ContinueDialogAsync(dc, cancellationToken);
74+
return await base.ContinueDialogAsync(dc, cancellationToken).ConfigureAwait(false);
7575
}
7676
else
7777
{
78-
return Task.FromResult(new DialogTurnResult(DialogTurnStatus.Waiting));
78+
if (dc.Context.Activity.Name == ActivityEventNames.ContinueConversation)
79+
{
80+
return await EndDialogAsync(dc, cancellationToken).ConfigureAwait(false);
81+
}
82+
else
83+
{
84+
return new DialogTurnResult(DialogTurnStatus.Waiting);
85+
}
7986
}
8087
}
8188
}

packages/Telephony/Actions/BatchRegexInput.cs

Lines changed: 106 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,36 @@ 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 the default value for the input dialog when a Timeout is reached.
100+
/// </summary>
101+
/// <value>
102+
/// Value or expression which evaluates to a value.
103+
/// </value>
104+
[JsonProperty("defaultValue")]
105+
public ValueExpression DefaultValue { get; set; }
106+
107+
/// <summary>
108+
/// Gets or sets the activity template to send when a Timeout is reached and the default value is used.
109+
/// </summary>
110+
/// <value>
111+
/// An activity template.
112+
/// </value>
113+
[JsonProperty("defaultValueResponse")]
114+
public ITemplate<Activity> DefaultValueResponse { get; set; }
115+
88116
/// <inheritdoc/>
89117
public override async Task<DialogTurnResult> BeginDialogAsync(DialogContext dc, object options = null, CancellationToken cancellationToken = default(CancellationToken))
90118
{
119+
//start a timer that will continue this conversation
120+
await InitTimeoutTimerAsync(dc, cancellationToken).ConfigureAwait(false);
121+
91122
return await PromptUserAsync(dc, cancellationToken).ConfigureAwait(false);
92123
}
93124

@@ -120,6 +151,19 @@ public override async Task<DialogTurnResult> ContinueDialogAsync(DialogContext d
120151
}
121152
else
122153
{
154+
//If we didn't timeout then we have to manage our timer somehow.
155+
//For starters, complete our existing timer.
156+
var timerId = dc.State.GetValue<string>("this.TimerId");
157+
158+
//Should never happen but if it does, it shouldn't be fatal.
159+
if (timerId != null)
160+
{
161+
await stateMatrix.CompleteAsync(timerId).ConfigureAwait(false);
162+
}
163+
164+
// Restart the timeout timer
165+
await InitTimeoutTimerAsync(dc, cancellationToken).ConfigureAwait(false);
166+
123167
//else, save the updated aggregation and end the turn
124168
dc.State.SetValue(AggregationDialogMemory, existingAggregation);
125169
return new DialogTurnResult(DialogTurnStatus.Waiting);
@@ -158,6 +202,30 @@ protected override async Task<bool> OnPreBubbleEventAsync(DialogContext dc, Dial
158202
return false;
159203
}
160204

205+
protected async Task<DialogTurnResult> EndDialogAsync(DialogContext dc, CancellationToken cancellationToken)
206+
{
207+
// Set the default value to the output property and send the default value response to the user
208+
if (this.DefaultValue != null)
209+
{
210+
var (value, error) = this.DefaultValue.TryGetValue(dc.State);
211+
if (this.DefaultValueResponse != null)
212+
{
213+
var response = await this.DefaultValueResponse.BindAsync(dc, cancellationToken: cancellationToken).ConfigureAwait(false);
214+
if (response != null)
215+
{
216+
await dc.Context.SendActivityAsync(response, cancellationToken).ConfigureAwait(false);
217+
}
218+
}
219+
220+
// Set output property
221+
dc.State.SetValue(this.Property.GetValue(dc.State), value);
222+
223+
return await dc.EndDialogAsync(value, cancellationToken).ConfigureAwait(false);
224+
}
225+
226+
return await dc.EndDialogAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
227+
}
228+
161229
private async Task<DialogTurnResult> PromptUserAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken))
162230
{
163231
//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
@@ -198,5 +266,43 @@ protected override async Task<bool> OnPreBubbleEventAsync(DialogContext dc, Dial
198266

199267
return new DialogTurnResult(DialogTurnStatus.Waiting);
200268
}
269+
270+
private void CreateTimerForConversation(DialogContext dc, int timeout, string timerId, CancellationToken cancellationToken)
271+
{
272+
BotAdapter adapter = dc.Context.Adapter;
273+
ConversationReference conversationReference = dc.Context.Activity.GetConversationReference();
274+
var identity = dc.Context.TurnState.Get<ClaimsIdentity>("BotIdentity");
275+
var audience = dc.Context.TurnState.Get<string>(BotAdapter.OAuthScopeKey);
276+
277+
//Question remaining to be answered: Will this task get garbage collected? If so, we need to maintain a handle for it.
278+
Task.Run(async () =>
279+
{
280+
await Task.Delay(timeout).ConfigureAwait(false);
281+
282+
//if we aren't already complete, go ahead and timeout
283+
await stateMatrix.RunForStatusAsync(timerId, StateStatus.Running, async () =>
284+
{
285+
await adapter.ContinueConversationAsync(
286+
identity,
287+
conversationReference,
288+
audience,
289+
BotWithLookup.OnTurn, //Leverage dirty hack to achieve Bot lookup from component
290+
cancellationToken).ConfigureAwait(false);
291+
}).ConfigureAwait(false);
292+
});
293+
}
294+
295+
private async Task InitTimeoutTimerAsync(DialogContext dc, CancellationToken cancellationToken)
296+
{
297+
var timeout = this.TimeOutInMilliseconds?.GetValue(dc.State) ?? 0;
298+
299+
if (timeout > 0)
300+
{
301+
var timerId = Guid.NewGuid().ToString();
302+
CreateTimerForConversation(dc, timeout, timerId, cancellationToken);
303+
await stateMatrix.StartAsync(timerId).ConfigureAwait(false);
304+
dc.State.SetValue("this.TimerId", timerId);
305+
}
306+
}
201307
}
202308
}

packages/Telephony/Actions/BatchTerminationCharacterInput.cs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,23 @@ public string TerminationCharacter
6060
}
6161

6262
/// <inheritdoc/>
63-
public override Task<DialogTurnResult> ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = default)
63+
public override async Task<DialogTurnResult> ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = default)
6464
{
65-
if ((dc.Context.Activity.Type == ActivityTypes.Message) &&
66-
(Regex.Match(dc.Context.Activity.Text, _dtmfCharacterRegex).Success || dc.State.GetValue(TurnPath.Interrupted, () => false)))
65+
if ((dc.Context.Activity.Type == ActivityTypes.Message) &&
66+
(Regex.Match(dc.Context.Activity.Text, _dtmfCharacterRegex).Success || dc.State.GetValue(TurnPath.Interrupted, () => false)))
6767
{
68-
return base.ContinueDialogAsync(dc, cancellationToken);
68+
return await base.ContinueDialogAsync(dc, cancellationToken).ConfigureAwait(false);
6969
}
7070
else
7171
{
72-
return Task.FromResult(new DialogTurnResult(DialogTurnStatus.Waiting));
72+
if (dc.Context.Activity.Name == ActivityEventNames.ContinueConversation)
73+
{
74+
return await EndDialogAsync(dc, cancellationToken).ConfigureAwait(false);
75+
}
76+
else
77+
{
78+
return new DialogTurnResult(DialogTurnStatus.Waiting);
79+
}
7380
}
7481
}
7582
}

packages/Telephony/Readme.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -120,23 +120,27 @@ The Stop Recording action stops recording of the conversation. Note that it is n
120120
## **Aggregate DTMF Input (n)**
121121
Prompts the user for multiple inputs that are aggregated until a specified character length is met or exceeded.
122122
Speech, DTMF inputs, and chat provided characters can all be used to provide input, but any inputs that aren't the characters 1,2,3,4,5,6,7,8,9,0,#,*, or some combination of said characters are dropped.
123+
A timeout timer will be initialized when the dialog begins or when the user sent a response that has not meet or exceed the batch length.
124+
When the timeout is reached, the dialog will end and if the Default Value is set, its value will be assigned to the Property field. Also, a response can be sent to the user using the Default value Response field.
123125

124126
#### Parameters
125127
* Batch Length
126128
* Property
127129
* Prompt
128130
* AllowInterruptions
129131
* AlwaysPrompt
132+
* Timeout
133+
* Default Value
134+
* Default Value Response
130135

131136
#### Usage
132137
* After started, each input the user sends will be appended to the last message until the user provides a number of characters equal to or greater than the batch length.
133138

134139
#### Dialog Flow
135-
* The dialog will only end and continue to the next dialog when the batch length is reached.
140+
* The dialog will only end and continue to the next dialog when the batch length is reached or the timeout is reached.
136141
* If AllowInterruptions is true, the parent dialog will receive non-digit input and can handle it as an intent.
137142
* After the interruption is handled, control flow will resume with this dialog. If AlwaysPrompt is set to true, the dialog will attempt to start over, otherwise it will end this dialog without setting the output property.
138-
* Best practice recommendation when using interruptions is to validate that the output property has been set and handle the case in which it is and isn't set.'
139-
143+
* Best practice recommendation when using interruptions is to validate that the output property has been set and handle the case in which it is and isn't set.
140144

141145
#### Failures
142146
* In the event that an exception occurs within the dialog, the dialog will end and the normal exception flow can be followed.
@@ -145,22 +149,27 @@ Speech, DTMF inputs, and chat provided characters can all be used to provide inp
145149
## **Aggregate DTMF Input (#)**
146150
Prompts the user for multiple inputs that are aggregated until the termination string is received.
147151
Speech, DTMF inputs, and chat provided characters can all be used to provide input, but any inputs that aren't the characters 1,2,3,4,5,6,7,8,9,0,#,*, or some combination of said characters are dropped.
152+
A timeout timer will be initialized when the dialog begins or when the user sent a response without including the termination character.
153+
When the timeout is reached, the dialog will end and if the Default Value is set, its value will be assigned to the Property field. Also, a response can be sent to the user using the Default value Response field.
148154

149155
#### Parameters
150156
* Termination Character
151157
* Property
152158
* Prompt
153159
* AllowInterruptions
154160
* AlwaysPrompt
161+
* Timeout
162+
* Default Value
163+
* Default Value Response
155164

156165
#### Usage
157166
* After started, each input the user sends will be appended to the last message until the user sends the provided termination character
158167

159168
#### Dialog Flow
160-
* The dialog will only end and continue to the next dialog when the termination character is sent.
169+
* The dialog will only end and continue to the next dialog when the termination character is sent or the timeout is reached.
161170
* If AllowInterruptions is true, the parent dialog will receive non-digit input and can handle it as an intent.
162171
* After the interruption is handled, control flow will resume with this dialog. If AlwaysPrompt is set to true, the dialog will attempt to start over, otherwise it will end this dialog without setting the output property.
163-
* Best practice recommendation when using interruptions is to validate that the output property has been set and handle the case in which it is and isn't set.'
172+
* Best practice recommendation when using interruptions is to validate that the output property has been set and handle the case in which it is and isn't set.
164173

165174
#### Failures
166175
* In the event that an exception occurs within the dialog, the dialog will end and the normal exception flow can be followed.

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,30 @@
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": "'Property' will be set to the value of this expression when a timeout is reached.",
66+
"examples": [
67+
"hello world",
68+
"Hello ${user.name}",
69+
"=concat(user.firstname, user.lastName)"
70+
]
71+
},
72+
"defaultValueResponse": {
73+
"$kind": "Microsoft.IActivityTemplate",
74+
"title": "Default value response",
75+
"description": "Message to send when a Timeout has been reached and the default value is selected as the value."
5276
}
5377
},
5478
"$policies": {

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
"property",
1010
"allowInterruptions",
1111
"alwaysPrompt",
12+
"timeOutInMilliseconds",
13+
"defaultValue",
14+
"defaultValueResponse",
1215
"*"
1316
],
1417
"properties": {

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,30 @@
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": "'Property' will be set to the value of this expression when a timeout is reached.",
74+
"examples": [
75+
"hello world",
76+
"Hello ${user.name}",
77+
"=concat(user.firstname, user.lastName)"
78+
]
79+
},
80+
"defaultValueResponse": {
81+
"$kind": "Microsoft.IActivityTemplate",
82+
"title": "Default value response",
83+
"description": "Message to send when a Timeout has been reached and the default value is selected as the value."
6084
}
6185
},
6286
"$policies": {

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
"property",
1010
"allowInterruptions",
1111
"alwaysPrompt",
12+
"timeOutInMilliseconds",
13+
"defaultValue",
14+
"defaultValueResponse",
1215
"*"
1316
],
1417
"properties": {
@@ -36,6 +39,21 @@
3639
"intellisenseScopes": [
3740
"variable-scopes"
3841
]
42+
},
43+
"timeOutInMilliseconds": {
44+
"intellisenseScopes": [
45+
"variable-scopes"
46+
]
47+
},
48+
"defaultValue": {
49+
"intellisenseScopes": [
50+
"variable-scopes"
51+
]
52+
},
53+
"defaultValueResponse": {
54+
"intellisenseScopes": [
55+
"variable-scopes"
56+
]
3957
}
4058
}
4159
},

0 commit comments

Comments
 (0)