botbuilder-dialogs-adaptive
Version:
Rule system for the Microsoft BotBuilder dialog system.
422 lines • 20 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.InputDialog = exports.InputState = void 0;
/**
* @module botbuilder-dialogs-adaptive
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
const templates_1 = require("../templates");
const converters_1 = require("../converters");
const botbuilder_1 = require("botbuilder");
const adaptiveEvents_1 = require("../adaptiveEvents");
const attachmentInput_1 = require("./attachmentInput");
const telemetryLoggerConstants_1 = require("../telemetryLoggerConstants");
const adaptive_expressions_1 = require("adaptive-expressions");
const botbuilder_dialogs_1 = require("botbuilder-dialogs");
var InputState;
(function (InputState) {
InputState["missing"] = "missing";
InputState["unrecognized"] = "unrecognized";
InputState["invalid"] = "invalid";
InputState["valid"] = "valid";
})(InputState = exports.InputState || (exports.InputState = {}));
/**
* Defines input dialogs.
*/
class InputDialog extends botbuilder_dialogs_1.Dialog {
/**
* Initializes a new instance of the [InputDialog](xref:botbuilder-dialogs-adaptive.InputDialog) class
*
* @param property Optional. The value expression which the input will be bound to.
* @param prompt Optional. The [Activity](xref:botframework-schema.Activity) to send to the user,
* if a string is specified it will instantiates an [ActivityTemplate](xref:botbuilder-dialogs-adaptive.ActivityTemplate).
*/
constructor(property, prompt) {
super();
/**
* The expressions to run to validate the input.
*/
this.validations = [];
if (property) {
this.property = new adaptive_expressions_1.StringExpression(property);
}
if (prompt) {
if (typeof prompt === 'string') {
this.prompt = new templates_1.ActivityTemplate(prompt);
}
else {
this.prompt = new templates_1.StaticActivityTemplate(prompt);
}
}
}
/**
* @param property The key of the conditional selector configuration.
* @returns The converter for the selector configuration.
*/
getConverter(property) {
switch (property) {
case 'alwaysPrompt':
return new adaptive_expressions_1.BoolExpressionConverter();
case 'allowInterruptions':
return new adaptive_expressions_1.BoolExpressionConverter();
case 'property':
return new adaptive_expressions_1.StringExpressionConverter();
case 'value':
return new adaptive_expressions_1.ValueExpressionConverter();
case 'prompt':
return new converters_1.ActivityTemplateConverter();
case 'unrecognizedPrompt':
return new converters_1.ActivityTemplateConverter();
case 'invalidPrompt':
return new converters_1.ActivityTemplateConverter();
case 'defaultValueResponse':
return new converters_1.ActivityTemplateConverter();
case 'maxTurnCount':
return new adaptive_expressions_1.IntExpressionConverter();
case 'defaultValue':
return new adaptive_expressions_1.ValueExpressionConverter();
case 'disabled':
return new adaptive_expressions_1.BoolExpressionConverter();
default:
return super.getConverter(property);
}
}
/**
* Called when the [Dialog](xref:botbuilder-dialogs.Dialog) is started and pushed onto the dialog stack.
*
* @param dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation.
* @param options Optional. Initial information to pass to the [Dialog](xref:botbuilder-dialogs.Dialog).
* @returns A [DialogTurnResult](xref:botbuilder-dialogs.DialogTurnResult) `Promise` representing the asynchronous operation.
*/
beginDialog(dc, options) {
return __awaiter(this, void 0, void 0, function* () {
if (this.disabled && this.disabled.getValue(dc.state)) {
return yield dc.endDialog();
}
// Initialize and persist options
const opts = yield this.onInitializeOptions(dc, options || {});
dc.state.setValue(InputDialog.OPTIONS_PROPERTY, opts);
// Initialize turn count & input
dc.state.setValue(InputDialog.TURN_COUNT_PROPERTY, 0);
if (this.property && this.alwaysPrompt && this.alwaysPrompt.getValue(dc.state)) {
dc.state.deleteValue(this.property.getValue(dc.state));
}
// Recognize input
const state = this.alwaysPrompt && this.alwaysPrompt.getValue(dc.state)
? InputState.missing
: yield this.recognizeInput(dc, 0);
if (state == InputState.valid) {
// Return input
const property = this.property.getValue(dc.state);
const value = dc.state.getValue(InputDialog.VALUE_PROPERTY);
dc.state.setValue(property, value);
return yield dc.endDialog(value);
}
else {
// Prompt user
dc.state.setValue(InputDialog.TURN_COUNT_PROPERTY, 1);
return yield this.promptUser(dc, state);
}
});
}
/**
* Called when the [Dialog](xref:botbuilder-dialogs.Dialog) is _continued_, where it is the active dialog and the user replies with a new activity.
*
* @param dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation.
* @returns A [DialogTurnResult](xref:botbuilder-dialogs.DialogTurnResult) `Promise` representing the asynchronous operation.
*/
continueDialog(dc) {
return __awaiter(this, void 0, void 0, function* () {
const activity = dc.context.activity;
// Interrupted dialogs reprompt so we can ignore the incoming activity.
const interrupted = dc.state.getValue(botbuilder_dialogs_1.TurnPath.interrupted, false);
if (!interrupted && activity.type !== botbuilder_1.ActivityTypes.Message) {
return botbuilder_dialogs_1.Dialog.EndOfTurn;
}
// Are we continuing after an interruption?
const turnCount = dc.state.getValue(InputDialog.TURN_COUNT_PROPERTY, 0);
const state = yield this.recognizeInput(dc, interrupted ? 0 : turnCount);
if (state === InputState.valid) {
const input = dc.state.getValue(InputDialog.VALUE_PROPERTY);
if (this.property) {
dc.state.setValue(this.property.getValue(dc.state), input);
}
return yield dc.endDialog(input);
}
else if (!this.maxTurnCount || turnCount < this.maxTurnCount.getValue(dc.state)) {
if (!interrupted) {
dc.state.setValue(InputDialog.TURN_COUNT_PROPERTY, turnCount + 1);
}
return yield this.promptUser(dc, state);
}
else {
if (this.defaultValue) {
if (this.defaultValueResponse) {
const response = yield this.defaultValueResponse.bind(dc, dc.state);
this.telemetryClient.trackEvent({
name: telemetryLoggerConstants_1.TelemetryLoggerConstants.GeneratorResultEvent,
properties: {
template: this.defaultValueResponse,
result: response || '',
context: telemetryLoggerConstants_1.TelemetryLoggerConstants.InputDialogResultEvent,
},
});
if (response != null) {
yield dc.context.sendActivity(response);
}
}
const property = this.property.getValue(dc.state);
const value = this.defaultValue.getValue(dc.state);
dc.state.setValue(property, value);
return yield dc.endDialog(value);
}
}
return yield dc.endDialog();
});
}
/**
* Called when a child [Dialog](xref:botbuilder-dialogs.Dialog) completes its turn, returning control to this dialog.
*
* @param dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation.
* @param _reason [DialogReason](xref:botbuilder-dialogs.DialogReason), reason why the dialog resumed.
* @param _result Optional. Value returned from the [Dialog](xref:botbuilder-dialogs.Dialog) that was called.
* The type of the value returned is dependent on the child dialog.
* @returns A [DialogTurnResult](xref:botbuilder-dialogs.DialogTurnResult) `Promise` representing the asynchronous operation.
*/
resumeDialog(dc, _reason, _result) {
return __awaiter(this, void 0, void 0, function* () {
// Re-send initial prompt
return yield this.promptUser(dc, InputState.missing);
});
}
/**
* @protected
* Called before an event is bubbled to its parent.
* @param dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation.
* @param event [DialogEvent](xref:botbuilder-dialogs.DialogEvent), the event being raised.
* @returns Whether the event is handled by the current [Dialog](xref:botbuilder-dialogs.Dialog) and further processing should stop.
*/
onPreBubbleEvent(dc, event) {
return __awaiter(this, void 0, void 0, function* () {
if (event.name === botbuilder_dialogs_1.DialogEvents.activityReceived && dc.context.activity.type === botbuilder_1.ActivityTypes.Message) {
if (dc.parent) {
yield dc.parent.emitEvent(adaptiveEvents_1.AdaptiveEvents.recognizeUtterance, dc.context.activity, false);
}
// should we allow interruptions
let canInterrupt = true;
if (this.allowInterruptions) {
const allowInterruptions = this.allowInterruptions.getValue(dc.state);
canInterrupt = !!allowInterruptions;
}
// stop bubbling if interruptions are NOT allowed
return !canInterrupt;
}
return false;
});
}
/**
* @protected
* Method which processes options.
* @param _dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation.
* @param options Initial information to pass to the dialog.
* @returns A promise representing the asynchronous operation.
*/
onInitializeOptions(_dc, options) {
return Promise.resolve(Object.assign({}, options));
}
/**
* @protected
* Method which renders the prompt to the user given the current input state.
* @param dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation.
* @param state Dialog [InputState](xref:botbuilder-dialogs-adaptive.InputState).
* @returns An [Activity](xref:botframework-schema.Activity) `Promise` representing the asynchronous operation.
*/
onRenderPrompt(dc, state) {
return __awaiter(this, void 0, void 0, function* () {
let msg;
let template;
switch (state) {
case InputState.unrecognized:
if (this.unrecognizedPrompt) {
template = this.unrecognizedPrompt;
msg = yield this.unrecognizedPrompt.bind(dc, dc.state);
}
else if (this.invalidPrompt) {
template = this.invalidPrompt;
msg = yield this.invalidPrompt.bind(dc, dc.state);
}
break;
case InputState.invalid:
if (this.invalidPrompt) {
template = this.invalidPrompt;
msg = yield this.invalidPrompt.bind(dc, dc.state);
}
else if (this.unrecognizedPrompt) {
template = this.unrecognizedPrompt;
msg = yield this.unrecognizedPrompt.bind(dc, dc.state);
}
break;
}
if (!msg) {
template = this.prompt;
if (!template)
throw new Error('InputDialog is missing Prompt.');
msg = yield this.prompt.bind(dc, dc.state);
}
if (msg != null && (typeof (msg === null || msg === void 0 ? void 0 : msg.inputHint) !== 'string' || !msg.inputHint)) {
msg.inputHint = botbuilder_1.InputHints.ExpectingInput;
}
this.trackGeneratorResultEvent(dc, template, msg);
return msg;
});
}
/**
* @protected
* Track GeneratorResultEvent telemetry event with InputDialogResultEvent context.
* @param dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation.
* @param activityTemplate used to create the Activity.
* @param msg The Partial [Activity](xref:botframework-schema.Activity) which will be sent.
*/
trackGeneratorResultEvent(dc, activityTemplate, msg) {
this.telemetryClient.trackEvent({
name: telemetryLoggerConstants_1.TelemetryLoggerConstants.GeneratorResultEvent,
properties: {
template: activityTemplate,
result: msg,
context: telemetryLoggerConstants_1.TelemetryLoggerConstants.InputDialogResultEvent,
},
});
}
/**
* Helper function to compose an output activity containing a set of choices.
*
* @param prompt The prompt to append the users choices to.
* @param channelId ID of the channel the prompt is being sent to.
* @param choices List of choices to append.
* @param style Configured style for the list of choices.
* @param options (Optional) options to configure the underlying ChoiceFactory call.
* @returns A bound activity ready to send to the user.
*/
appendChoices(prompt, channelId, choices, style, options) {
// Create temporary msg
let msg;
const text = prompt.text || '';
switch (style) {
case botbuilder_dialogs_1.ListStyle.inline:
msg = botbuilder_dialogs_1.ChoiceFactory.inline(choices, text, null, options);
break;
case botbuilder_dialogs_1.ListStyle.list:
msg = botbuilder_dialogs_1.ChoiceFactory.list(choices, text, null, options);
break;
case botbuilder_dialogs_1.ListStyle.suggestedAction:
msg = botbuilder_dialogs_1.ChoiceFactory.suggestedAction(choices, text);
break;
case botbuilder_dialogs_1.ListStyle.heroCard:
msg = botbuilder_dialogs_1.ChoiceFactory.heroCard(choices, text);
break;
case botbuilder_dialogs_1.ListStyle.none:
msg = botbuilder_1.MessageFactory.text(text);
break;
default:
msg = botbuilder_dialogs_1.ChoiceFactory.forChannel(channelId, choices, text, null, options);
break;
}
// Update clone of prompt with text, actions and attachments
const clone = JSON.parse(JSON.stringify(prompt));
clone.text = msg.text;
if (msg.suggestedActions &&
Array.isArray(msg.suggestedActions.actions) &&
msg.suggestedActions.actions.length > 0) {
clone.suggestedActions = msg.suggestedActions;
}
if (msg.attachments) {
clone.attachments = msg.attachments;
}
if (!clone.inputHint) {
clone.inputHint = botbuilder_1.InputHints.ExpectingInput;
}
return clone;
}
/**
* @private
*/
recognizeInput(dc, turnCount) {
return __awaiter(this, void 0, void 0, function* () {
let input;
if (this.property) {
const property = this.property.getValue(dc.state);
input = dc.state.getValue(property);
dc.state.deleteValue(property);
}
if (!input && this.value) {
const value = this.value.getValue(dc.state);
input = value;
}
const activityProcessed = dc.state.getValue(botbuilder_dialogs_1.TurnPath.activityProcessed);
if (!activityProcessed && !input && turnCount > 0) {
if (this instanceof attachmentInput_1.AttachmentInput) {
input = dc.context.activity.attachments || [];
}
else {
input = dc.context.activity.text;
// if there is no visible text AND we have a value object, then fallback to that.
if (!input && dc.context.activity.value != undefined) {
input = dc.context.activity.value;
}
}
}
dc.state.setValue(InputDialog.VALUE_PROPERTY, input);
if (input) {
const state = yield this.onRecognizeInput(dc);
if (state == InputState.valid) {
for (let i = 0; i < this.validations.length; i++) {
const validation = this.validations[i];
const exp = new adaptive_expressions_1.ExpressionParser().parse(validation);
const { value } = exp.tryEvaluate(dc.state);
if (!value) {
return InputState.invalid;
}
}
dc.state.setValue(botbuilder_dialogs_1.TurnPath.activityProcessed, true);
return InputState.valid;
}
else {
return state;
}
}
else {
return InputState.missing;
}
});
}
/**
* @private
*/
promptUser(dc, state) {
return __awaiter(this, void 0, void 0, function* () {
const prompt = yield this.onRenderPrompt(dc, state);
if (prompt == null) {
throw new Error(`Call to onRenderPrompt() returned a null activity for state ${state}.`);
}
yield dc.context.sendActivity(prompt);
return botbuilder_dialogs_1.Dialog.EndOfTurn;
});
}
}
exports.InputDialog = InputDialog;
InputDialog.OPTIONS_PROPERTY = 'this.options';
InputDialog.VALUE_PROPERTY = 'this.value';
InputDialog.TURN_COUNT_PROPERTY = 'this.turnCount';
//# sourceMappingURL=inputDialog.js.map