mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2025-12-22 01:27:55 +00:00
add support for retry logic and dtmf
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user