mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2026-07-04 19:32:01 +00:00
Merge branch 'main' of https://github.com/jambonz/jambonz-feature-server into feat/sh_asr
This commit is contained in:
@@ -54,22 +54,24 @@ class AdultingCallSession extends CallSession {
|
||||
return this.callInfo.callSid;
|
||||
}
|
||||
|
||||
_callerHungup() {
|
||||
this._hangup('caller');
|
||||
_callerHungup(req) {
|
||||
this._hangup('caller', req);
|
||||
}
|
||||
|
||||
_jambonzHangup() {
|
||||
this._hangup();
|
||||
}
|
||||
|
||||
_hangup(terminatedBy = 'jambonz') {
|
||||
_hangup(terminatedBy = 'jambonz', req) {
|
||||
if (this.dlg.connectTime) {
|
||||
const duration = moment().diff(this.dlg.connectTime, 'seconds');
|
||||
const headers = this._extractCustomHeaders(req);
|
||||
this.rootSpan.setAttributes({'call.termination': `hangup by ${terminatedBy}`});
|
||||
this.callInfo.callTerminationBy = terminatedBy;
|
||||
this.emit('callStatusChange', {
|
||||
callStatus: CallStatus.Completed,
|
||||
duration
|
||||
duration,
|
||||
...(headers && {headers})
|
||||
});
|
||||
}
|
||||
this.logger.info(`InboundCallSession: ${terminatedBy} hung up`);
|
||||
|
||||
@@ -124,6 +124,14 @@ class CallInfo {
|
||||
return this._customerData;
|
||||
}
|
||||
|
||||
set sipHeaders(obj) {
|
||||
this._sipHeaders = obj;
|
||||
}
|
||||
|
||||
get sipHeaders() {
|
||||
return this._sipHeaders;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const obj = {
|
||||
callSid: this.callSid,
|
||||
@@ -150,6 +158,10 @@ class CallInfo {
|
||||
Object.assign(obj, {customerData: this._customerData});
|
||||
}
|
||||
|
||||
if (this._sipHeaders) {
|
||||
Object.assign(obj, {headers: this._sipHeaders});
|
||||
}
|
||||
|
||||
if (JAMBONES_API_BASE_URL) {
|
||||
Object.assign(obj, {apiBaseUrl: JAMBONES_API_BASE_URL});
|
||||
}
|
||||
|
||||
@@ -2664,6 +2664,18 @@ Duration=${duration} `
|
||||
assert(false, 'subclass responsibility to override this method');
|
||||
}
|
||||
|
||||
_extractCustomHeaders(req) {
|
||||
const dominated = ['via', 'from', 'to', 'call-id', 'cseq', 'max-forwards',
|
||||
'content-length', 'contact', 'route', 'record-route', 'content-type',
|
||||
'authorization', 'proxy-authorization', 'www-authenticate', 'proxy-authenticate'];
|
||||
if (!req || !req.headers) return null;
|
||||
const headers = {};
|
||||
Object.keys(req.headers).forEach((h) => {
|
||||
if (!dominated.includes(h)) headers[h] = req.headers[h];
|
||||
});
|
||||
return Object.keys(headers).length ? headers : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* called when the jambonz has hung up. Provided for subclasses to override
|
||||
* in order to apply logic at this point if needed.
|
||||
@@ -3053,7 +3065,7 @@ Duration=${duration} `
|
||||
* @param {number} sipStatus - current sip status
|
||||
* @param {number} [duration] - duration of a completed call, in seconds
|
||||
*/
|
||||
async _notifyCallStatusChange({callStatus, sipStatus, sipReason, duration}) {
|
||||
async _notifyCallStatusChange({callStatus, sipStatus, sipReason, duration, headers}) {
|
||||
if (this.callMoved) return;
|
||||
|
||||
// manage record all call.
|
||||
@@ -3077,6 +3089,7 @@ Duration=${duration} `
|
||||
|
||||
this.callInfo.updateCallStatus(callStatus, sipStatus, sipReason);
|
||||
if (typeof duration === 'number') this.callInfo.duration = duration;
|
||||
if (headers) this.callInfo.sipHeaders = headers;
|
||||
this.executeStatusCallback(callStatus, sipStatus);
|
||||
|
||||
// update calls db
|
||||
|
||||
@@ -85,8 +85,8 @@ class InboundCallSession extends CallSession {
|
||||
/**
|
||||
* This is invoked when the caller hangs up, in order to calculate the call duration.
|
||||
*/
|
||||
_callerHungup() {
|
||||
this._hangup('caller');
|
||||
_callerHungup(req) {
|
||||
this._hangup('caller', req);
|
||||
}
|
||||
|
||||
_jambonzHangup(reason) {
|
||||
@@ -99,7 +99,7 @@ class InboundCallSession extends CallSession {
|
||||
this._callReleased();
|
||||
}
|
||||
|
||||
_hangup(terminatedBy = 'jambonz') {
|
||||
_hangup(terminatedBy = 'jambonz', req) {
|
||||
if (this.dlg === null) {
|
||||
this.logger.info('InboundCallSession:_hangup - race condition, dlg cleared by app hangup');
|
||||
return;
|
||||
@@ -107,11 +107,13 @@ class InboundCallSession extends CallSession {
|
||||
this.logger.info(`InboundCallSession: ${terminatedBy} hung up`);
|
||||
assert(this.dlg.connectTime);
|
||||
const duration = moment().diff(this.dlg.connectTime, 'seconds');
|
||||
const headers = this._extractCustomHeaders(req);
|
||||
this.rootSpan.setAttributes({'call.termination': `hangup by ${terminatedBy}`});
|
||||
this.callInfo.callTerminationBy = terminatedBy;
|
||||
this.emit('callStatusChange', {
|
||||
callStatus: CallStatus.Completed,
|
||||
duration
|
||||
duration,
|
||||
...(headers && {headers})
|
||||
});
|
||||
this._callReleased();
|
||||
this.req.removeAllListeners('cancel');
|
||||
|
||||
@@ -51,21 +51,22 @@ class RestCallSession extends CallSession {
|
||||
/**
|
||||
* This is invoked when the called party hangs up, in order to calculate the call duration.
|
||||
*/
|
||||
_callerHungup() {
|
||||
this._hangup('caller');
|
||||
_callerHungup(req) {
|
||||
this._hangup('caller', req);
|
||||
}
|
||||
|
||||
_jambonzHangup() {
|
||||
this._hangup();
|
||||
}
|
||||
|
||||
_hangup(terminatedBy = 'jambonz') {
|
||||
_hangup(terminatedBy = 'jambonz', req) {
|
||||
if (this.restDialTask) {
|
||||
this.restDialTask.turnOffAmd();
|
||||
}
|
||||
this.callInfo.callTerminationBy = terminatedBy;
|
||||
const duration = moment().diff(this.dlg.connectTime, 'seconds');
|
||||
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
|
||||
const headers = this._extractCustomHeaders(req);
|
||||
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration, ...(headers && {headers})});
|
||||
this.logger.info(`RestCallSession: called party hung up by ${terminatedBy}`);
|
||||
this._callReleased();
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ const google_server_events = [
|
||||
'error',
|
||||
'session.created',
|
||||
'session.updated',
|
||||
'llm_event',
|
||||
];
|
||||
|
||||
const expandWildcards = (events) => {
|
||||
@@ -36,9 +37,10 @@ class TaskLlmGoogle_S2S extends Task {
|
||||
this.model = this.parent.model || 'models/gemini-2.0-flash-live-001';
|
||||
this.auth = this.parent.auth;
|
||||
this.connectionOptions = this.parent.connectOptions;
|
||||
const {host, version} = this.connectionOptions || {};
|
||||
const {host, version, path} = this.connectionOptions || {};
|
||||
this.host = host;
|
||||
this.version = version;
|
||||
this.path = path;
|
||||
|
||||
const {apiKey} = this.auth || {};
|
||||
if (!apiKey) throw new Error('auth.apiKey is required for Google S2S');
|
||||
@@ -49,11 +51,12 @@ class TaskLlmGoogle_S2S extends Task {
|
||||
this.eventHook = this.data.eventHook;
|
||||
this.toolHook = this.data.toolHook;
|
||||
|
||||
const {setup, sessionResumption} = this.data.llmOptions;
|
||||
const {setup, sessionResumption, greeting} = this.data.llmOptions;
|
||||
|
||||
if (typeof setup !== 'object') {
|
||||
throw new Error('llmOptions with an initial setup is required for Google S2S');
|
||||
}
|
||||
this.greeting = typeof greeting === 'string' ? greeting : null;
|
||||
this.setup = {
|
||||
...setup,
|
||||
model: this.model,
|
||||
@@ -144,7 +147,9 @@ class TaskLlmGoogle_S2S extends Task {
|
||||
const args = [ep.uuid, 'session.create', this.apiKey];
|
||||
if (this.host) {
|
||||
args.push(this.host);
|
||||
if (this.version) args.push(this.version);
|
||||
// 5th arg: full path (preferred) or legacy version string. `path` wins.
|
||||
if (this.path) args.push(this.path);
|
||||
else if (this.version) args.push(this.version);
|
||||
}
|
||||
await this._api(ep, args);
|
||||
} catch (err) {
|
||||
@@ -193,6 +198,17 @@ class TaskLlmGoogle_S2S extends Task {
|
||||
})) {
|
||||
this.logger.debug(this.setup, 'TaskLlmGoogle_S2S:_sendInitialMessage - sending session.update');
|
||||
this.notifyTaskDone();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.greeting) {
|
||||
this.logger.debug({text: this.greeting}, 'TaskLlmGoogle_S2S:_sendInitialMessage - sending proactive greeting');
|
||||
// Use realtimeInput.text — clientContent is only for seeding history on
|
||||
// gemini-3.1-flash-live-preview and does not trigger a model response.
|
||||
// realtimeInput.text works across 2.0 and 3.1 live models.
|
||||
await this._sendClientEvent(ep, {
|
||||
realtimeInput: {text: this.greeting}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -265,10 +265,11 @@ class SingleDialer extends Emitter {
|
||||
}
|
||||
|
||||
this.dlg
|
||||
.on('destroy', () => {
|
||||
.on('destroy', (req) => {
|
||||
const duration = moment().diff(connectTime, 'seconds');
|
||||
const headers = this._extractCustomHeaders(req);
|
||||
this.logger.debug('SingleDialer:exec called party hung up');
|
||||
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
|
||||
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration, ...(headers && {headers})});
|
||||
this.ep && this.ep.destroy();
|
||||
})
|
||||
.on('refresh', () => this.logger.info('SingleDialer:exec - dialog refreshed by uas'))
|
||||
@@ -530,7 +531,19 @@ class SingleDialer extends Emitter {
|
||||
}
|
||||
}
|
||||
|
||||
_notifyCallStatusChange({callStatus, sipStatus, sipReason, duration}) {
|
||||
_extractCustomHeaders(req) {
|
||||
const dominated = ['via', 'from', 'to', 'call-id', 'cseq', 'max-forwards',
|
||||
'content-length', 'contact', 'route', 'record-route', 'content-type',
|
||||
'authorization', 'proxy-authorization', 'www-authenticate', 'proxy-authenticate'];
|
||||
if (!req || !req.headers) return null;
|
||||
const headers = {};
|
||||
Object.keys(req.headers).forEach((h) => {
|
||||
if (!dominated.includes(h)) headers[h] = req.headers[h];
|
||||
});
|
||||
return Object.keys(headers).length ? headers : null;
|
||||
}
|
||||
|
||||
_notifyCallStatusChange({callStatus, sipStatus, sipReason, duration, headers}) {
|
||||
assert((typeof duration === 'number' && callStatus === CallStatus.Completed) ||
|
||||
(!duration && callStatus !== CallStatus.Completed),
|
||||
'duration MUST be supplied when call completed AND ONLY when call completed');
|
||||
@@ -538,6 +551,7 @@ class SingleDialer extends Emitter {
|
||||
if (this.callInfo) {
|
||||
this.callInfo.updateCallStatus(callStatus, sipStatus, sipReason);
|
||||
if (typeof duration === 'number') this.callInfo.duration = duration;
|
||||
if (headers) this.callInfo.sipHeaders = headers;
|
||||
try {
|
||||
this.notifier.request('call:status', this.application.call_status_hook, this.callInfo.toJSON());
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user