feat actionHook delay action (#470)

* feat actionHook delay action

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip
This commit is contained in:
Hoan Luu Huu
2024-02-21 09:09:19 +07:00
committed by GitHub
parent 81234a583c
commit c187685054
4 changed files with 276 additions and 2 deletions

View File

@@ -449,6 +449,47 @@ class CallSession extends Emitter {
this._sipRequestWithinDialogHook = url; 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() { hasGlobalSttPunctuation() {
return this._globalSttPunctuation !== undefined; return this._globalSttPunctuation !== undefined;
} }
@@ -836,6 +877,7 @@ class CallSession extends Emitter {
task.on('VerbHookSpanWaitForEnd', ({span}) => { task.on('VerbHookSpanWaitForEnd', ({span}) => {
this.verbHookSpan = span; this.verbHookSpan = span;
}); });
task.on('ActionHookDelayActionOptions', this._onActionHookDelayActions.bind(this));
try { try {
const resources = await this._evaluatePreconditions(task); const resources = await this._evaluatePreconditions(task);
let skip = false; let skip = false;
@@ -1107,6 +1149,9 @@ class CallSession extends Emitter {
this.currentTask.kill(this); this.currentTask.kill(this);
} }
this._endVerbHookSpan(); 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')); 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 * perform live call control
@@ -1505,6 +1558,9 @@ Duration=${duration} `
} }
resolution = {reason: 'received command, new tasks', queue: queueCommand, command}; resolution = {reason: 'received command, new tasks', queue: queueCommand, command};
resolution.command = listTaskNames(t); resolution.command = listTaskNames(t);
// clear all delay action hook timeout if there is
this._clearActionHookNoResponseGiveUpTimer();
this._clearActionHookNoResponseTimer();
} }
else this._lccCallHook(data); else this._lccCallHook(data);
break; break;
@@ -1745,6 +1801,8 @@ Duration=${duration} `
this.rootSpan && this.rootSpan.end(); this.rootSpan && this.rootSpan.end();
// close all background tasks // close all background tasks
this.backgroundTaskManager.stopAll(); this.backgroundTaskManager.stopAll();
this._clearActionHookNoResponseGiveUpTimer();
this._clearActionHookNoResponseTimer();
} }
/** /**
@@ -2170,6 +2228,69 @@ Duration=${duration} `
this.verbHookSpan = null; 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; module.exports = CallSession;

View File

@@ -10,7 +10,8 @@ class TaskConfig extends Task {
'bargeIn', 'bargeIn',
'record', 'record',
'listen', 'listen',
'transcribe' 'transcribe',
'actionHookDelayAction'
].forEach((k) => this[k] = this.data[k] || {}); ].forEach((k) => this[k] = this.data[k] || {});
if ('notifyEvents' in this.data) { if ('notifyEvents' in this.data) {
@@ -249,6 +250,14 @@ class TaskConfig extends Task {
cs.stopBackgroundTask('transcribe'); 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) { if (this.data.sipRequestWithinDialogHook) {
cs.sipRequestWithinDialogHook = this.data.sipRequestWithinDialogHook; cs.sipRequestWithinDialogHook = this.data.sipRequestWithinDialogHook;
} }

View File

@@ -27,7 +27,7 @@ class TaskGather extends SttTask {
[ [
'finishOnKey', 'input', 'numDigits', 'minDigits', 'maxDigits', 'finishOnKey', 'input', 'numDigits', 'minDigits', 'maxDigits',
'interDigitTimeout', 'partialResultHook', 'bargein', 'dtmfBargein', 'interDigitTimeout', 'partialResultHook', 'bargein', 'dtmfBargein',
'speechTimeout', 'timeout', 'say', 'play' 'speechTimeout', 'timeout', 'say', 'play', 'actionHookDelayAction'
].forEach((k) => this[k] = this.data[k]); ].forEach((k) => this[k] = this.data[k]);
// gather default input is digits // gather default input is digits
@@ -138,6 +138,30 @@ class TaskGather extends SttTask {
this.interim = true; this.interim = true;
this.logger.debug('Gather:exec - early hints match enabled'); 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) => { const startListening = async(cs, ep) => {
this._startTimer(); this._startTimer();
if (this.isContinuousAsr && 0 === this.timeout) this._startAsrTimer(); if (this.isContinuousAsr && 0 === this.timeout) this._startAsrTimer();
@@ -231,6 +255,7 @@ class TaskGather extends SttTask {
kill(cs) { kill(cs) {
super.kill(cs); super.kill(cs);
this._killAudio(cs); this._killAudio(cs);
this._killActionHookDelayAction();
this.ep.removeAllListeners('dtmf'); this.ep.removeAllListeners('dtmf');
clearTimeout(this.interDigitTimer); clearTimeout(this.interDigitTimer);
this._clearAsrTimer(); this._clearAsrTimer();
@@ -521,6 +546,104 @@ class TaskGather extends SttTask {
this._asrTimer = null; 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) { _startFastRecognitionTimer(evt) {
assert(this.fastRecognitionTimeout > 0); assert(this.fastRecognitionTimeout > 0);
this._clearFastRecognitionTimer(); this._clearFastRecognitionTimer();
@@ -865,6 +988,15 @@ class TaskGather extends SttTask {
return; return;
} }
// Enabled action Hook delay timer to applied actions
if (this._actionHookNoResponseTimeout > 0) {
this._startActionHookNoResponseTimer();
}
if (this._actionHookNoResponseGiveUpTimeout > 0) {
this._startActionHookNoResponseGiveUpTimer();
}
try { try {
if (reason.startsWith('dtmf')) { if (reason.startsWith('dtmf')) {
if (this.parentTask) this.parentTask.emit('dtmf', evt); if (this.parentTask) this.parentTask.emit('dtmf', evt);
@@ -895,6 +1027,11 @@ class TaskGather extends SttTask {
} }
} }
} catch (err) { /*already logged error*/ } } catch (err) { /*already logged error*/ }
// Gather got response from hook, cancel all delay timers if there is any
this._clearActionHookNoResponseTimer();
this._clearActionHookNoResponseGiveUpTimer();
this.notifyTaskDone(); this.notifyTaskDone();
} }
} }

View File

@@ -173,6 +173,13 @@ class Task extends Emitter {
* first new set of verbs arrive after sending a transcript * first new set of verbs arrive after sending a transcript
* */ * */
this.emit('VerbHookSpanWaitForEnd', {span}); 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)) { if (expectResponse && json && Array.isArray(json)) {
const makeTask = require('./make_task'); const makeTask = require('./make_task');