From c18768505427033a5f66fbc153e514740aa8c479 Mon Sep 17 00:00:00 2001 From: Hoan Luu Huu <110280845+xquanluu@users.noreply.github.com> Date: Wed, 21 Feb 2024 09:09:19 +0700 Subject: [PATCH] feat actionHook delay action (#470) * feat actionHook delay action * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip --- lib/session/call-session.js | 121 +++++++++++++++++++++++++++++++ lib/tasks/config.js | 11 ++- lib/tasks/gather.js | 139 +++++++++++++++++++++++++++++++++++- lib/tasks/task.js | 7 ++ 4 files changed, 276 insertions(+), 2 deletions(-) diff --git a/lib/session/call-session.js b/lib/session/call-session.js index 74673d89..17e66b3f 100644 --- a/lib/session/call-session.js +++ b/lib/session/call-session.js @@ -449,6 +449,47 @@ class CallSession extends Emitter { this._sipRequestWithinDialogHook = url; } + // Bot Delay (actionHook delayed) + get actionHookDelayEnabled() { + return this._actionHookDelayEnabled; + } + + set actionHookDelayEnabled(e) { + this._actionHookDelayEnabled = e; + } + + get actionHookNoResponseTimeout() { + return this._actionHookNoResponseTimeout; + } + + set actionHookNoResponseTimeout(e) { + this._actionHookNoResponseTimeout = e; + } + + get actionHookNoResponseGiveUpTimeout() { + return this._actionHookNoResponseGiveUpTimeout; + } + + set actionHookNoResponseGiveUpTimeout(e) { + this._actionHookNoResponseGiveUpTimeout = e; + } + + get actionHookDelayRetries() { + return this._actionHookDelayRetries; + } + + set actionHookDelayRetries(e) { + this._actionHookDelayRetries = e; + } + + get actionHookDelayActions() { + return this._actionHookDelayActions; + } + + set actionHookDelayActions(e) { + this._actionHookDelayActions = e; + } + hasGlobalSttPunctuation() { return this._globalSttPunctuation !== undefined; } @@ -836,6 +877,7 @@ class CallSession extends Emitter { task.on('VerbHookSpanWaitForEnd', ({span}) => { this.verbHookSpan = span; }); + task.on('ActionHookDelayActionOptions', this._onActionHookDelayActions.bind(this)); try { const resources = await this._evaluatePreconditions(task); let skip = false; @@ -1107,6 +1149,9 @@ class CallSession extends Emitter { this.currentTask.kill(this); } this._endVerbHookSpan(); + // clear all delay action hook timeout if there is + this._clearActionHookNoResponseGiveUpTimer(); + this._clearActionHookNoResponseTimer(); } /** @@ -1294,6 +1339,14 @@ Duration=${duration} ` task.whisper(tasks, callSid).catch((err) => this.logger.error(err, 'CallSession:_lccWhisper')); } + /** + * perform call hangup by jambonz + */ + + async hangup() { + return this._callerHungup(); + } + /** * perform live call control @@ -1505,6 +1558,9 @@ Duration=${duration} ` } resolution = {reason: 'received command, new tasks', queue: queueCommand, command}; resolution.command = listTaskNames(t); + // clear all delay action hook timeout if there is + this._clearActionHookNoResponseGiveUpTimer(); + this._clearActionHookNoResponseTimer(); } else this._lccCallHook(data); break; @@ -1745,6 +1801,8 @@ Duration=${duration} ` this.rootSpan && this.rootSpan.end(); // close all background tasks this.backgroundTaskManager.stopAll(); + this._clearActionHookNoResponseGiveUpTimer(); + this._clearActionHookNoResponseTimer(); } /** @@ -2170,6 +2228,69 @@ Duration=${duration} ` this.verbHookSpan = null; } } + // actionHook delay actions + _onActionHookDelayActions(options) { + this._actionHookDelayRetryCount = 0; + this._startActionHookNoResponseTimer(options); + this._startActionHookNoResponseGiveUpTimer(options); + } + + _startActionHookNoResponseTimer(options) { + this._clearActionHookNoResponseTimer(); + if (options.noResponseTimeoutMs) { + this.logger.debug(`CallSession:_startActionHookNoResponseTimer ${options.noResponseTimeoutMs}`); + this._actionHookNoResponseTimer = setTimeout(() => { + if (this._actionHookDelayRetryCount >= options.retries) { + this._callerHungup(); + } + const verb = options.actions[this._actionHookDelayRetryCount % options.actions.length]; + // Inject verb to main stack + const t = normalizeJambones(this.logger, [verb]) + .map((tdata) => makeTask(this.logger, tdata)); + if (t.length) { + t[0].on('playDone', (err) => { + if (err) this.logger.error({err}, `Call-Session:exec Error delay action, play ${verb}`); + this._startActionHookNoResponseTimer(options); + }); + } + this.tasks.push(...t); + if (this.wakeupResolver) { + this.wakeupResolver({reason: 'actionHook no response, applied delay actions', verb}); + this.wakeupResolver = null; + } + + this.logger.debug(`CallSession:_startActionHookNoResponseTimer, executing verb ${JSON.stringify(verb)}`); + + this._actionHookDelayRetryCount++; + }, options.noResponseTimeoutMs); + } + } + + _clearActionHookNoResponseTimer() { + if (this._actionHookNoResponseTimer) { + clearTimeout(this._actionHookNoResponseTimer); + } + this._actionHookNoResponseTimer = null; + } + + _startActionHookNoResponseGiveUpTimer(options) { + this._clearActionHookNoResponseGiveUpTimer(); + if (options.noResponseGiveUpTimeoutMs) { + this.logger.debug(`CallSession:_startActionHookNoResponseGiveUpTimer ${options.noResponseGiveUpTimeoutMs}`); + this._actionHookNoResponseGiveUpTimer = setTimeout(() => { + this.logger.debug('CallSession:_startActionHookNoResponseGiveUpTimer Timeout'); + this._callerHungup(); + this._actionHookNoResponseGiveUpTimer = null; + }, options.noResponseGiveUpTimeoutMs); + } + } + + _clearActionHookNoResponseGiveUpTimer() { + if (this._actionHookNoResponseGiveUpTimer) { + clearTimeout(this._actionHookNoResponseGiveUpTimer); + } + this._actionHookNoResponseGiveUpTimer = null; + } } module.exports = CallSession; diff --git a/lib/tasks/config.js b/lib/tasks/config.js index bddb8f0e..a084bf7c 100644 --- a/lib/tasks/config.js +++ b/lib/tasks/config.js @@ -10,7 +10,8 @@ class TaskConfig extends Task { 'bargeIn', 'record', 'listen', - 'transcribe' + 'transcribe', + 'actionHookDelayAction' ].forEach((k) => this[k] = this.data[k] || {}); if ('notifyEvents' in this.data) { @@ -249,6 +250,14 @@ class TaskConfig extends Task { cs.stopBackgroundTask('transcribe'); } } + + if (this.actionHookDelayAction) { + cs.actionHookDelayEnabled = this.actionHookDelayAction.enabled || false; + cs.actionHookNoResponseTimeout = this.actionHookDelayAction.noResponseTimeout || 0; + cs.actionHookNoResponseGiveUpTimeout = this.actionHookDelayAction.noResponseGiveUpTimeout || 0; + cs.actionHookDelayRetries = this.actionHookDelayAction.retries || 1; + cs.actionHookDelayActions = this.actionHookDelayAction.actions || []; + } if (this.data.sipRequestWithinDialogHook) { cs.sipRequestWithinDialogHook = this.data.sipRequestWithinDialogHook; } diff --git a/lib/tasks/gather.js b/lib/tasks/gather.js index ffed44d9..c2915dbe 100644 --- a/lib/tasks/gather.js +++ b/lib/tasks/gather.js @@ -27,7 +27,7 @@ class TaskGather extends SttTask { [ 'finishOnKey', 'input', 'numDigits', 'minDigits', 'maxDigits', 'interDigitTimeout', 'partialResultHook', 'bargein', 'dtmfBargein', - 'speechTimeout', 'timeout', 'say', 'play' + 'speechTimeout', 'timeout', 'say', 'play', 'actionHookDelayAction' ].forEach((k) => this[k] = this.data[k]); // gather default input is digits @@ -138,6 +138,30 @@ class TaskGather extends SttTask { this.interim = true; this.logger.debug('Gather:exec - early hints match enabled'); } + // actionHook delay + this._actionHookDelayEnabled = cs.actionHookDelayEnabled || !!this.actionHookDelayAction; + this._actionHookDelayActions = this.actionHookDelayAction && this.actionHookDelayAction.actions ? + this.actionHookDelayAction.actions : cs.actionHookDelayActions || []; + if (this._actionHookDelayEnabled && this._actionHookDelayActions.length > 0) { + this._actionHookNoResponseTimeout = (this.actionHookDelayAction && this.actionHookDelayAction.noResponseTimeout ? + this.actionHookDelayAction.noResponseTimeout : cs.actionHookNoResponseTimeout || 0) * 1000; + + this._actionHookNoResponseGiveUpTimeout = (this.actionHookDelayAction && + this.actionHookDelayAction.noResponseGiveUpTimeout ? + this.actionHookDelayAction.noResponseGiveUpTimeout : cs.actionHookNoResponseGiveUpTimeout || 0) * 1000; + + this._actionHookDelayRetries = this.actionHookDelayAction && this.actionHookDelayAction.retries ? + this.actionHookDelayAction.retries : cs.actionHookDelayRetries || 1; + this._actionHookDelayTryCount = 0; + this.actionHookDelayActionOptions = { + enabled: this._actionHookDelayEnabled, + actions: this._actionHookDelayActions, + noResponseTimeoutMs: this._actionHookNoResponseTimeout, + noResponseGiveUpTimeoutMs: this._actionHookNoResponseGiveUpTimeout, + retries: this._actionHookDelayRetries + }; + } + const startListening = async(cs, ep) => { this._startTimer(); if (this.isContinuousAsr && 0 === this.timeout) this._startAsrTimer(); @@ -231,6 +255,7 @@ class TaskGather extends SttTask { kill(cs) { super.kill(cs); this._killAudio(cs); + this._killActionHookDelayAction(); this.ep.removeAllListeners('dtmf'); clearTimeout(this.interDigitTimer); this._clearAsrTimer(); @@ -521,6 +546,104 @@ class TaskGather extends SttTask { this._asrTimer = null; } + _hangupCall() { + this.logger.debug('_hangupCall'); + this.cs.hangup(); + } + + _actionHookDelaySayAction(verb) { + delete verb.verb; + this.logger.debug(`_actionHookDelaySayAction ${verb}`); + this._actionHookDelaySayTask = makeTask(this.logger, {say: verb}, this); + const {span, ctx} = this.startChildSpan(`actionHookDelayAction:${this._actionHookDelaySayTask.summary}`); + this._actionHookDelaySayTask.span = span; + this._actionHookDelaySayTask.ctx = ctx; + this._actionHookDelaySayTask.exec(this.cs, {ep: this.ep}); + this._actionHookDelaySayTask.on('playDone', (err) => { + this._actionHookDelaySayTask = null; + span.end(); + if (err) this.logger.error({err}, 'Gather:actionHookDelay Error playing tts'); + }); + } + + _killActionHookDelayAction() { + this.logger.debug('_killActionHookDelayAction'); + if (this._actionHookDelaySayTask && !this._actionHookDelaySayTask.killed) { + this._actionHookDelaySayTask.removeAllListeners('playDone'); + this._actionHookDelaySayTask.kill(this.cs); + this._actionHookDelaySayTask.span.end(); + this._actionHookDelaySayTask = null; + } + + if (this._actionHookDelayPlayTask && !this._actionHookDelayPlayTask.killed) { + this._actionHookDelayPlayTask.removeAllListeners('playDone'); + this._actionHookDelayPlayTask.kill(this.cs); + this._actionHookDelayPlayTask.span.end(); + this._actionHookDelayPlayTask = null; + } + } + + _actionHookDelayPlayAction(verb) { + delete verb.verb; + this.logger.debug(`_actionHookDelayPlayAction ${verb}`); + this._actionHookDelayPlayTask = makeTask(this.logger, {play: verb}, this); + const {span, ctx} = this.startChildSpan(`actionHookDelayAction:${this._actionHookDelayPlayTask.summary}`); + this._actionHookDelayPlayTask.span = span; + this._actionHookDelayPlayTask.ctx = ctx; + this._actionHookDelayPlayTask.exec(this.cs, {ep: this.ep}); + this._actionHookDelayPlayTask.on('playDone', (err) => { + this._actionHookDelayPlayTask = null; + span.end(); + if (err) this.logger.error({err}, 'Gather:actionHookDelay Error playing tts'); + }); + + } + + _startActionHookNoResponseTimer() { + assert(this._actionHookNoResponseTimeout > 0); + this._clearActionHookNoResponseTimer(); + this.logger.debug('startActionHookNoResponseTimer'); + this._actionHookNoResponseTimer = setTimeout(() => { + if (this._actionHookDelayTryCount >= this._actionHookDelayRetries) { + this._hangupCall(); + return; + } + const verb = this._actionHookDelayActions[this._actionHookDelayTryCount % this._actionHookDelayActions.length]; + if (verb.verb === 'say') { + this._actionHookDelaySayAction(verb); + } else if (verb.verb === 'play') { + this._actionHookDelayPlayAction(verb); + } + this._actionHookDelayTryCount++; + this._startActionHookNoResponseTimer(); + + }, this._actionHookNoResponseTimeout); + + } + + _clearActionHookNoResponseTimer() { + if (this._actionHookNoResponseTimer) { + clearTimeout(this._actionHookNoResponseTimer); + } + this._actionHookNoResponseTimer = null; + } + + _startActionHookNoResponseGiveUpTimer() { + assert(this._actionHookNoResponseGiveUpTimeout > 0); + this._clearActionHookNoResponseGiveUpTimer(); + this.logger.debug('startActionHookNoResponseGiveUpTimer'); + this._actionHookNoResponseGiveUpTimer = setTimeout(() => { + this._hangupCall(); + }, this._actionHookNoResponseGiveUpTimeout); + } + + _clearActionHookNoResponseGiveUpTimer() { + if (this._actionHookNoResponseGiveUpTimer) { + clearTimeout(this._actionHookNoResponseGiveUpTimer); + } + this._actionHookNoResponseGiveUpTimer = null; + } + _startFastRecognitionTimer(evt) { assert(this.fastRecognitionTimeout > 0); this._clearFastRecognitionTimer(); @@ -865,6 +988,15 @@ class TaskGather extends SttTask { return; } + // Enabled action Hook delay timer to applied actions + if (this._actionHookNoResponseTimeout > 0) { + this._startActionHookNoResponseTimer(); + } + + if (this._actionHookNoResponseGiveUpTimeout > 0) { + this._startActionHookNoResponseGiveUpTimer(); + } + try { if (reason.startsWith('dtmf')) { if (this.parentTask) this.parentTask.emit('dtmf', evt); @@ -895,6 +1027,11 @@ class TaskGather extends SttTask { } } } catch (err) { /*already logged error*/ } + + // Gather got response from hook, cancel all delay timers if there is any + this._clearActionHookNoResponseTimer(); + this._clearActionHookNoResponseGiveUpTimer(); + this.notifyTaskDone(); } } diff --git a/lib/tasks/task.js b/lib/tasks/task.js index 15877d04..1cede8fa 100644 --- a/lib/tasks/task.js +++ b/lib/tasks/task.js @@ -173,6 +173,13 @@ class Task extends Emitter { * first new set of verbs arrive after sending a transcript * */ this.emit('VerbHookSpanWaitForEnd', {span}); + + // If actionHook delay action is configured, and ws application have not responded yet any verb for actionHook + // We have to transfer the task to call-session to await on next ws command verbs, and also run action Hook + // delay actions + if (this.actionHookDelayActionOptions) { + this.emit('ActionHookDelayActionOptions', this.actionHookDelayActionOptions); + } } if (expectResponse && json && Array.isArray(json)) { const makeTask = require('./make_task');