mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2025-12-20 16:50:39 +00:00
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:
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
Reference in New Issue
Block a user