-
Notifications
You must be signed in to change notification settings - Fork 85
feat: Added timeout support for some Telephony package's dialogs #1479
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
||
|
|
@@ -23,6 +26,7 @@ public class BatchRegexInput : Dialog | |
| [JsonProperty("$kind")] | ||
| public const string Kind = "Microsoft.Telephony.BatchRegexInput"; | ||
| protected const string AggregationDialogMemory = "this.aggregation"; | ||
| private static IStateMatrix stateMatrix = new LatchingStateMatrix(); | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="BatchRegexInput"/> class. | ||
|
|
@@ -85,9 +89,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; } | ||
|
|
||
| /// <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); | ||
| } | ||
|
|
||
|
|
@@ -120,6 +151,19 @@ 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. | ||
| var timerId = dc.State.GetValue<string>("this.TimerId"); | ||
|
||
|
|
||
| //Should never happen but if it does, it shouldn't be fatal. | ||
| 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); | ||
|
|
@@ -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 | ||
|
|
@@ -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) | ||
| { | ||
| BotAdapter adapter = dc.Context.Adapter; | ||
| ConversationReference 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("this.TimerId", timerId); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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. | ||
| 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. | ||
|
||
| 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. | ||
|
|
||
| #### 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. | ||
|
|
@@ -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. | ||
| A timeout timer will be initialized when the dialog begins or when the user sent a response without including the termination character. | ||
| 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. | ||
|
|
||
| #### 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. | ||
|
|
||
There was a problem hiding this comment.
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
defaultValueanddefaultValueResponsewhentimeOutInMillisecondsis some small number, e.g.,1.When
timeOutInMillisecondsis1, the bot should ask the user its prompt, then immediately send anydefaultValueResponseand end with thedefaultValuein theDialogTurnResult.Please add a test that covers this scenario.