Skip to content
This repository was archived by the owner on Jan 15, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from 1 commit
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,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.
Expand Down Expand Up @@ -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; }
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 +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");
Copy link
Member

Choose a reason for hiding this comment

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

Please turn this string into a private const.


//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);
Copy link
Member

Choose a reason for hiding this comment

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

A timer isn't always set for this prompt, because users aren't required to provide a timeOutInMilliseconds value, which is the precusor to a timerId being generated. So timerId can be null.

It seems that placing await InitTimeoutTimerAsync(dc, cancellationToken).ConfigureAwait(false); inside of the if(timerId != null) { } block would be better for human readability, and a slight optimization.


//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)
{
BotAdapter adapter = dc.Context.Adapter;
ConversationReference conversationReference = dc.Context.Activity.GetConversationReference();
Copy link
Member

Choose a reason for hiding this comment

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

These explicit typings are unnecessary and don't align with the rest of the changes, can you use var instead?

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);
}
}
}
}
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.
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.
Copy link
Member

Choose a reason for hiding this comment

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

The Timeout parameter isn't required, and in the code when the timeOutInMilliseconds value is null or less than 0, no timer is set.

The first part of the sentence is inaccurate and should be updated to reflect the functionality of the code.

If we want to make the timeout a requirement, there should be a default value of probably 20-40s and it needs to be signed off by @jameslew. I support the idea of making the timeout a required parameter due to the headless experience of phone calls, where a user might be distracted and forget what the bot asked, and the user has no easy way to ask the bot to repeat itself.

This IVR experience is different from the experience of interacting with a bot by typing messages over WebChat, etc.

Copy link
Member

Choose a reason for hiding this comment

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

For the current functionality in the code, a more accurate pair of statements would be:

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.

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