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

feat: Added timeout support for some Telephony package's dialogs #1479

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions packages/Telephony/Actions/BatchFixedLengthInput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,23 @@ public int BatchLength
}

/// <inheritdoc/>
public override Task<DialogTurnResult> ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = default)
public async override Task<DialogTurnResult> ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = default)
{
if ((dc.Context.Activity.Type == ActivityTypes.Message) &&
(Regex.Match(dc.Context.Activity.Text, _dtmfCharacterRegex).Success || dc.State.GetValue(TurnPath.Interrupted, () => false)))
{
return base.ContinueDialogAsync(dc, cancellationToken);
return await base.ContinueDialogAsync(dc, cancellationToken).ConfigureAwait(false);
}
else
{
return Task.FromResult(new DialogTurnResult(DialogTurnStatus.Waiting));
if (dc.Context.Activity.Name == ActivityEventNames.ContinueConversation)
{
return await EndDialogAsync(dc, cancellationToken).ConfigureAwait(false);
}
else
{
return new DialogTurnResult(DialogTurnStatus.Waiting);
}
}
}
}
Expand Down
106 changes: 106 additions & 0 deletions packages/Telephony/Actions/BatchRegexInput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@
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;

Expand All @@ -23,6 +26,8 @@ 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.
Expand Down Expand Up @@ -85,9 +90,36 @@ public BatchRegexInput([CallerFilePath] string sourceFilePath = "", [CallerLineN
[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; }
Comment on lines +99 to +115
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to me that it should be straightforward to add a unit test verifying the functionality tied to defaultValue and defaultValueResponse when timeOutInMilliseconds is some small number, e.g., 1.

When timeOutInMilliseconds is 1, the bot should ask the user its prompt, then immediately send any defaultValueResponse and end with the defaultValue in the DialogTurnResult.

Please add a test that covers this scenario.


/// <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);
}

Expand Down Expand Up @@ -120,6 +152,18 @@ public override async Task<DialogTurnResult> ContinueDialogAsync(DialogContext d
}
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);
Expand Down Expand Up @@ -158,6 +202,30 @@ protected override async Task<bool> OnPreBubbleEventAsync(DialogContext dc, Dial
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
Expand Down Expand Up @@ -198,5 +266,43 @@ protected override async Task<bool> OnPreBubbleEventAsync(DialogContext dc, Dial

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);
}
}
}
}
17 changes: 12 additions & 5 deletions packages/Telephony/Actions/BatchTerminationCharacterInput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,16 +60,23 @@ public string TerminationCharacter
}

/// <inheritdoc/>
public override Task<DialogTurnResult> ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = default)
public override async Task<DialogTurnResult> ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = default)
{
if ((dc.Context.Activity.Type == ActivityTypes.Message) &&
(Regex.Match(dc.Context.Activity.Text, _dtmfCharacterRegex).Success || dc.State.GetValue(TurnPath.Interrupted, () => false)))
if ((dc.Context.Activity.Type == ActivityTypes.Message) &&
(Regex.Match(dc.Context.Activity.Text, _dtmfCharacterRegex).Success || dc.State.GetValue(TurnPath.Interrupted, () => false)))
{
return base.ContinueDialogAsync(dc, cancellationToken);
return await base.ContinueDialogAsync(dc, cancellationToken).ConfigureAwait(false);
}
else
{
return Task.FromResult(new DialogTurnResult(DialogTurnStatus.Waiting));
if (dc.Context.Activity.Name == ActivityEventNames.ContinueConversation)
{
return await EndDialogAsync(dc, cancellationToken).ConfigureAwait(false);
}
else
{
return new DialogTurnResult(DialogTurnStatus.Waiting);
}
}
}
}
Expand Down
19 changes: 14 additions & 5 deletions packages/Telephony/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,23 +120,27 @@ The Stop Recording action stops recording of the conversation. Note that it is n
## **Aggregate DTMF Input (n)**
Prompts the user for multiple inputs that are aggregated until a specified character length is met or exceeded.
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.
When the Timeout parameter is set an integer greater than 0, a timer will be set whenever the Aggreate DTMF Input(n) node begins. This timer will be reset whenever the user responds to the bot, until the expected batch length is met.
In case 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.

#### Parameters
* Batch Length
* Property
* Prompt
* AllowInterruptions
* AlwaysPrompt
* Timeout
* Default Value
* Default Value Response

#### Usage
* 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.

#### Dialog Flow
* The dialog will only end and continue to the next dialog when the batch length is reached.
* The dialog will only end and continue to the next dialog when the batch length is reached or the timeout is reached.
* If AllowInterruptions is true, the parent dialog will receive non-digit input and can handle it as an intent.
* 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.
* 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.'

* 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.

#### Failures
* In the event that an exception occurs within the dialog, the dialog will end and the normal exception flow can be followed.
Expand All @@ -145,22 +149,27 @@ Speech, DTMF inputs, and chat provided characters can all be used to provide inp
## **Aggregate DTMF Input (#)**
Prompts the user for multiple inputs that are aggregated until the termination string is received.
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.
When the Timeout parameter is set an integer greater than 0, a timer will be set whenever the Aggreate DTMF Input(n) node begins. This timer will be reset whenever the user responds to the bot, until the expected batch length is met.
In case 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.

#### Parameters
* Termination Character
* Property
* Prompt
* AllowInterruptions
* AlwaysPrompt
* Timeout
* Default Value
* Default Value Response

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

#### Dialog Flow
* The dialog will only end and continue to the next dialog when the termination character is sent.
* The dialog will only end and continue to the next dialog when the termination character is sent or the timeout is reached.
* If AllowInterruptions is true, the parent dialog will receive non-digit input and can handle it as an intent.
* 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.
* 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.'
* 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.

#### Failures
* In the event that an exception occurs within the dialog, the dialog will end and the normal exception flow can be followed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,30 @@
"$ref": "schema:#/definitions/booleanExpression",
"title": "Always Prompt",
"description": "When true, if batch is interrupted, it will attempt to restart the batch rather than abandon it."
},
"timeOutInMilliseconds": {
"$ref": "schema:#/definitions/integerExpression",
"title": "Timeout in milliseconds",
"description": "After the specified amount of milliseconds the dialog will complete with its default value if the user doesn't respond.",
"examples": [
"10",
"=conversation.xyz"
]
},
"defaultValue": {
"$ref": "schema:#/definitions/stringExpression",
"title": "Default value",
"description": "'Property' will be set to the value of this expression when a timeout is reached.",
"examples": [
"hello world",
"Hello ${user.name}",
"=concat(user.firstname, user.lastName)"
]
},
"defaultValueResponse": {
"$kind": "Microsoft.IActivityTemplate",
"title": "Default value response",
"description": "Message to send when a Timeout has been reached and the default value is selected as the value."
}
},
"$policies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
"property",
"allowInterruptions",
"alwaysPrompt",
"timeOutInMilliseconds",
"defaultValue",
"defaultValueResponse",
"*"
],
"properties": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,30 @@
"$ref": "schema:#/definitions/booleanExpression",
"title": "Always Prompt",
"description": "When true, if batch is interrupted, it will attempt to restart the batch rather than abandon it."
},
"timeOutInMilliseconds": {
"$ref": "schema:#/definitions/integerExpression",
"title": "Timeout in milliseconds",
"description": "After the specified amount of milliseconds the dialog will complete with its default value if the user doesn't respond.",
"examples": [
"10",
"=conversation.xyz"
]
},
"defaultValue": {
"$ref": "schema:#/definitions/stringExpression",
"title": "Default value",
"description": "'Property' will be set to the value of this expression when a timeout is reached.",
"examples": [
"hello world",
"Hello ${user.name}",
"=concat(user.firstname, user.lastName)"
]
},
"defaultValueResponse": {
"$kind": "Microsoft.IActivityTemplate",
"title": "Default value response",
"description": "Message to send when a Timeout has been reached and the default value is selected as the value."
}
},
"$policies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
"property",
"allowInterruptions",
"alwaysPrompt",
"timeOutInMilliseconds",
"defaultValue",
"defaultValueResponse",
"*"
],
"properties": {
Expand Down Expand Up @@ -36,6 +39,21 @@
"intellisenseScopes": [
"variable-scopes"
]
},
"timeOutInMilliseconds": {
"intellisenseScopes": [
"variable-scopes"
]
},
"defaultValue": {
"intellisenseScopes": [
"variable-scopes"
]
},
"defaultValueResponse": {
"intellisenseScopes": [
"variable-scopes"
]
}
}
},
Expand Down
Loading