add support for retry logic and dtmf

This commit is contained in:
Dave Horton
2022-01-26 14:01:38 -05:00
parent dcd6ddcbca
commit 8c00c89882
4 changed files with 102 additions and 26 deletions

View File

@@ -3,6 +3,7 @@ const {TaskName, TaskPreconditions} = require('../../utils/constants');
const makeTask = require('../make_task');
const { SocketClient } = require('@cognigy/socket-client');
const SpeechConfig = require('./speech-config');
const { IoTThingsGraph } = require('aws-sdk');
const parseGallery = (obj = {}) => {
const {_default} = obj;
@@ -46,6 +47,8 @@ class Cognigy extends Task {
this.actionHook = this.data?.actionHook;
this.data = this.data.data || {};
this.prompts = [];
this.retry = {};
this.timeoutCount = 0;
}
get name() { return TaskName.Cognigy; }
@@ -80,6 +83,7 @@ class Cognigy extends Task {
/* set event handlers and start transcribing */
this.on('transcription', this._onTranscription.bind(this, cs, ep));
this.on('dtmf-collected', this._onDtmf.bind(this, cs, ep));
this.on('timeout', this._onTimeout.bind(this, cs, ep));
this.on('error', this._onError.bind(this, cs, ep));
@@ -132,9 +136,11 @@ class Cognigy extends Task {
this.notifyTaskDone();
}
_makeGatherTask(prompt) {
const config = this.config.makeGatherTaskConfig(prompt);
const gather = makeTask(this.logger, {gather: config}, this);
_makeGatherTask({textPrompt, urlPrompt}) {
const config = this.config.makeGatherTaskConfig({textPrompt, urlPrompt});
const {retry, ...rest} = config;
this.retry = retry;
const gather = makeTask(this.logger, {gather: rest}, this);
return gather;
}
@@ -148,7 +154,6 @@ class Cognigy extends Task {
voice: 'default'
}
};
this.logger.debug({opts}, 'constructing a nested say object');
const say = makeTask(this.logger, {say: opts}, this);
return say;
}
@@ -165,7 +170,7 @@ class Cognigy extends Task {
if (this.prompts.length) {
const text = this.prompts.join('.');
if (text && !this.killed) {
this.gatherTask = this._makeGatherTask(text);
this.gatherTask = this._makeGatherTask({textPrompt: text});
this.gatherTask.exec(cs, ep, this)
.catch((err) => this.logger.info({err}, 'Cognigy gather task returned error'));
}
@@ -230,18 +235,45 @@ class Cognigy extends Task {
this.notifyTaskDone();
}
}
_onDtmf(cs, ep, evt) {
this.logger.info({evt}, 'got dtmf');
/* send dtmf to bot */
try {
if (this.client && this.client.connected) {
this.client.sendMessage(evt.digits);
}
else {
this.logger.info('Cognigy_onTranscription - not sending user dtmf as bot is disconnected');
}
} catch (err) {
this.logger.error({err}, '_onDtmf: Error sending user dtmf to Cognigy - ending task');
this.performAction({cognigyResult: 'socketError'});
this.reportedFinalAction = true;
this.notifyTaskDone();
}
}
_onError(cs, ep, err) {
this.logger.debug({err}, 'Cognigy: got error');
this.logger.info({err}, 'Cognigy: got error');
if (!this.hasReportedFinalAction) this.performAction({cognigyResult: 'error', err});
this.reportedFinalAction = true;
this.notifyTaskDone();
}
_onTimeout(cs, ep, evt) {
this.logger.debug({evt}, 'Cognigy: got timeout');
if (!this.hasReportedFinalAction) this.performAction({cognigyResult: 'timeout'});
this.reportedFinalAction = true;
this.notifyTaskDone();
const {noInputRetries, noInputSpeech, noInputUrl} = this.retry;
this.logger.debug({evt, retry: this.retry}, 'Cognigy: got timeout');
if (noInputRetries && this.timeoutCount++ < noInputRetries) {
this.gatherTask = this._makeGatherTask({textPrompt: noInputSpeech, urlPrompt: noInputUrl});
this.gatherTask.exec(cs, ep, this)
.catch((err) => this.logger.info({err}, 'Cognigy gather task returned error'));
}
else {
if (!this.hasReportedFinalAction) this.performAction({cognigyResult: 'timeout'});
this.reportedFinalAction = true;
this.notifyTaskDone();
}
}
}

View File

@@ -2,6 +2,11 @@ const Emitter = require('events');
const hasKeys = (obj) => typeof obj === 'object' && Object.keys(obj) > 0;
const stripNulls = (obj) => {
Object.keys(obj).forEach((k) => (obj[k] === null || typeof obj[k] === 'undefined') && delete obj[k]);
return obj;
};
class SpeechConfig extends Emitter {
constructor({logger, ep, opts = {}}) {
super();
@@ -19,7 +24,7 @@ class SpeechConfig extends Emitter {
this.logger.debug({opts, sessionLevel: this.sessionConfig, turnLevel: this.turnConfig}, 'SpeechConfig updated');
}
makeGatherTaskConfig(prompt) {
makeGatherTaskConfig({textPrompt, urlPrompt}) {
const opts = JSON.parse(JSON.stringify(this.sessionConfig || {}));
const nextTurnKeys = Object.keys(this.turnConfig || {});
const newKeys = nextTurnKeys.filter((k) => !(k in opts));
@@ -42,26 +47,43 @@ class SpeechConfig extends Emitter {
/* bargein settings */
const bargein = opts.bargein || {};
const speechBargein = Array.isArray(bargein.enable) && bargein.enable.includes('speech');
const dtmfBargein = Array.isArray(bargein.enable) && bargein.enable.includes('dtmf');
const minBargeinWordCount = speechBargein ? (bargein.minWordCount || 1) : 0;
const {interDigitTimeout=0, maxDigits, minDigits=1, submitDigit} = (opts.dtmf || {});
const {noInputTimeout, noInputRetries, noInputSpeech, noInputUrl} = (opts.user || {});
const sayConfig = {
text: prompt,
text: textPrompt,
synthesizer: opts.synthesizer
};
const playConfig = {
url: urlPrompt
};
const config = {
input,
listenDuringPrompt: speechBargein,
bargein: speechBargein,
minBargeinWordCount,
dtmfBargein,
minDigits,
maxDigits,
interDigitTimeout,
finishOnKey: submitDigit,
recognizer: opts?.recognizer,
timeout: opts?.user?.noInputTimeout || 0,
say: sayConfig
timeout: noInputTimeout,
retry : {
noInputRetries,
noInputSpeech,
noInputUrl
}
};
this.logger.debug({config}, 'Congigy SpeechConfig:_makeGatherTask config');
const final = stripNulls(config);
/* turn config can now be emptied for next turn of conversation */
this.turnConfig = {};
return config;
return textPrompt ?
{...final, say: sayConfig} :
{...final, play: playConfig};
}
}

View File

@@ -16,14 +16,15 @@ class TaskGather extends Task {
this.preconditions = TaskPreconditions.Endpoint;
[
'finishOnKey', 'hints', 'input', 'numDigits',
'partialResultHook', 'bargein',
'finishOnKey', 'hints', 'input', 'numDigits', 'minDigits', 'maxDigits',
'interDigitTimeout', 'submitDigit', 'partialResultHook', 'bargein', 'dtmfBargein',
'retries', 'retryPromptTts', 'retryPromptUrl',
'speechTimeout', 'timeout', 'say', 'play'
].forEach((k) => this[k] = this.data[k]);
this.listenDuringPrompt = this.data.listenDuringPrompt === false ? false : true;
this.minBargeinWordCount = this.data.minBargeinWordCount || 1;
this.timeout = (this.timeout || 15) * 1000;
this.interim = this.partialResultCallback;
this.interim = this.partialResultCallback || this.bargein;
if (this.data.recognizer) {
const recognizer = this.data.recognizer;
this.vendor = recognizer.vendor;
@@ -119,7 +120,7 @@ class TaskGather extends Task {
.catch(() => {/*already logged error */});
}
if (this.input.includes('digits')) {
if (this.input.includes('digits') || this.dtmfBargein) {
ep.on('dtmf', this._onDtmf.bind(this, cs, ep));
}
@@ -145,12 +146,28 @@ class TaskGather extends Task {
_onDtmf(cs, ep, evt) {
this.logger.debug(evt, 'TaskGather:_onDtmf');
if (evt.dtmf === this.finishOnKey) this._resolve('dtmf-terminator-key');
clearTimeout(this.interDigitTimer);
let resolved = false;
if (this.dtmfBargein) this._killAudio(cs);
if (evt.dtmf === this.finishOnKey) {
resolved = true;
this._resolve('dtmf-terminator-key');
}
else {
this.digitBuffer += evt.dtmf;
if (this.digitBuffer.length === this.numDigits) this._resolve('dtmf-num-digits');
const len = this.digitBuffer.length;
if (len === this.numDigits || len === this.maxDigits) {
resolved = true;
this._resolve('dtmf-num-digits');
}
}
if (!resolved && this.interDigitTimeout > 0 && this.digitBuffer.length >= this.minDigits) {
/* start interDigitTimer */
const ms = this.interDigitTimeout * 1000;
this.logger.debug(`starting interdigit timer of ${ms}`);
this.interDigitTimer = setTimeout(() => this._resolve('dtmf-interdigit-timeout'), ms);
}
this._killAudio(cs);
}
async _initSpeech(cs, ep) {
@@ -216,7 +233,7 @@ class TaskGather extends Task {
ep.startTranscription({
vendor: this.vendor,
locale: this.language,
interim: this.partialResultCallback || this.bargein,
interim: this.interim,
}).catch((err) => {
const {writeAlerts, AlertType} = this.cs.srf.locals;
this.logger.error(err, 'TaskGather:_startTranscribing error');
@@ -280,7 +297,7 @@ class TaskGather extends Task {
transcript: evt.Text
}
]
};
}
}
}
if (evt.is_final) this._resolve('speech', evt);
@@ -318,7 +335,8 @@ class TaskGather extends Task {
this._clearTimer();
if (reason.startsWith('dtmf')) {
await this.performAction({digits: this.digitBuffer, reason: 'dtmfDetected'});
if (this.parentTask) this.parentTask.emit('dtmf-collected', {reason, digits: this.digitBuffer});
else await this.performAction({digits: this.digitBuffer, reason: 'dtmfDetected'});
}
else if (reason.startsWith('speech')) {
if (this.parentTask) this.parentTask.emit('transcription', evt);

View File

@@ -103,6 +103,10 @@
"listenDuringPrompt": "boolean",
"bargein": "boolean",
"minBargeinWordCount": "number",
"dtmfBargein": "boolean",
"minDigits": "number",
"maxDigits": "number",
"interDigitTimeout": "number",
"timeout": "number",
"recognizer": "#recognizer",
"play": "#play",