diff --git a/.gitignore b/.gitignore index 75c6634b81..b646ad1e38 100644 --- a/.gitignore +++ b/.gitignore @@ -403,4 +403,5 @@ experimental/generator-dotnet-yeoman/node_modules package-lock.json # Ignore test bots -testing/* \ No newline at end of file +testing/* +packages/.vs/* diff --git a/packages/Telephony/Actions/ITimeoutInput.cs b/packages/Telephony/Actions/ITimeoutInput.cs new file mode 100644 index 0000000000..ca5ba1abc8 --- /dev/null +++ b/packages/Telephony/Actions/ITimeoutInput.cs @@ -0,0 +1,21 @@ +using AdaptiveExpressions.Properties; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Bot.Components.Telephony.Actions +{ + public interface ITimeoutInput + { + // Summary: + // Defines dialog context state property value. + const string SilenceDetected = "dialog.silenceDetected"; + + /// + /// Gets or sets a value indicating how long to wait for before timing out and using the default value. + /// + [JsonProperty("timeOutInMilliseconds")] + IntExpression TimeOutInMilliseconds { get; set; } + } +} diff --git a/packages/Telephony/Actions/TimeoutChoiceInput.cs b/packages/Telephony/Actions/TimeoutChoiceInput.cs index f86cf6e66b..6f3f3d872b 100644 --- a/packages/Telephony/Actions/TimeoutChoiceInput.cs +++ b/packages/Telephony/Actions/TimeoutChoiceInput.cs @@ -16,12 +16,16 @@ namespace Microsoft.Bot.Components.Telephony.Actions { - public class TimeoutChoiceInput : ChoiceInput + public class TimeoutChoiceInput : ChoiceInput, ITimeoutInput { [JsonProperty("$kind")] public new const string Kind = "Microsoft.Telephony.TimeoutChoiceInput"; - private static IStateMatrix stateMatrix = new LatchingStateMatrix(); + /// + /// Gets or sets a value indicating how long to wait for before timing out and using the default value. + /// + [JsonProperty("timeOutInMilliseconds")] + public IntExpression TimeOutInMilliseconds { get; set; } [JsonConstructor] public TimeoutChoiceInput([CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0) @@ -31,118 +35,20 @@ public TimeoutChoiceInput([CallerFilePath] string sourceFilePath = "", [CallerLi this.RegisterSourceLocation(sourceFilePath, sourceLineNumber); } - /// - /// Gets or sets a value indicating how long to wait for before timing out and using the default value. - /// - [JsonProperty("timeOutInMilliseconds")] - public IntExpression TimeOutInMilliseconds { get; set; } - public async override Task BeginDialogAsync(DialogContext dc, object options = null, CancellationToken cancellationToken = default(CancellationToken)) { - if (options is CancellationToken) - { - throw new ArgumentException($"{nameof(options)} cannot be a cancellation token"); - } - - if (this.Disabled != null && this.Disabled.GetValue(dc.State) == true) - { - return await dc.EndDialogAsync(cancellationToken: cancellationToken).ConfigureAwait(false); - } - - //start a timer that will continue this conversation - var timerId = Guid.NewGuid().ToString(); - CreateTimerForConversation(dc, timerId, cancellationToken); - await stateMatrix.StartAsync(timerId).ConfigureAwait(false); - dc.State.SetValue("this.TimerId", timerId); - - return await base.BeginDialogAsync(dc, options, cancellationToken).ConfigureAwait(false); + return await TimeoutInput.BeginDialogAsync(this, dc, + base.BeginDialogAsync, + options, cancellationToken); } public override async Task ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken)) { - var activity = dc.Context.Activity; - - //Handle case where we timed out - var interrupted = dc.State.GetValue(TurnPath.Interrupted, () => false); - if (!interrupted && activity.Type != ActivityTypes.Message && activity.Name == ActivityEventNames.ContinueConversation) - { - //Set max turns so that we evaluate the default when we visit the inputdialog. - MaxTurnCount = 1; - - //We need to set interrupted here or it will discard the continueconversation event... - dc.State.SetValue(TurnPath.Interrupted, true); - return await base.ContinueDialogAsync(dc, cancellationToken).ConfigureAwait(false); - } - 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("this.TimerId"); - - //Should never happen but if it does, it shouldn't be fatal. - if (timerId != null) - { - await stateMatrix.CompleteAsync(timerId).ConfigureAwait(false); - } - - //Begin dirty hack to start a timer for the reprompt - - //If our input was not valid, restart the timer. - dc.State.SetValue(VALUE_PROPERTY, activity.Text); //OnRecognizeInput assumes this was already set earlier on in the dialog. We will set it and then unset it to simulate passing an argument to a function... :D - if (await OnRecognizeInputAsync(dc, cancellationToken).ConfigureAwait(false) != InputState.Valid) - { - //We are cheating to force this recognition here. Maybe not good? - - //Known bug: Sometimes invalid input gets accepted anyway(due to max turns and defaulting rules), this will start a continuation for a thing it shouldn't. - //Sure do wish EndDialog was available to the adaptive stack. - - var newTimerId = Guid.NewGuid().ToString(); - CreateTimerForConversation(dc, newTimerId, cancellationToken); - await stateMatrix.StartAsync(newTimerId).ConfigureAwait(false); - } - - //Clear our the input property after recognition since it will happen again later :D - dc.State.SetValue(VALUE_PROPERTY, null); - - //End dirty hack - } - - return await base.ContinueDialogAsync(dc, cancellationToken).ConfigureAwait(false); + return await TimeoutInput.ContinueDialogAsync(this, dc, VALUE_PROPERTY, TURN_COUNT_PROPERTY, + OnRecognizeInputAsync, + base.ContinueDialogAsync, + cancellationToken); } - private void CreateTimerForConversation(DialogContext dc, string timerId, CancellationToken cancellationToken) - { - BotAdapter adapter = dc.Context.Adapter; - var identity = dc.Context.TurnState.Get("BotIdentity"); - - var appId = identity?.Claims?.FirstOrDefault(c => c.Type == AuthenticationConstants.AudienceClaim)?.Value; - ConversationReference conversationReference = dc.Context.Activity.GetConversationReference(); - int timeout = TimeOutInMilliseconds.GetValue(dc.State); - - //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); - string msAppId = appId; - - // If the channel is the Emulator, and authentication is not in use, - // the AppId will be null. We generate a random AppId for this case only. - // This is not required for production, since the AppId will have a value. - if (string.IsNullOrEmpty(msAppId)) - { - msAppId = Guid.NewGuid().ToString(); //if no AppId, use a random Guid - } - - //if we aren't already complete, go ahead and timeout - await stateMatrix.RunForStatusAsync(timerId, StateStatus.Running, async () => - { - await adapter.ContinueConversationAsync( - msAppId, - conversationReference, - BotWithLookup.OnTurn, //Leverage dirty hack to achieve Bot lookup from component - cancellationToken).ConfigureAwait(false); - }).ConfigureAwait(false); - }); - } } } \ No newline at end of file diff --git a/packages/Telephony/Actions/TimeoutInput.cs b/packages/Telephony/Actions/TimeoutInput.cs new file mode 100644 index 0000000000..dccf3f7482 --- /dev/null +++ b/packages/Telephony/Actions/TimeoutInput.cs @@ -0,0 +1,231 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using AdaptiveExpressions.Properties; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Adapters; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Builder.Dialogs.Adaptive.Input; +using Microsoft.Bot.Components.Telephony.Common; +using Microsoft.Bot.Connector.Authentication; +using Microsoft.Bot.Schema; +using Newtonsoft.Json; + +namespace Microsoft.Bot.Components.Telephony.Actions +{ + public static class TimeoutInput + { + // Summary: + // Defines dialog context state property value. + private const string TimerID = "dialog.TimerId"; + //private const string TimeoutId = "this.TimeoutId"; + //private const string ActiveTimeoutId = "conversation.ActiveTimeoutId"; + + private static ConcurrentDictionary triggeredTimers, IStateMatrix timersState)> conversationStateMatrix = new ConcurrentDictionary triggeredTimers, IStateMatrix timersState)>(); + + public static async Task BeginDialogAsync(T inputActivity, DialogContext dc, + Func> baseClassCall, + object options = null, CancellationToken cancellationToken = default(CancellationToken)) where T : InputDialog, ITimeoutInput + { + if (options is CancellationToken) + { + throw new ArgumentException($"{nameof(options)} cannot be a cancellation token"); + } + + if (inputActivity.Disabled?.GetValue(dc.State) == true) + { + return await dc.EndDialogAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + } + + dc.State.SetValue(ITimeoutInput.SilenceDetected, false); + + //start a timer that will continue this conversation + var timerId = CreateTimerForConversation(inputActivity, dc, cancellationToken); + if (!conversationStateMatrix.TryGetValue(dc.Context.Activity.Conversation.Id, out var convState)) + { + convState = (new ConcurrentQueue(), new LatchingStateMatrix()); + conversationStateMatrix[dc.Context.Activity.Conversation.Id] = convState; + } + await convState.timersState.StartAsync(timerId).ConfigureAwait(false); + + dc.Services.Get().TrackEvent("Start TimeoutInput", new Dictionary + { + {"timerId", timerId} + }); + + var res = await baseClassCall(dc, options, cancellationToken).ConfigureAwait(false); + return res; + } + + public static async Task ContinueDialogAsync(K inputActivity, DialogContext dc, string valueProperty, string turnCountProperty, + Func> onRecognizeInputAsync, + Func> continueDialogAsync, + CancellationToken cancellationToken = default(CancellationToken)) where K : InputDialog, ITimeoutInput + { + var timerId = dc.State.GetValue(TimerID); + + //conversation is ended + if (!conversationStateMatrix.TryGetValue(dc.Context.Activity.Conversation.Id, out var convState)) + return Dialog.EndOfTurn; + + + //if we aren't already complete, go ahead and timeout + return await convState.timersState.RunForStatusAsync(timerId, StateStatus.Running, async () => + { + //-----------------------------Body--------------- + var activity = dc.Context.Activity; + + //Handle case where we timed out + var interrupted = dc.State.GetValue(TurnPath.Interrupted, () => false); + if (!interrupted && activity.Type != ActivityTypes.Message && activity.Name == ActivityEventNames.ContinueConversation) + { + string calledTimerId = ""; + //if there is no matched conversation (could be removed by endOfConversations, or there is no called timer (shouldn't happen) or + //the last called Timer is not the same as activity's timer, then don't continue + if (!convState.triggeredTimers.TryDequeue(out calledTimerId) || calledTimerId != timerId) + { + dc.Services.Get().TrackEvent("Abort Timer routine", new Dictionary + { + {"timerId", calledTimerId} + }); + return Dialog.EndOfTurn; + } + + + dc.Services.Get().TrackEvent("Continue Timer", new Dictionary + { + {"timerId", timerId} + }); + + //stop any more incoming event for this activity + convState.timersState.ForceComplete(timerId); + + //Set max turns so that we evaluate the default when we visit the inputdialog. + var oldValue = inputActivity.MaxTurnCount; + inputActivity.MaxTurnCount = 1; + + //We need to set interrupted here or it will discard the continueconversation event... + DialogTurnResult result; + try + { + dc.State.SetValue(ITimeoutInput.SilenceDetected, true); + dc.State.SetValue(TurnPath.Interrupted, true); + result = await continueDialogAsync(dc, cancellationToken).ConfigureAwait(false); + } + finally + { + inputActivity.MaxTurnCount = oldValue; + } + + return result; + } + + //continue for any other events + + //stop any more incoming event for this activity + convState.timersState.ForceComplete(timerId); + + //check if it is user input + if (activity.Type == ActivityTypes.Message) + { + dc.Services.Get().TrackEvent("User Continue", new Dictionary + { + {"timerId", timerId} + }); + + //Begin dirty hack to start a timer for the reprompt + + //If our input was not valid, restart the timer. + dc.State.SetValue(valueProperty, dc.Context.Activity.Text); //OnRecognizeInput assumes this was already set earlier on in the dialog. We will set it and then unset it to simulate passing an argument to a function... :D + + if (await onRecognizeInputAsync(dc, cancellationToken).ConfigureAwait(false) != InputState.Valid) + { + var turnCount = dc.State.GetValue(turnCountProperty, () => 0); + if (turnCount < inputActivity.MaxTurnCount?.GetValue(dc.State)) + { + //We are cheating to force this recognition here. Maybe not good? + + //Known bug: Sometimes invalid input gets accepted anyway(due to max turns and defaulting rules), this will start a continuation for a thing it shouldn't. + //Sure do wish EndDialog was available to the adaptive stack. + + timerId = inputActivity.CreateTimerForConversation(dc, cancellationToken); + await convState.timersState.StartAsync(timerId).ConfigureAwait(false); + } + } + + //Clear our the input property after recognition since it will happen again later :D + dc.State.SetValue(valueProperty, null); + + + //End dirty hack + } + + return await continueDialogAsync(dc, cancellationToken).ConfigureAwait(false); + }, + () => + { + dc.Services.Get().TrackEvent("Stop the routine", new Dictionary + { + {"timerId", timerId} + }); + return Task.FromResult(Dialog.EndOfTurn); + }); + } + + public static string CreateTimerForConversation(this K inputActivity, DialogContext dc, CancellationToken cancellationToken) where K : InputDialog, ITimeoutInput + { + var timerId = Guid.NewGuid().ToString(); + dc.State.SetValue(TimerID, timerId); + BotAdapter adapter = dc.Context.Adapter; + var identity = dc.Context.TurnState.Get("BotIdentity"); + + var appId = identity?.Claims?.FirstOrDefault(c => c.Type == AuthenticationConstants.AudienceClaim)?.Value; + ConversationReference conversationReference = dc.Context.Activity.GetConversationReference(); + + dc.Services.Get().TrackEvent("Creating Timer", new Dictionary + { + {"timerId", timerId} + }); + + int timeout = inputActivity.TimeOutInMilliseconds.GetValue(dc.State); + + //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 (!conversationStateMatrix.TryGetValue(conversationReference.Conversation.Id, out var convState)) + return; + convState.triggeredTimers.Enqueue(timerId); + // If the channel is the Emulator, and authentication is not in use, + // the AppId will be null. We generate a random AppId for this case only. + // This is not required for production, since the AppId will have a value. + if (string.IsNullOrEmpty(appId)) + { + appId = Guid.NewGuid().ToString(); //if no AppId, use a random Guid + } + + + dc.Services.Get().TrackEvent("Timer Triggered", new Dictionary + { + {"timerId", timerId} + }); + await adapter.ContinueConversationAsync( + appId, + conversationReference, + BotWithLookup.OnTurn, //Leverage dirty hack to achieve Bot lookup from component + cancellationToken).ConfigureAwait(false); + }); + return timerId; + } + + public static void RemoveTimers(ITurnContext turnContext) + { + conversationStateMatrix.TryRemove(turnContext.Activity.Conversation.Id, out _); + } + } +} \ No newline at end of file diff --git a/packages/Telephony/Actions/TimeoutTextInput.cs b/packages/Telephony/Actions/TimeoutTextInput.cs new file mode 100644 index 0000000000..9c4c4f187f --- /dev/null +++ b/packages/Telephony/Actions/TimeoutTextInput.cs @@ -0,0 +1,54 @@ +using System; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using AdaptiveExpressions.Properties; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Adapters; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Builder.Dialogs.Adaptive.Input; +using Microsoft.Bot.Components.Telephony.Common; +using Microsoft.Bot.Connector.Authentication; +using Microsoft.Bot.Schema; +using Newtonsoft.Json; + +namespace Microsoft.Bot.Components.Telephony.Actions +{ + public class TimeoutTextInput : TextInput, ITimeoutInput + { + [JsonProperty("$kind")] + public new const string Kind = "Microsoft.Telephony.TimeoutTextInput"; + + /// + /// Gets or sets a value indicating how long to wait for before timing out and using the default value. + /// + [JsonProperty("timeOutInMilliseconds")] + public IntExpression TimeOutInMilliseconds { get; set; } + + [JsonConstructor] + public TimeoutTextInput([CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0) + : base() + { + // enable instances of this command as debug break point + this.RegisterSourceLocation(sourceFilePath, sourceLineNumber); + } + + public async override Task BeginDialogAsync(DialogContext dc, object options = null, CancellationToken cancellationToken = default(CancellationToken)) + { + return await TimeoutInput.BeginDialogAsync(this, dc, + base.BeginDialogAsync, + options, cancellationToken); + } + + public override async Task ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken)) + { + return await TimeoutInput.ContinueDialogAsync(this, dc, VALUE_PROPERTY, TURN_COUNT_PROPERTY, + OnRecognizeInputAsync, + base.ContinueDialogAsync, + cancellationToken); + } + + } +} \ No newline at end of file diff --git a/packages/Telephony/Common/IStateMatrix.cs b/packages/Telephony/Common/IStateMatrix.cs index da4a1359a9..783e90d42b 100644 --- a/packages/Telephony/Common/IStateMatrix.cs +++ b/packages/Telephony/Common/IStateMatrix.cs @@ -24,9 +24,10 @@ internal interface IStateMatrix Task GetStatusForIdAsync(string id); Task CompleteAsync(string id); + void ForceComplete(string id); Task StartAsync(string id); - Task RunForStatusAsync(string id, StateStatus status, Func action); + Task RunForStatusAsync(string id, StateStatus status, Func> matchedAction, Func> notMatchedAction); } } diff --git a/packages/Telephony/Common/LatchingStateMatrix.cs b/packages/Telephony/Common/LatchingStateMatrix.cs index 8d08045784..94335d5f5a 100644 --- a/packages/Telephony/Common/LatchingStateMatrix.cs +++ b/packages/Telephony/Common/LatchingStateMatrix.cs @@ -27,6 +27,19 @@ public async Task CompleteAsync(string id) } } + public void ForceComplete(string id) + { + (SemaphoreSlim semaphore, _) = perStateIndexLatching[id]; + try + { + perStateIndexLatching[id] = (semaphore, StateStatus.Completed); + } + finally + { + semaphore.Release(); + } + } + public async Task GetStatusForIdAsync(string id) { (SemaphoreSlim semaphore, _) = perStateIndexLatching[id]; @@ -44,7 +57,7 @@ public async Task GetStatusForIdAsync(string id) } } - public async Task RunForStatusAsync(string id, StateStatus status, Func action) + public async Task RunForStatusAsync(string id, StateStatus status, Func> matchedAction, Func> notMatchedAction) { (SemaphoreSlim semaphore, _) = perStateIndexLatching[id]; try @@ -54,9 +67,8 @@ public async Task RunForStatusAsync(string id, StateStatus status, Func ac //retrieve status again, since we got it wrong the first time XD (_, StateStatus updatedStatus) = perStateIndexLatching[id]; if (updatedStatus == status) - { - await action().ConfigureAwait(false); - } + return await matchedAction().ConfigureAwait(false); + else return await notMatchedAction().ConfigureAwait(false); } finally { diff --git a/packages/Telephony/Microsoft.Bot.Components.Telephony.csproj b/packages/Telephony/Microsoft.Bot.Components.Telephony.csproj index 4f38a5a37a..506fd2d509 100644 --- a/packages/Telephony/Microsoft.Bot.Components.Telephony.csproj +++ b/packages/Telephony/Microsoft.Bot.Components.Telephony.csproj @@ -2,7 +2,7 @@ Library - netstandard2.0 + netstandard2.1 @@ -19,6 +19,7 @@ + @@ -51,12 +52,16 @@ + + $(NoWarn),SA0001,SA1649 + False + False diff --git a/packages/Telephony/Middleware/ContextReceiverMiddleware.cs b/packages/Telephony/Middleware/ContextReceiverMiddleware.cs new file mode 100644 index 0000000000..6f1b59b611 --- /dev/null +++ b/packages/Telephony/Middleware/ContextReceiverMiddleware.cs @@ -0,0 +1,60 @@ +// --------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// --------------------------------------------------------------------------- + +using Microsoft.Bot.Builder; +using Microsoft.Bot.Schema; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Bot.Components.Telephony.Middleware +{ + /// + /// The middleware that handles all outgoing messages + /// + public class ContextReceiverMiddleware : IMiddleware + { + + public delegate void EventReceiverHandler(ITurnContext turnContext); + private Dictionary> _receivers; + + /// + /// Initializes a new SetSpeakMiddleware class + /// + public ContextReceiverMiddleware() + { + _receivers = new Dictionary>(); + } + + /// + /// Handles the outgoing message + /// + /// The turn context + /// The next delegate + /// The cancellation token + /// + public async Task OnTurnAsync(ITurnContext turnContext, NextDelegate next, CancellationToken cancellationToken = default) + { + if (!string.IsNullOrEmpty(turnContext.Activity?.Type) && _receivers.TryGetValue(turnContext.Activity.Type, out var receivers)) + receivers.ForEach(rc => rc.Invoke(turnContext)); + + await next(cancellationToken); + } + + public void addEventReceiver(string eventName, EventReceiverHandler handler) + { + if (!_receivers.ContainsKey(eventName)) _receivers.Add(eventName, new List()); + _receivers[eventName].Add(handler); + } + + public void removeEventReceiver(string eventName, EventReceiverHandler handler) + { + if (!_receivers.ContainsKey(eventName)) return; + _receivers[eventName].Remove(handler); + } + + } +} diff --git a/packages/Telephony/Schemas/Microsoft.Telephony.TimeoutChoiceInput.schema b/packages/Telephony/Schemas/Microsoft.Telephony.TimeoutChoiceInput.schema index 9db6c1e4a9..8e91a522fe 100644 --- a/packages/Telephony/Schemas/Microsoft.Telephony.TimeoutChoiceInput.schema +++ b/packages/Telephony/Schemas/Microsoft.Telephony.TimeoutChoiceInput.schema @@ -1,7 +1,7 @@ { "$schema": "https://schemas.botframework.com/schemas/component/v1.0/component.schema", "$role": [ "implements(Microsoft.IDialog)", "extends(Microsoft.InputDialog)" ], - "title": "(Preview) Choice input dialog with silence detection", + "title": "Choice input dialog with silence detection", "description": "Collect information - Pick from a list of choices", "type": "object", "properties": { diff --git a/packages/Telephony/Schemas/Microsoft.Telephony.TimeoutTextInput.schema b/packages/Telephony/Schemas/Microsoft.Telephony.TimeoutTextInput.schema new file mode 100644 index 0000000000..8515dd7d48 --- /dev/null +++ b/packages/Telephony/Schemas/Microsoft.Telephony.TimeoutTextInput.schema @@ -0,0 +1,50 @@ +{ + "$schema": "https://schemas.botframework.com/schemas/component/v1.0/component.schema", + "$role": [ "implements(Microsoft.IDialog)", "extends(Microsoft.InputDialog)" ], + "type": "object", + "title": "Text input dialog with silence detection", + "description": "Collection information - Ask for a word or sentence.", + "properties": { + "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 max turn count is exceeded.", + "examples": [ + "hello world", + "Hello ${user.name}", + "=concat(user.firstname, user.lastName)" + ] + }, + "value": { + "$ref": "schema:#/definitions/stringExpression", + "title": "Value", + "description": "'Property' will be set to the value of this expression unless it evaluates to null.", + "examples": [ + "hello world", + "Hello ${user.name}", + "=concat(user.firstname, user.lastName)" + ] + }, + "outputFormat": { + "$ref": "schema:#/definitions/stringExpression", + "title": "Output format", + "description": "Expression to format the output.", + "examples": [ + "=toUpper(this.value)", + "${toUpper(this.value)}" + ] + } + }, + "$policies": { + "interactive": true + } +} diff --git a/packages/Telephony/Schemas/Microsoft.Telephony.TimeoutTextInput.uischema b/packages/Telephony/Schemas/Microsoft.Telephony.TimeoutTextInput.uischema new file mode 100644 index 0000000000..8f7965aa25 --- /dev/null +++ b/packages/Telephony/Schemas/Microsoft.Telephony.TimeoutTextInput.uischema @@ -0,0 +1,63 @@ +{ + "$schema": "https://schemas.botframework.com/schemas/ui/v1.0/ui.schema", + "form": { + "label": "Prompt for text", + "subtitle": "Text Input", + "helpLink": "https://aka.ms/bfc-ask-for-user-input", + "order": [ + "prompt", + "timeOutInMilliseconds", + "*" + ] + "properties": { + "property": { + "intellisenseScopes": [ + "variable-scopes" + ] + } + } + }, + "menu": { + "label": "Text input | Preview", + "submenu": [ "Silence Detection" ] + }, + "flow": { + "widget": "PromptWidget", + "body": "=action.prompt", + "nowrap": true, + "botAsks": { + "widget": "ActionCard", + "header": { + "widget": "ActionHeader", + "icon": "MessageBot", + "colors": { + "theme": "#EEEAF4", + "icon": "#5C2E91" + } + }, + "body": { + "widget": "LgWidget", + "field": "prompt", + "defaultContent": "" + } + }, + "userInput": { + "widget": "ActionCard", + "header": { + "widget": "ActionHeader", + "disableSDKTitle": true, + "icon": "User", + "menu": "none", + "colors": { + "theme": "#E5F0FF", + "icon": "#0078D4" + } + }, + "body": { + "widget": "LgWidget", + "field": "prompt", + "defaultContent": "" + } + } + } +} diff --git a/packages/Telephony/TelephonyBotComponent.cs b/packages/Telephony/TelephonyBotComponent.cs index ab9cf9e4be..0020fb8bcd 100644 --- a/packages/Telephony/TelephonyBotComponent.cs +++ b/packages/Telephony/TelephonyBotComponent.cs @@ -6,6 +6,7 @@ namespace Microsoft.Bot.Components.Telephony using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Dialogs.Declarative; using Microsoft.Bot.Components.Telephony.Actions; + using Microsoft.Bot.Schema; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -18,6 +19,10 @@ public class TelephonyBotComponent : BotComponent public override void ConfigureServices(IServiceCollection services, IConfiguration configuration) { // Conditionals + var middleware = new Middleware.ContextReceiverMiddleware(); + services.AddSingleton(middleware); + middleware.addEventReceiver(ActivityTypes.EndOfConversation, TimeoutInput.RemoveTimers); + services.AddSingleton(sp => new DeclarativeType(CallTransfer.Kind)); services.AddSingleton(sp => new DeclarativeType(PauseRecording.Kind)); services.AddSingleton(sp => new DeclarativeType(ResumeRecording.Kind)); @@ -26,6 +31,7 @@ public override void ConfigureServices(IServiceCollection services, IConfigurati services.AddSingleton(sp => new DeclarativeType(BatchTerminationCharacterInput.Kind)); services.AddSingleton(sp => new DeclarativeType(BatchRegexInput.Kind)); services.AddSingleton(sp => new DeclarativeType(TimeoutChoiceInput.Kind)); + services.AddSingleton(sp => new DeclarativeType(TimeoutTextInput.Kind)); services.AddSingleton(sp => new DeclarativeType(SerialNumberInput.Kind)); } }