diff --git a/lib/tasks/cognigy/index.js b/lib/tasks/cognigy/index.js index d0289a6d..4d2cb6ae 100644 --- a/lib/tasks/cognigy/index.js +++ b/lib/tasks/cognigy/index.js @@ -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(); + } } } diff --git a/lib/tasks/cognigy/speech-config.js b/lib/tasks/cognigy/speech-config.js index b50da9f7..a802beb9 100644 --- a/lib/tasks/cognigy/speech-config.js +++ b/lib/tasks/cognigy/speech-config.js @@ -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}; } } diff --git a/lib/tasks/gather.js b/lib/tasks/gather.js index 1d08c6e8..a38ca293 100644 --- a/lib/tasks/gather.js +++ b/lib/tasks/gather.js @@ -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); diff --git a/lib/tasks/specs.json b/lib/tasks/specs.json index d454a6e5..81a55196 100644 --- a/lib/tasks/specs.json +++ b/lib/tasks/specs.json @@ -103,6 +103,10 @@ "listenDuringPrompt": "boolean", "bargein": "boolean", "minBargeinWordCount": "number", + "dtmfBargein": "boolean", + "minDigits": "number", + "maxDigits": "number", + "interDigitTimeout": "number", "timeout": "number", "recognizer": "#recognizer", "play": "#play",