add support for ws verb:status event notifications (#196)

This commit is contained in:
Dave Horton
2022-12-09 21:11:47 -05:00
committed by GitHub
parent 5b875c3ad4
commit a60c6a4740
5 changed files with 96 additions and 10 deletions

View File

@@ -63,6 +63,7 @@ class CallSession extends Emitter {
assert(rootSpan); assert(rootSpan);
this._recordState = RecordState.RecordingOff; this._recordState = RecordState.RecordingOff;
this._notifyEvents = false;
this.tmpFiles = new Set(); this.tmpFiles = new Set();
@@ -265,6 +266,9 @@ class CallSession extends Emitter {
get recordState() { return this._recordState; } get recordState() { return this._recordState; }
get notifyEvents() { return this._notifyEvents; }
set notifyEvents(notify) { this._notifyEvents = !!notify; }
set globalSttHints({hints, hintsBoost}) { set globalSttHints({hints, hintsBoost}) {
this._globalSttHints = {hints, hintsBoost}; this._globalSttHints = {hints, hintsBoost};
} }
@@ -612,6 +616,7 @@ class CallSession extends Emitter {
const stackNum = this.stackIdx; const stackNum = this.stackIdx;
const task = this.tasks.shift(); const task = this.tasks.shift();
this.logger.info(`CallSession:exec starting task #${stackNum}:${taskNum}: ${task.name}`); this.logger.info(`CallSession:exec starting task #${stackNum}:${taskNum}: ${task.name}`);
this._notifyTaskStatus(task, {event: 'starting'});
try { try {
const resources = await this._evaluatePreconditions(task); const resources = await this._evaluatePreconditions(task);
let skip = false; let skip = false;
@@ -635,6 +640,7 @@ class CallSession extends Emitter {
} }
this.currentTask = null; this.currentTask = null;
this.logger.info(`CallSession:exec completed task #${stackNum}:${taskNum}: ${task.name}`); this.logger.info(`CallSession:exec completed task #${stackNum}:${taskNum}: ${task.name}`);
this._notifyTaskStatus(task, {event: 'finished'});
} catch (err) { } catch (err) {
task.span?.end(); task.span?.end();
this.currentTask = null; this.currentTask = null;
@@ -1623,6 +1629,25 @@ class CallSession extends Emitter {
.catch((err) => this.logger.error(err, 'redis error')); .catch((err) => this.logger.error(err, 'redis error'));
} }
/**
* notifyTaskError - only used when websocket connection is used instead of webhooks
*/
_notifyTaskError(obj) {
if (this.requestor instanceof WsRequestor) {
this.requestor.request('jambonz:error', '/error', obj)
.catch((err) => this.logger.debug({err}, 'CallSession:_notifyTaskError - Error sending'));
}
}
_notifyTaskStatus(task, evt) {
if (this.notifyEvents && this.requestor instanceof WsRequestor) {
const obj = {...evt, id: task.id, name: task.name};
this.requestor.request('verb:status', '/status', obj)
.catch((err) => this.logger.debug({err}, 'CallSession:_notifyTaskStatus - Error sending'));
}
}
_awaitCommandsOrHangup() { _awaitCommandsOrHangup() {
assert(!this.wakeupResolver); assert(!this.wakeupResolver);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View File

@@ -11,6 +11,10 @@ class TaskConfig extends Task {
'record' 'record'
].forEach((k) => this[k] = this.data[k] || {}); ].forEach((k) => this[k] = this.data[k] || {});
if ('notifyEvents' in this.data) {
this.notifyEvents = !!this.data.notifyEvents;
}
if (this.bargeIn.enable) { if (this.bargeIn.enable) {
this.gatherOpts = { this.gatherOpts = {
verb: 'gather', verb: 'gather',
@@ -51,12 +55,19 @@ class TaskConfig extends Task {
phrase.push(`set recognizer${s}`); phrase.push(`set recognizer${s}`);
} }
if (this.data.amd) phrase.push('enable amd'); if (this.data.amd) phrase.push('enable amd');
if (this.notifyEvents) phrase.push(`event notification ${this.notifyEvents ? 'on' : 'off'}`);
return `${this.name}{${phrase.join(',')}`; return `${this.name}{${phrase.join(',')}`;
} }
async exec(cs, {ep} = {}) { async exec(cs, {ep} = {}) {
await super.exec(cs); await super.exec(cs);
if (this.notifyEvents) {
this.logger.debug(`turning event notification ${this.notifyEvents ? 'on' : 'off'}`);
cs.notifyEvents = !!this.data.notifEvents;
}
if (this.data.amd) { if (this.data.amd) {
this.startAmd = cs.startAmd; this.startAmd = cs.startAmd;
this.stopAmd = cs.stopAmd; this.stopAmd = cs.stopAmd;

View File

@@ -156,7 +156,10 @@ class TaskSay extends Task {
alert_type: AlertType.TTS_NOT_PROVISIONED, alert_type: AlertType.TTS_NOT_PROVISIONED,
vendor vendor
}).catch((err) => this.logger.info({err}, 'Error generating alert for no tts')); }).catch((err) => this.logger.info({err}, 'Error generating alert for no tts'));
this.notifyError(`No speech credentials have been provisioned for ${vendor}`); this.notifyError({
msg: 'TTS error',
details:`No speech credentials provisioned for selected vendor ${vendor}`
});
throw new Error('no provisioned speech credentials for TTS'); throw new Error('no provisioned speech credentials for TTS');
} }
// synthesize all of the text elements // synthesize all of the text elements
@@ -174,7 +177,7 @@ class TaskSay extends Task {
'tts.voice': voice 'tts.voice': voice
}); });
try { try {
const {filePath, servedFromCache} = await synthAudio(stats, { const {filePath, servedFromCache, rtt} = await synthAudio(stats, {
text, text,
vendor, vendor,
language, language,
@@ -193,6 +196,15 @@ class TaskSay extends Task {
} }
span.setAttributes({'tts.cached': servedFromCache}); span.setAttributes({'tts.cached': servedFromCache});
span.end(); span.end();
if (!servedFromCache && rtt) {
this.notifyStatus({
event: 'synthesized-audio',
vendor,
language,
characters: text.length,
elapsedTime: rtt
});
}
return filePath; return filePath;
} catch (err) { } catch (err) {
this.logger.info({err}, 'Error synthesizing tts'); this.logger.info({err}, 'Error synthesizing tts');
@@ -203,7 +215,7 @@ class TaskSay extends Task {
vendor, vendor,
detail: err.message detail: err.message
}).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure')); }).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure'));
this.notifyError(err.message || err); this.notifyError({msg: 'TTS error', details: err.message || err});
return; return;
} }
}; };
@@ -211,6 +223,7 @@ class TaskSay extends Task {
const arr = this.text.map((t) => generateAudio(t)); const arr = this.text.map((t) => generateAudio(t));
const filepath = (await Promise.all(arr)).filter((fp) => fp && fp.length); const filepath = (await Promise.all(arr)).filter((fp) => fp && fp.length);
this.logger.debug({filepath}, 'synthesized files for tts'); this.logger.debug({filepath}, 'synthesized files for tts');
this.notifyStatus({event: 'start-playback'});
while (!this.killed && (this.loop === 'forever' || this.loop--) && this.ep?.connected) { while (!this.killed && (this.loop === 'forever' || this.loop--) && this.ep?.connected) {
let segment = 0; let segment = 0;
@@ -242,6 +255,7 @@ class TaskSay extends Task {
this.killPlayToConfMember(this.ep, memberId, confName); this.killPlayToConfMember(this.ep, memberId, confName);
} }
else { else {
this.notifyStatus({event: 'kill-playback'});
this.ep.api('uuid_break', this.ep.uuid); this.ep.api('uuid_break', this.ep.uuid);
} }
} }

View File

@@ -1,6 +1,7 @@
{ {
"sip:decline": { "sip:decline": {
"properties": { "properties": {
"id": "string",
"status": "number", "status": "number",
"reason": "string", "reason": "string",
"headers": "object" "headers": "object"
@@ -11,6 +12,7 @@
}, },
"sip:request": { "sip:request": {
"properties": { "properties": {
"id": "string",
"method": "string", "method": "string",
"body": "string", "body": "string",
"headers": "object", "headers": "object",
@@ -22,6 +24,7 @@
}, },
"sip:refer": { "sip:refer": {
"properties": { "properties": {
"id": "string",
"referTo": "string", "referTo": "string",
"referredBy": "string", "referredBy": "string",
"headers": "object", "headers": "object",
@@ -34,11 +37,13 @@
}, },
"config": { "config": {
"properties": { "properties": {
"id": "string",
"synthesizer": "#synthesizer", "synthesizer": "#synthesizer",
"recognizer": "#recognizer", "recognizer": "#recognizer",
"bargeIn": "#bargeIn", "bargeIn": "#bargeIn",
"record": "#recordOptions", "record": "#recordOptions",
"amd": "#amd" "amd": "#amd",
"notifyEvents": "boolean"
}, },
"required": [] "required": []
}, },
@@ -62,6 +67,7 @@
}, },
"dequeue": { "dequeue": {
"properties": { "properties": {
"id": "string",
"name": "string", "name": "string",
"actionHook": "object|string", "actionHook": "object|string",
"timeout": "number", "timeout": "number",
@@ -73,6 +79,7 @@
}, },
"enqueue": { "enqueue": {
"properties": { "properties": {
"id": "string",
"name": "string", "name": "string",
"actionHook": "object|string", "actionHook": "object|string",
"waitHook": "object|string", "waitHook": "object|string",
@@ -84,11 +91,12 @@
}, },
"leave": { "leave": {
"properties": { "properties": {
"id": "string"
} }
}, },
"hangup": { "hangup": {
"properties": { "properties": {
"id": "string",
"headers": "object" "headers": "object"
}, },
"required": [ "required": [
@@ -96,6 +104,7 @@
}, },
"play": { "play": {
"properties": { "properties": {
"id": "string",
"url": "string|array", "url": "string|array",
"loop": "number|string", "loop": "number|string",
"earlyMedia": "boolean", "earlyMedia": "boolean",
@@ -109,6 +118,7 @@
}, },
"say": { "say": {
"properties": { "properties": {
"id": "string",
"text": "string|array", "text": "string|array",
"loop": "number|string", "loop": "number|string",
"synthesizer": "#synthesizer", "synthesizer": "#synthesizer",
@@ -120,6 +130,7 @@
}, },
"gather": { "gather": {
"properties": { "properties": {
"id": "string",
"actionHook": "object|string", "actionHook": "object|string",
"finishOnKey": "string", "finishOnKey": "string",
"input": "array", "input": "array",
@@ -143,6 +154,7 @@
}, },
"conference": { "conference": {
"properties": { "properties": {
"id": "string",
"name": "string", "name": "string",
"beep": "boolean", "beep": "boolean",
"startConferenceOnEnter": "boolean", "startConferenceOnEnter": "boolean",
@@ -162,6 +174,7 @@
}, },
"dial": { "dial": {
"properties": { "properties": {
"id": "string",
"actionHook": "object|string", "actionHook": "object|string",
"answerOnBridge": "boolean", "answerOnBridge": "boolean",
"callerId": "string", "callerId": "string",
@@ -185,6 +198,7 @@
}, },
"dialogflow": { "dialogflow": {
"properties": { "properties": {
"id": "string",
"credentials": "object|string", "credentials": "object|string",
"project": "string", "project": "string",
"environment": "string", "environment": "string",
@@ -213,6 +227,7 @@
}, },
"dtmf": { "dtmf": {
"properties": { "properties": {
"id": "string",
"dtmf": "string", "dtmf": "string",
"duration": "number" "duration": "number"
}, },
@@ -222,6 +237,7 @@
}, },
"lex": { "lex": {
"properties": { "properties": {
"id": "string",
"botId": "string", "botId": "string",
"botAlias": "string", "botAlias": "string",
"credentials": "object", "credentials": "object",
@@ -246,6 +262,7 @@
}, },
"listen": { "listen": {
"properties": { "properties": {
"id": "string",
"actionHook": "object|string", "actionHook": "object|string",
"auth": "#auth", "auth": "#auth",
"finishOnKey": "string", "finishOnKey": "string",
@@ -270,6 +287,7 @@
}, },
"message": { "message": {
"properties": { "properties": {
"id": "string",
"carrier": "string", "carrier": "string",
"account_sid": "string", "account_sid": "string",
"message_sid": "string", "message_sid": "string",
@@ -286,6 +304,7 @@
}, },
"pause": { "pause": {
"properties": { "properties": {
"id": "string",
"length": "number" "length": "number"
}, },
"required": [ "required": [
@@ -294,6 +313,7 @@
}, },
"rasa": { "rasa": {
"properties": { "properties": {
"id": "string",
"url": "string", "url": "string",
"recognizer": "#recognizer", "recognizer": "#recognizer",
"tts": "#synthesizer", "tts": "#synthesizer",
@@ -328,6 +348,7 @@
}, },
"redirect": { "redirect": {
"properties": { "properties": {
"id": "string",
"actionHook": "object|string" "actionHook": "object|string"
}, },
"required": [ "required": [
@@ -336,6 +357,7 @@
}, },
"rest:dial": { "rest:dial": {
"properties": { "properties": {
"id": "string",
"account_sid": "string", "account_sid": "string",
"application_sid": "string", "application_sid": "string",
"call_hook": "object|string", "call_hook": "object|string",
@@ -360,6 +382,7 @@
}, },
"tag": { "tag": {
"properties": { "properties": {
"id": "string",
"data": "object" "data": "object"
}, },
"required": [ "required": [
@@ -368,6 +391,7 @@
}, },
"transcribe": { "transcribe": {
"properties": { "properties": {
"id": "string",
"transcriptionHook": "string", "transcriptionHook": "string",
"recognizer": "#recognizer", "recognizer": "#recognizer",
"earlyMedia": "boolean" "earlyMedia": "boolean"

View File

@@ -4,6 +4,7 @@ const debug = require('debug')('jambonz:feature-server');
const assert = require('assert'); const assert = require('assert');
const {TaskPreconditions} = require('../utils/constants'); const {TaskPreconditions} = require('../utils/constants');
const normalizeJambones = require('../utils/normalize-jambones'); const normalizeJambones = require('../utils/normalize-jambones');
const WsRequestor = require('../utils/ws-requestor');
const {trace} = require('@opentelemetry/api'); const {trace} = require('@opentelemetry/api');
const specs = new Map(); const specs = new Map();
const _specData = require('./specs'); const _specData = require('./specs');
@@ -21,6 +22,7 @@ class Task extends Emitter {
this.logger = logger; this.logger = logger;
this.data = data; this.data = data;
this.actionHook = this.data.actionHook; this.actionHook = this.data.actionHook;
this.id = data.id;
this._killInProgress = false; this._killInProgress = false;
this._completionPromise = new Promise((resolve) => this._completionResolver = resolve); this._completionPromise = new Promise((resolve) => this._completionResolver = resolve);
@@ -137,11 +139,21 @@ class Task extends Emitter {
return this.callSession.normalizeUrl(url, method, auth); return this.callSession.normalizeUrl(url, method, auth);
} }
notifyError(errMsg) { notifyError(obj) {
const params = {error: errMsg, verb: this.name}; if (this.cs.requestor instanceof WsRequestor) {
const params = {...obj, verb: this.name, id: this.id};
this.cs.requestor.request('jambonz:error', '/error', params) this.cs.requestor.request('jambonz:error', '/error', params)
.catch((err) => this.logger.info({err}, 'Task:notifyError error sending error')); .catch((err) => this.logger.info({err}, 'Task:notifyError error sending error'));
} }
}
notifyStatus(obj) {
if (this.cs.notifyEvents && this.cs.requestor instanceof WsRequestor) {
const params = {...obj, verb: this.name, id: this.id};
this.cs.requestor.request('verb:status', '/status', params)
.catch((err) => this.logger.info({err}, 'Task:notifyStatus error sending error'));
}
}
async performAction(results, expectResponse = true) { async performAction(results, expectResponse = true) {
if (this.actionHook) { if (this.actionHook) {