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 makeTask = require('../make_task');
const { SocketClient } = require('@cognigy/socket-client'); const { SocketClient } = require('@cognigy/socket-client');
const SpeechConfig = require('./speech-config'); const SpeechConfig = require('./speech-config');
const { IoTThingsGraph } = require('aws-sdk');
const parseGallery = (obj = {}) => { const parseGallery = (obj = {}) => {
const {_default} = obj; const {_default} = obj;
@@ -46,6 +47,8 @@ class Cognigy extends Task {
this.actionHook = this.data?.actionHook; this.actionHook = this.data?.actionHook;
this.data = this.data.data || {}; this.data = this.data.data || {};
this.prompts = []; this.prompts = [];
this.retry = {};
this.timeoutCount = 0;
} }
get name() { return TaskName.Cognigy; } get name() { return TaskName.Cognigy; }
@@ -80,6 +83,7 @@ class Cognigy extends Task {
/* set event handlers and start transcribing */ /* set event handlers and start transcribing */
this.on('transcription', this._onTranscription.bind(this, cs, ep)); 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('timeout', this._onTimeout.bind(this, cs, ep));
this.on('error', this._onError.bind(this, cs, ep)); this.on('error', this._onError.bind(this, cs, ep));
@@ -132,9 +136,11 @@ class Cognigy extends Task {
this.notifyTaskDone(); this.notifyTaskDone();
} }
_makeGatherTask(prompt) { _makeGatherTask({textPrompt, urlPrompt}) {
const config = this.config.makeGatherTaskConfig(prompt); const config = this.config.makeGatherTaskConfig({textPrompt, urlPrompt});
const gather = makeTask(this.logger, {gather: config}, this); const {retry, ...rest} = config;
this.retry = retry;
const gather = makeTask(this.logger, {gather: rest}, this);
return gather; return gather;
} }
@@ -148,7 +154,6 @@ class Cognigy extends Task {
voice: 'default' voice: 'default'
} }
}; };
this.logger.debug({opts}, 'constructing a nested say object');
const say = makeTask(this.logger, {say: opts}, this); const say = makeTask(this.logger, {say: opts}, this);
return say; return say;
} }
@@ -165,7 +170,7 @@ class Cognigy extends Task {
if (this.prompts.length) { if (this.prompts.length) {
const text = this.prompts.join('.'); const text = this.prompts.join('.');
if (text && !this.killed) { if (text && !this.killed) {
this.gatherTask = this._makeGatherTask(text); this.gatherTask = this._makeGatherTask({textPrompt: text});
this.gatherTask.exec(cs, ep, this) this.gatherTask.exec(cs, ep, this)
.catch((err) => this.logger.info({err}, 'Cognigy gather task returned error')); .catch((err) => this.logger.info({err}, 'Cognigy gather task returned error'));
} }
@@ -230,19 +235,46 @@ class Cognigy extends Task {
this.notifyTaskDone(); 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) { _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}); if (!this.hasReportedFinalAction) this.performAction({cognigyResult: 'error', err});
this.reportedFinalAction = true; this.reportedFinalAction = true;
this.notifyTaskDone(); this.notifyTaskDone();
} }
_onTimeout(cs, ep, evt) { _onTimeout(cs, ep, evt) {
this.logger.debug({evt}, 'Cognigy: got timeout'); 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'}); if (!this.hasReportedFinalAction) this.performAction({cognigyResult: 'timeout'});
this.reportedFinalAction = true; this.reportedFinalAction = true;
this.notifyTaskDone(); this.notifyTaskDone();
} }
} }
}
module.exports = Cognigy; module.exports = Cognigy;

View File

@@ -2,6 +2,11 @@ const Emitter = require('events');
const hasKeys = (obj) => typeof obj === 'object' && Object.keys(obj) > 0; 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 { class SpeechConfig extends Emitter {
constructor({logger, ep, opts = {}}) { constructor({logger, ep, opts = {}}) {
super(); super();
@@ -19,7 +24,7 @@ class SpeechConfig extends Emitter {
this.logger.debug({opts, sessionLevel: this.sessionConfig, turnLevel: this.turnConfig}, 'SpeechConfig updated'); 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 opts = JSON.parse(JSON.stringify(this.sessionConfig || {}));
const nextTurnKeys = Object.keys(this.turnConfig || {}); const nextTurnKeys = Object.keys(this.turnConfig || {});
const newKeys = nextTurnKeys.filter((k) => !(k in opts)); const newKeys = nextTurnKeys.filter((k) => !(k in opts));
@@ -42,26 +47,43 @@ class SpeechConfig extends Emitter {
/* bargein settings */ /* bargein settings */
const bargein = opts.bargein || {}; const bargein = opts.bargein || {};
const speechBargein = Array.isArray(bargein.enable) && bargein.enable.includes('speech'); 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 minBargeinWordCount = speechBargein ? (bargein.minWordCount || 1) : 0;
const {interDigitTimeout=0, maxDigits, minDigits=1, submitDigit} = (opts.dtmf || {});
const {noInputTimeout, noInputRetries, noInputSpeech, noInputUrl} = (opts.user || {});
const sayConfig = { const sayConfig = {
text: prompt, text: textPrompt,
synthesizer: opts.synthesizer synthesizer: opts.synthesizer
}; };
const playConfig = {
url: urlPrompt
};
const config = { const config = {
input, input,
listenDuringPrompt: speechBargein, listenDuringPrompt: speechBargein,
bargein: speechBargein, bargein: speechBargein,
minBargeinWordCount, minBargeinWordCount,
dtmfBargein,
minDigits,
maxDigits,
interDigitTimeout,
finishOnKey: submitDigit,
recognizer: opts?.recognizer, recognizer: opts?.recognizer,
timeout: opts?.user?.noInputTimeout || 0, timeout: noInputTimeout,
say: sayConfig 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 */ /* turn config can now be emptied for next turn of conversation */
this.turnConfig = {}; 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; this.preconditions = TaskPreconditions.Endpoint;
[ [
'finishOnKey', 'hints', 'input', 'numDigits', 'finishOnKey', 'hints', 'input', 'numDigits', 'minDigits', 'maxDigits',
'partialResultHook', 'bargein', 'interDigitTimeout', 'submitDigit', 'partialResultHook', 'bargein', 'dtmfBargein',
'retries', 'retryPromptTts', 'retryPromptUrl',
'speechTimeout', 'timeout', 'say', 'play' 'speechTimeout', 'timeout', 'say', 'play'
].forEach((k) => this[k] = this.data[k]); ].forEach((k) => this[k] = this.data[k]);
this.listenDuringPrompt = this.data.listenDuringPrompt === false ? false : true; this.listenDuringPrompt = this.data.listenDuringPrompt === false ? false : true;
this.minBargeinWordCount = this.data.minBargeinWordCount || 1; this.minBargeinWordCount = this.data.minBargeinWordCount || 1;
this.timeout = (this.timeout || 15) * 1000; this.timeout = (this.timeout || 15) * 1000;
this.interim = this.partialResultCallback; this.interim = this.partialResultCallback || this.bargein;
if (this.data.recognizer) { if (this.data.recognizer) {
const recognizer = this.data.recognizer; const recognizer = this.data.recognizer;
this.vendor = recognizer.vendor; this.vendor = recognizer.vendor;
@@ -119,7 +120,7 @@ class TaskGather extends Task {
.catch(() => {/*already logged error */}); .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)); ep.on('dtmf', this._onDtmf.bind(this, cs, ep));
} }
@@ -145,12 +146,28 @@ class TaskGather extends Task {
_onDtmf(cs, ep, evt) { _onDtmf(cs, ep, evt) {
this.logger.debug(evt, 'TaskGather:_onDtmf'); 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 { else {
this.digitBuffer += evt.dtmf; 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) { async _initSpeech(cs, ep) {
@@ -216,7 +233,7 @@ class TaskGather extends Task {
ep.startTranscription({ ep.startTranscription({
vendor: this.vendor, vendor: this.vendor,
locale: this.language, locale: this.language,
interim: this.partialResultCallback || this.bargein, interim: this.interim,
}).catch((err) => { }).catch((err) => {
const {writeAlerts, AlertType} = this.cs.srf.locals; const {writeAlerts, AlertType} = this.cs.srf.locals;
this.logger.error(err, 'TaskGather:_startTranscribing error'); this.logger.error(err, 'TaskGather:_startTranscribing error');
@@ -280,7 +297,7 @@ class TaskGather extends Task {
transcript: evt.Text transcript: evt.Text
} }
] ]
}; }
} }
} }
if (evt.is_final) this._resolve('speech', evt); if (evt.is_final) this._resolve('speech', evt);
@@ -318,7 +335,8 @@ class TaskGather extends Task {
this._clearTimer(); this._clearTimer();
if (reason.startsWith('dtmf')) { 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')) { else if (reason.startsWith('speech')) {
if (this.parentTask) this.parentTask.emit('transcription', evt); if (this.parentTask) this.parentTask.emit('transcription', evt);

View File

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