mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2025-12-21 09:08:02 +00:00
Merge branch 'main' into fix/1155
This commit is contained in:
95
app.js
95
app.js
@@ -27,8 +27,61 @@ const pino = require('pino');
|
|||||||
const logger = pino(opts, pino.destination({sync: false}));
|
const logger = pino(opts, pino.destination({sync: false}));
|
||||||
const {LifeCycleEvents, FS_UUID_SET_NAME, SystemState, FEATURE_SERVER} = require('./lib/utils/constants');
|
const {LifeCycleEvents, FS_UUID_SET_NAME, SystemState, FEATURE_SERVER} = require('./lib/utils/constants');
|
||||||
const installSrfLocals = require('./lib/utils/install-srf-locals');
|
const installSrfLocals = require('./lib/utils/install-srf-locals');
|
||||||
installSrfLocals(srf, logger);
|
const createHttpListener = require('./lib/utils/http-listener');
|
||||||
|
const healthCheck = require('@jambonz/http-health-check');
|
||||||
|
|
||||||
|
logger.on('level-change', (lvl, _val, prevLvl, _prevVal, instance) => {
|
||||||
|
if (logger !== instance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.info('system log level %s was changed to %s', prevLvl, lvl);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Install the srf locals
|
||||||
|
installSrfLocals(srf, logger, {
|
||||||
|
onFreeswitchConnect: (wraper) => {
|
||||||
|
// Only connect to drachtio if freeswitch is connected
|
||||||
|
logger.info(`connected to freeswitch at ${wraper.ms.address}, start drachtio server`);
|
||||||
|
if (DRACHTIO_HOST) {
|
||||||
|
srf.connect({host: DRACHTIO_HOST, port: DRACHTIO_PORT, secret: DRACHTIO_SECRET });
|
||||||
|
srf.on('connect', (err, hp) => {
|
||||||
|
const arr = /^(.*)\/(.*)$/.exec(hp.split(',').pop());
|
||||||
|
srf.locals.localSipAddress = `${arr[2]}`;
|
||||||
|
logger.info(`connected to drachtio listening on ${hp}, local sip address is ${srf.locals.localSipAddress}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
logger.info(`listening for drachtio requests on port ${DRACHTIO_PORT}`);
|
||||||
|
srf.listen({port: DRACHTIO_PORT, secret: DRACHTIO_SECRET});
|
||||||
|
}
|
||||||
|
// Start Http server
|
||||||
|
createHttpListener(logger, srf)
|
||||||
|
.then(({server, app}) => {
|
||||||
|
httpServer = server;
|
||||||
|
healthCheck({app, logger, path: '/', fn: getCount});
|
||||||
|
return {server, app};
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
logger.error(err, 'Error creating http listener');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onFreeswitchDisconnect: (wraper) => {
|
||||||
|
// check if all freeswitch connections are lost, disconnect drachtio server
|
||||||
|
logger.info(`lost connection to freeswitch at ${wraper.ms.address}`);
|
||||||
|
const ms = srf.locals.getFreeswitch();
|
||||||
|
if (!ms) {
|
||||||
|
logger.info('no freeswitch connections, stopping drachtio server');
|
||||||
|
disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (NODE_ENV === 'test') {
|
||||||
|
srf.on('error', (err) => {
|
||||||
|
logger.info(err, 'Error connecting to drachtio');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init services
|
||||||
const writeSystemAlerts = srf.locals?.writeSystemAlerts;
|
const writeSystemAlerts = srf.locals?.writeSystemAlerts;
|
||||||
if (writeSystemAlerts) {
|
if (writeSystemAlerts) {
|
||||||
writeSystemAlerts({
|
writeSystemAlerts({
|
||||||
@@ -54,24 +107,6 @@ const {
|
|||||||
const InboundCallSession = require('./lib/session/inbound-call-session');
|
const InboundCallSession = require('./lib/session/inbound-call-session');
|
||||||
const SipRecCallSession = require('./lib/session/siprec-call-session');
|
const SipRecCallSession = require('./lib/session/siprec-call-session');
|
||||||
|
|
||||||
if (DRACHTIO_HOST) {
|
|
||||||
srf.connect({host: DRACHTIO_HOST, port: DRACHTIO_PORT, secret: DRACHTIO_SECRET });
|
|
||||||
srf.on('connect', (err, hp) => {
|
|
||||||
const arr = /^(.*)\/(.*)$/.exec(hp.split(',').pop());
|
|
||||||
srf.locals.localSipAddress = `${arr[2]}`;
|
|
||||||
logger.info(`connected to drachtio listening on ${hp}, local sip address is ${srf.locals.localSipAddress}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
logger.info(`listening for drachtio requests on port ${DRACHTIO_PORT}`);
|
|
||||||
srf.listen({port: DRACHTIO_PORT, secret: DRACHTIO_SECRET});
|
|
||||||
}
|
|
||||||
if (NODE_ENV === 'test') {
|
|
||||||
srf.on('error', (err) => {
|
|
||||||
logger.info(err, 'Error connecting to drachtio');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
srf.use('invite', [
|
srf.use('invite', [
|
||||||
initLocals,
|
initLocals,
|
||||||
createRootSpan,
|
createRootSpan,
|
||||||
@@ -97,27 +132,20 @@ sessionTracker.on('idle', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
const getCount = () => sessionTracker.count;
|
const getCount = () => sessionTracker.count;
|
||||||
const healthCheck = require('@jambonz/http-health-check');
|
|
||||||
let httpServer;
|
let httpServer;
|
||||||
|
|
||||||
const createHttpListener = require('./lib/utils/http-listener');
|
|
||||||
createHttpListener(logger, srf)
|
|
||||||
.then(({server, app}) => {
|
|
||||||
httpServer = server;
|
|
||||||
healthCheck({app, logger, path: '/', fn: getCount});
|
|
||||||
return {server, app};
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
logger.error(err, 'Error creating http listener');
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const monInterval = setInterval(async() => {
|
const monInterval = setInterval(async() => {
|
||||||
srf.locals.stats.gauge('fs.sip.calls.count', sessionTracker.count);
|
srf.locals.stats.gauge('fs.sip.calls.count', sessionTracker.count);
|
||||||
try {
|
try {
|
||||||
const systemInformation = await srf.locals.dbHelpers.lookupSystemInformation();
|
const systemInformation = await srf.locals.dbHelpers.lookupSystemInformation();
|
||||||
if (systemInformation && systemInformation.log_level) {
|
if (systemInformation && systemInformation.log_level) {
|
||||||
logger.level = systemInformation.log_level;
|
const envLogLevel = logger.levels.values[JAMBONES_LOGLEVEL.toLowerCase()];
|
||||||
|
const dbLogLevel = logger.levels.values[systemInformation.log_level];
|
||||||
|
const appliedLogLevel = Math.min(envLogLevel, dbLogLevel);
|
||||||
|
if (logger.levelVal !== appliedLogLevel) {
|
||||||
|
logger.level = logger.levels.labels[Math.min(envLogLevel, dbLogLevel)];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (process.env.NODE_ENV === 'test') {
|
if (process.env.NODE_ENV === 'test') {
|
||||||
@@ -133,6 +161,7 @@ const disconnect = () => {
|
|||||||
httpServer?.on('close', resolve);
|
httpServer?.on('close', resolve);
|
||||||
httpServer?.close();
|
httpServer?.close();
|
||||||
srf.disconnect();
|
srf.disconnect();
|
||||||
|
srf.removeAllListeners();
|
||||||
srf.locals.mediaservers?.forEach((ms) => ms.disconnect());
|
srf.locals.mediaservers?.forEach((ms) => ms.disconnect());
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const {
|
|||||||
JAMBONES_DISABLE_DIRECT_P2P_CALL
|
JAMBONES_DISABLE_DIRECT_P2P_CALL
|
||||||
} = require('./config');
|
} = require('./config');
|
||||||
const { createJambonzApp } = require('./dynamic-apps');
|
const { createJambonzApp } = require('./dynamic-apps');
|
||||||
|
const { decrypt } = require('./utils/encrypt-decrypt');
|
||||||
|
|
||||||
module.exports = function(srf, logger) {
|
module.exports = function(srf, logger) {
|
||||||
const {
|
const {
|
||||||
@@ -348,11 +349,10 @@ module.exports = function(srf, logger) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.locals.application = app2;
|
req.locals.application = app2;
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
const {call_hook, call_status_hook, ...appInfo} = app; // mask sensitive data like user/pass on webhook
|
const {call_hook, call_status_hook, ...appInfo} = app; // mask sensitive data like user/pass on webhook
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
const {requestor, notifier, ...loggable} = appInfo;
|
const {requestor, notifier, env_vars, ...loggable} = appInfo;
|
||||||
logger.info({app: loggable}, `retrieved application for incoming call to ${req.locals.calledNumber}`);
|
logger.info({app: loggable}, `retrieved application for incoming call to ${req.locals.calledNumber}`);
|
||||||
req.locals.callInfo = new CallInfo({
|
req.locals.callInfo = new CallInfo({
|
||||||
req,
|
req,
|
||||||
@@ -417,10 +417,22 @@ module.exports = function(srf, logger) {
|
|||||||
...(app.fallback_speech_recognizer_language && {fallback_language: app.fallback_speech_recognizer_language})
|
...(app.fallback_speech_recognizer_language && {fallback_language: app.fallback_speech_recognizer_language})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let env_vars;
|
||||||
|
try {
|
||||||
|
if (app.env_vars) {
|
||||||
|
const d_env_vars = JSON.parse(decrypt(app.env_vars));
|
||||||
|
logger.info(`Setting env_vars: ${Object.keys(d_env_vars)}`); // Only log the keys not the values
|
||||||
|
env_vars = d_env_vars;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.info('Unable to set env_vars', error);
|
||||||
|
}
|
||||||
const params = Object.assign(['POST', 'WS'].includes(app.call_hook.method) ? { sip: req.msg } : {},
|
const params = Object.assign(['POST', 'WS'].includes(app.call_hook.method) ? { sip: req.msg } : {},
|
||||||
req.locals.callInfo,
|
req.locals.callInfo,
|
||||||
{ service_provider_sid: req.locals.service_provider_sid },
|
{ service_provider_sid: req.locals.service_provider_sid },
|
||||||
{ defaults });
|
{ defaults },
|
||||||
|
{ env_vars }
|
||||||
|
);
|
||||||
logger.debug({ params }, 'sending initial webhook');
|
logger.debug({ params }, 'sending initial webhook');
|
||||||
const obj = rootSpan.startChildSpan('performAppWebhook');
|
const obj = rootSpan.startChildSpan('performAppWebhook');
|
||||||
span = obj.span;
|
span = obj.span;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const HttpRequestor = require('../utils/http-requestor');
|
|||||||
const WsRequestor = require('../utils/ws-requestor');
|
const WsRequestor = require('../utils/ws-requestor');
|
||||||
const ActionHookDelayProcessor = require('../utils/action-hook-delay');
|
const ActionHookDelayProcessor = require('../utils/action-hook-delay');
|
||||||
const TtsStreamingBuffer = require('../utils/tts-streaming-buffer');
|
const TtsStreamingBuffer = require('../utils/tts-streaming-buffer');
|
||||||
|
const StickyEventEmitter = require('../utils/sticky-event-emitter');
|
||||||
const {parseUri} = require('drachtio-srf');
|
const {parseUri} = require('drachtio-srf');
|
||||||
const {
|
const {
|
||||||
JAMBONES_INJECT_CONTENT,
|
JAMBONES_INJECT_CONTENT,
|
||||||
@@ -79,6 +80,10 @@ class CallSession extends Emitter {
|
|||||||
this.callGone = false;
|
this.callGone = false;
|
||||||
this.notifiedComplete = false;
|
this.notifiedComplete = false;
|
||||||
this.rootSpan = rootSpan;
|
this.rootSpan = rootSpan;
|
||||||
|
this.stickyEventEmitter = new StickyEventEmitter();
|
||||||
|
this.stickyEventEmitter.onSuccess = () => {
|
||||||
|
this.taskInProgress = null;
|
||||||
|
};
|
||||||
this.backgroundTaskManager = new BackgroundTaskManager({
|
this.backgroundTaskManager = new BackgroundTaskManager({
|
||||||
cs: this,
|
cs: this,
|
||||||
logger,
|
logger,
|
||||||
@@ -1180,7 +1185,9 @@ class CallSession extends Emitter {
|
|||||||
const taskNum = ++this.taskIdx;
|
const taskNum = ++this.taskIdx;
|
||||||
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.isCurTaskPlay = TaskName.Play === task.name;
|
||||||
|
this.taskInProgress = task;
|
||||||
|
this.logger.info(`CallSession:exec starting task #${stackNum}:${taskNum}: ${task.name} : {task.taskId}`);
|
||||||
this._notifyTaskStatus(task, {event: 'starting'});
|
this._notifyTaskStatus(task, {event: 'starting'});
|
||||||
// Register verbhook span wait for end
|
// Register verbhook span wait for end
|
||||||
task.on('VerbHookSpanWaitForEnd', ({span}) => {
|
task.on('VerbHookSpanWaitForEnd', ({span}) => {
|
||||||
@@ -1919,6 +1926,8 @@ Duration=${duration} `
|
|||||||
this.logger.debug({tasks: listTaskNames(tasks)},
|
this.logger.debug({tasks: listTaskNames(tasks)},
|
||||||
`CallSession:replaceApplication reset with ${tasks.length} new tasks, stack depth is ${this.stackIdx}`);
|
`CallSession:replaceApplication reset with ${tasks.length} new tasks, stack depth is ${this.stackIdx}`);
|
||||||
if (this.currentTask) {
|
if (this.currentTask) {
|
||||||
|
this.logger.debug('CallSession:replaceApplication - killing current task ' +
|
||||||
|
this.currentTask?.name + ', taskId: ' + this.currentTask.taskId);
|
||||||
this.currentTask.kill(this, KillReason.Replaced);
|
this.currentTask.kill(this, KillReason.Replaced);
|
||||||
this.currentTask = null;
|
this.currentTask = null;
|
||||||
}
|
}
|
||||||
@@ -1927,6 +1936,10 @@ Duration=${duration} `
|
|||||||
this.wakeupResolver({reason: 'new tasks'});
|
this.wakeupResolver({reason: 'new tasks'});
|
||||||
this.wakeupResolver = null;
|
this.wakeupResolver = null;
|
||||||
}
|
}
|
||||||
|
if ((!this.currentTask || this.currentTask === undefined) && this.isCurTaskPlay) {
|
||||||
|
this.logger.debug(`CallSession:replaceApplication - emitting uuid_break, taskId: ${this.taskInProgress?.taskId}`);
|
||||||
|
this.stickyEventEmitter.emit('uuid_break', this.taskInProgress);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
kill(onBackgroundGatherBargein = false) {
|
kill(onBackgroundGatherBargein = false) {
|
||||||
@@ -2387,6 +2400,9 @@ Duration=${duration} `
|
|||||||
* Hang up the call and free the media endpoint
|
* Hang up the call and free the media endpoint
|
||||||
*/
|
*/
|
||||||
async _clearResources() {
|
async _clearResources() {
|
||||||
|
this.stickyEventEmitter.destroy();
|
||||||
|
this.stickyEventEmitter = null;
|
||||||
|
this.taskInProgress = null;
|
||||||
for (const resource of [this.dlg, this.ep, this.ep2]) {
|
for (const resource of [this.dlg, this.ep, this.ep2]) {
|
||||||
try {
|
try {
|
||||||
if (resource && resource.connected) await resource.destroy();
|
if (resource && resource.connected) await resource.destroy();
|
||||||
|
|||||||
@@ -685,7 +685,9 @@ class TaskGather extends SttTask {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_startAsrTimer() {
|
_startAsrTimer() {
|
||||||
if (this.vendor === 'deepgram') return; // no need
|
// Deepgram has a case that UtteranceEnd is not sent to cover the last word end time.
|
||||||
|
// So we need to wait for the asrTimeout to be sure that the last word is sent.
|
||||||
|
// if (this.vendor === 'deepgram') return; // no need
|
||||||
assert(this.isContinuousAsr);
|
assert(this.isContinuousAsr);
|
||||||
this._clearAsrTimer();
|
this._clearAsrTimer();
|
||||||
this._asrTimer = setTimeout(() => {
|
this._asrTimer = setTimeout(() => {
|
||||||
@@ -845,7 +847,8 @@ class TaskGather extends SttTask {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const utteranceTime = evt.last_word_end;
|
const utteranceTime = evt.last_word_end;
|
||||||
if (utteranceTime && this._dgTimeOfLastUnprocessedWord && utteranceTime < this._dgTimeOfLastUnprocessedWord) {
|
// eslint-disable-next-line max-len
|
||||||
|
if (utteranceTime && this._dgTimeOfLastUnprocessedWord && utteranceTime < this._dgTimeOfLastUnprocessedWord && utteranceTime != -1) {
|
||||||
this.logger.debug('Gather:_onTranscription - got UtteranceEnd with unprocessed words, continue listening');
|
this.logger.debug('Gather:_onTranscription - got UtteranceEnd with unprocessed words, continue listening');
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ const TaskLlmOpenAI_S2S = require('./llms/openai_s2s');
|
|||||||
const TaskLlmVoiceAgent_S2S = require('./llms/voice_agent_s2s');
|
const TaskLlmVoiceAgent_S2S = require('./llms/voice_agent_s2s');
|
||||||
const TaskLlmUltravox_S2S = require('./llms/ultravox_s2s');
|
const TaskLlmUltravox_S2S = require('./llms/ultravox_s2s');
|
||||||
const TaskLlmElevenlabs_S2S = require('./llms/elevenlabs_s2s');
|
const TaskLlmElevenlabs_S2S = require('./llms/elevenlabs_s2s');
|
||||||
|
const TaskLlmGoogle_S2S = require('./llms/google_s2s');
|
||||||
|
const LlmMcpService = require('../../utils/llm-mcp');
|
||||||
|
|
||||||
class TaskLlm extends Task {
|
class TaskLlm extends Task {
|
||||||
constructor(logger, opts) {
|
constructor(logger, opts) {
|
||||||
@@ -18,6 +20,8 @@ class TaskLlm extends Task {
|
|||||||
|
|
||||||
// delegate to the specific llm model
|
// delegate to the specific llm model
|
||||||
this.llm = this.createSpecificLlm();
|
this.llm = this.createSpecificLlm();
|
||||||
|
// MCP
|
||||||
|
this.mcpServers = this.data.mcpServers || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
get name() { return this.llm.name ; }
|
get name() { return this.llm.name ; }
|
||||||
@@ -28,14 +32,32 @@ class TaskLlm extends Task {
|
|||||||
|
|
||||||
get ep() { return this.cs.ep; }
|
get ep() { return this.cs.ep; }
|
||||||
|
|
||||||
|
get mcpService() {
|
||||||
|
return this.llmMcpService;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isMcpEnabled() {
|
||||||
|
return this.mcpServers.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
async exec(cs, {ep}) {
|
async exec(cs, {ep}) {
|
||||||
await super.exec(cs, {ep});
|
await super.exec(cs, {ep});
|
||||||
|
|
||||||
|
// create the MCP service if we have MCP servers
|
||||||
|
if (this.isMcpEnabled) {
|
||||||
|
this.llmMcpService = new LlmMcpService(this.logger, this.mcpServers);
|
||||||
|
await this.llmMcpService.init();
|
||||||
|
}
|
||||||
await this.llm.exec(cs, {ep});
|
await this.llm.exec(cs, {ep});
|
||||||
}
|
}
|
||||||
|
|
||||||
async kill(cs) {
|
async kill(cs) {
|
||||||
super.kill(cs);
|
super.kill(cs);
|
||||||
await this.llm.kill(cs);
|
await this.llm.kill(cs);
|
||||||
|
// clean up MCP clients
|
||||||
|
if (this.isMcpEnabled) {
|
||||||
|
await this.mcpService.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createSpecificLlm() {
|
createSpecificLlm() {
|
||||||
@@ -59,6 +81,10 @@ class TaskLlm extends Task {
|
|||||||
llm = new TaskLlmElevenlabs_S2S(this.logger, this.data, this);
|
llm = new TaskLlmElevenlabs_S2S(this.logger, this.data, this);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'google':
|
||||||
|
llm = new TaskLlmGoogle_S2S(this.logger, this.data, this);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported vendor ${this.vendor} for LLM`);
|
throw new Error(`Unsupported vendor ${this.vendor} for LLM`);
|
||||||
}
|
}
|
||||||
@@ -82,8 +108,15 @@ class TaskLlm extends Task {
|
|||||||
await this.cs?.requestor.request('llm:event', this.eventHook, data);
|
await this.cs?.requestor.request('llm:event', this.eventHook, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async sendToolHook(tool_call_id, data) {
|
async sendToolHook(tool_call_id, data) {
|
||||||
await this.cs?.requestor.request('llm:tool-call', this.toolHook, {tool_call_id, ...data});
|
const tool_response = await this.cs?.requestor.request('llm:tool-call', this.toolHook, {tool_call_id, ...data});
|
||||||
|
// if the toolHook was a websocket it will return undefined, otherwise it should return an object
|
||||||
|
if (typeof tool_response != 'undefined') {
|
||||||
|
tool_response.type = 'client_tool_result';
|
||||||
|
tool_response.invocation_id = tool_call_id;
|
||||||
|
this.processToolOutput(tool_call_id, tool_response);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async processToolOutput(tool_call_id, data) {
|
async processToolOutput(tool_call_id, data) {
|
||||||
|
|||||||
@@ -244,13 +244,36 @@ class TaskLlmElevenlabs_S2S extends Task {
|
|||||||
/* tool calls */
|
/* tool calls */
|
||||||
else if (type === 'client_tool_call') {
|
else if (type === 'client_tool_call') {
|
||||||
this.logger.debug({evt}, 'TaskLlmElevenlabs_S2S:_onServerEvent - function_call');
|
this.logger.debug({evt}, 'TaskLlmElevenlabs_S2S:_onServerEvent - function_call');
|
||||||
if (!this.toolHook) {
|
const {tool_name: name, tool_call_id: call_id, parameters: args} = evt.client_tool_call;
|
||||||
|
|
||||||
|
const mcpTools = this.parent.isMcpEnabled ? await this.parent.mcpService.getAvailableMcpTools() : [];
|
||||||
|
if (mcpTools.some((tool) => tool.name === name)) {
|
||||||
|
this.logger.debug({name, args}, 'TaskLlmElevenlabs_S2S:_onServerEvent - calling mcp tool');
|
||||||
|
try {
|
||||||
|
const res = await this.parent.mcpService.callMcpTool(name, args);
|
||||||
|
this.logger.debug({res}, 'TaskLlmElevenlabs_S2S:_onServerEvent - function_call - mcp result');
|
||||||
|
this.processToolOutput(ep, call_id, {
|
||||||
|
data: {
|
||||||
|
type: 'client_tool_result',
|
||||||
|
tool_call_id: call_id,
|
||||||
|
result: res.content?.length ? res.content[0] : res.content,
|
||||||
|
is_error: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
this.logger.info({err, evt}, 'TaskLlmElevenlabs_S2S - error calling mcp tool');
|
||||||
|
this.results = {
|
||||||
|
completionReason: 'client error calling mcp function',
|
||||||
|
error: err
|
||||||
|
};
|
||||||
|
endConversation = true;
|
||||||
|
}
|
||||||
|
} else if (!this.toolHook) {
|
||||||
this.logger.warn({evt}, 'TaskLlmElevenlabs_S2S:_onServerEvent - no toolHook defined!');
|
this.logger.warn({evt}, 'TaskLlmElevenlabs_S2S:_onServerEvent - no toolHook defined!');
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const {client_tool_call} = evt;
|
|
||||||
const {tool_name: name, tool_call_id: call_id, parameters: args} = client_tool_call;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.parent.sendToolHook(call_id, {name, args});
|
await this.parent.sendToolHook(call_id, {name, args});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
313
lib/tasks/llm/llms/google_s2s.js
Normal file
313
lib/tasks/llm/llms/google_s2s.js
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
const Task = require('../../task');
|
||||||
|
const TaskName = 'Llm_Google_s2s';
|
||||||
|
const {LlmEvents_Google} = require('../../../utils/constants');
|
||||||
|
const ClientEvent = 'client.event';
|
||||||
|
const SessionDelete = 'session.delete';
|
||||||
|
|
||||||
|
const google_server_events = [
|
||||||
|
'error',
|
||||||
|
'session.created',
|
||||||
|
'session.updated',
|
||||||
|
];
|
||||||
|
|
||||||
|
const expandWildcards = (events) => {
|
||||||
|
const expandedEvents = [];
|
||||||
|
|
||||||
|
events.forEach((evt) => {
|
||||||
|
if (evt.endsWith('.*')) {
|
||||||
|
const prefix = evt.slice(0, -2); // Remove the wildcard ".*"
|
||||||
|
const matchingEvents = google_server_events.filter((e) => e.startsWith(prefix));
|
||||||
|
expandedEvents.push(...matchingEvents);
|
||||||
|
} else {
|
||||||
|
expandedEvents.push(evt);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return expandedEvents;
|
||||||
|
};
|
||||||
|
|
||||||
|
class TaskLlmGoogle_S2S extends Task {
|
||||||
|
constructor(logger, opts, parentTask) {
|
||||||
|
super(logger, opts, parentTask);
|
||||||
|
this.parent = parentTask;
|
||||||
|
|
||||||
|
this.vendor = this.parent.vendor;
|
||||||
|
this.vendor = this.parent.vendor;
|
||||||
|
this.model = this.parent.model || 'gemini-2o';
|
||||||
|
this.auth = this.parent.auth;
|
||||||
|
this.connectionOptions = this.parent.connectOptions;
|
||||||
|
|
||||||
|
const {apiKey} = this.auth || {};
|
||||||
|
if (!apiKey) throw new Error('auth.apiKey is required for Google S2S');
|
||||||
|
|
||||||
|
this.apiKey = apiKey;
|
||||||
|
|
||||||
|
this.actionHook = this.data.actionHook;
|
||||||
|
this.eventHook = this.data.eventHook;
|
||||||
|
this.toolHook = this.data.toolHook;
|
||||||
|
|
||||||
|
const {setup} = this.data.llmOptions;
|
||||||
|
|
||||||
|
if (typeof setup !== 'object') {
|
||||||
|
throw new Error('llmOptions with an initial setup is required for Google S2S');
|
||||||
|
}
|
||||||
|
this.setup = {
|
||||||
|
...setup,
|
||||||
|
// make sure output is always audio
|
||||||
|
generationConfig: {
|
||||||
|
...(setup.generationConfig || {}),
|
||||||
|
responseModalities: 'audio'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.results = {
|
||||||
|
completionReason: 'normal conversation end'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* only one of these will have items,
|
||||||
|
* if includeEvents, then these are the events to include
|
||||||
|
* if excludeEvents, then these are the events to exclude
|
||||||
|
*/
|
||||||
|
this.includeEvents = [];
|
||||||
|
this.excludeEvents = [];
|
||||||
|
|
||||||
|
/* default to all events if user did not specify */
|
||||||
|
this._populateEvents(this.data.events || google_server_events);
|
||||||
|
|
||||||
|
this.addCustomEventListener = parentTask.addCustomEventListener.bind(parentTask);
|
||||||
|
this.removeCustomEventListeners = parentTask.removeCustomEventListeners.bind(parentTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() { return TaskName; }
|
||||||
|
|
||||||
|
async _api(ep, args) {
|
||||||
|
const res = await ep.api('uuid_google_s2s', `^^|${args.join('|')}`);
|
||||||
|
if (!res.body?.startsWith('+OK')) {
|
||||||
|
throw new Error({args}, `Error calling uuid_openai_s2s: ${res.body}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async exec(cs, {ep}) {
|
||||||
|
await super.exec(cs);
|
||||||
|
|
||||||
|
await this._startListening(cs, ep);
|
||||||
|
|
||||||
|
await this.awaitTaskDone();
|
||||||
|
|
||||||
|
/* note: the parent llm verb started the span, which is why this is necessary */
|
||||||
|
await this.parent.performAction(this.results);
|
||||||
|
|
||||||
|
this._unregisterHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
async kill(cs) {
|
||||||
|
super.kill(cs);
|
||||||
|
|
||||||
|
this._api(cs.ep, [cs.ep.uuid, SessionDelete])
|
||||||
|
.catch((err) => this.logger.info({err}, 'TaskLlmGoogle_S2S:kill - error deleting session'));
|
||||||
|
|
||||||
|
this.notifyTaskDone();
|
||||||
|
}
|
||||||
|
|
||||||
|
_populateEvents(events) {
|
||||||
|
if (events.includes('all')) {
|
||||||
|
/* work by excluding specific events */
|
||||||
|
const exclude = events
|
||||||
|
.filter((evt) => evt.startsWith('-'))
|
||||||
|
.map((evt) => evt.slice(1));
|
||||||
|
if (exclude.length === 0) this.includeEvents = google_server_events;
|
||||||
|
else this.excludeEvents = expandWildcards(exclude);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
/* work by including specific events */
|
||||||
|
const include = events
|
||||||
|
.filter((evt) => !evt.startsWith('-'));
|
||||||
|
this.includeEvents = expandWildcards(include);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.debug({
|
||||||
|
includeEvents: this.includeEvents,
|
||||||
|
excludeEvents: this.excludeEvents
|
||||||
|
}, 'TaskLlmGoogle_S2S:_populateEvents');
|
||||||
|
}
|
||||||
|
|
||||||
|
async _startListening(cs, ep) {
|
||||||
|
this._registerHandlers(ep);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const args = [ep.uuid, 'session.create', this.apiKey];
|
||||||
|
await this._api(ep, args);
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error({err}, 'TaskLlmGoogle_S2S:_startListening');
|
||||||
|
this.notifyTaskDone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _sendClientEvent(ep, obj) {
|
||||||
|
let ok = true;
|
||||||
|
this.logger.debug({obj}, 'TaskLlmGoogle_S2S:_sendClientEvent');
|
||||||
|
try {
|
||||||
|
const args = [ep.uuid, ClientEvent, JSON.stringify(obj)];
|
||||||
|
await this._api(ep, args);
|
||||||
|
} catch (err) {
|
||||||
|
ok = false;
|
||||||
|
this.logger.error({err}, 'TaskLlmGoogle_S2S:_sendClientEvent - Error');
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _sendInitialMessage(ep) {
|
||||||
|
const setup = this.setup;
|
||||||
|
const mcpTools = this.parent.isMcpEnabled ? await this.parent.mcpService.getAvailableMcpTools() : [];
|
||||||
|
if (mcpTools && mcpTools.length > 0) {
|
||||||
|
const convertedTools = mcpTools.map((tool) => {
|
||||||
|
return {
|
||||||
|
functionDeclarations: {
|
||||||
|
name: tool.name,
|
||||||
|
description: tool.description,
|
||||||
|
parameters: tool.inputSchema,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// merge with any existing tools
|
||||||
|
setup.tools = [...convertedTools, ...(this.setup.tools || [])];
|
||||||
|
}
|
||||||
|
if (!await this._sendClientEvent(ep, {
|
||||||
|
setup,
|
||||||
|
})) {
|
||||||
|
this.logger.debug(this.setup, 'TaskLlmGoogle_S2S:_sendInitialMessage - sending session.update');
|
||||||
|
this.notifyTaskDone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_registerHandlers(ep) {
|
||||||
|
this.addCustomEventListener(ep, LlmEvents_Google.Connect, this._onConnect.bind(this, ep));
|
||||||
|
this.addCustomEventListener(ep, LlmEvents_Google.ConnectFailure, this._onConnectFailure.bind(this, ep));
|
||||||
|
this.addCustomEventListener(ep, LlmEvents_Google.Disconnect, this._onDisconnect.bind(this, ep));
|
||||||
|
this.addCustomEventListener(ep, LlmEvents_Google.ServerEvent, this._onServerEvent.bind(this, ep));
|
||||||
|
}
|
||||||
|
|
||||||
|
_unregisterHandlers() {
|
||||||
|
this.removeCustomEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
_onError(ep, evt) {
|
||||||
|
this.logger.info({evt}, 'TaskLlmGoogle_S2S:_onError');
|
||||||
|
this.notifyTaskDone();
|
||||||
|
}
|
||||||
|
|
||||||
|
_onConnect(ep) {
|
||||||
|
this.logger.debug('TaskLlmGoogle_S2S:_onConnect');
|
||||||
|
this._sendInitialMessage(ep);
|
||||||
|
}
|
||||||
|
_onConnectFailure(_ep, evt) {
|
||||||
|
this.logger.info(evt, 'TaskLlmGoogle_S2S:_onConnectFailure');
|
||||||
|
this.results = {completionReason: 'connection failure'};
|
||||||
|
this.notifyTaskDone();
|
||||||
|
}
|
||||||
|
_onDisconnect(_ep, evt) {
|
||||||
|
this.logger.info(evt, 'TaskLlmGoogle_S2S:_onConnectFailure');
|
||||||
|
this.results = {completionReason: 'disconnect from remote end'};
|
||||||
|
this.notifyTaskDone();
|
||||||
|
}
|
||||||
|
|
||||||
|
async _onServerEvent(ep, evt) {
|
||||||
|
let endConversation = false;
|
||||||
|
this.logger.debug({evt}, 'TaskLlmGoogle_S2S:_onServerEvent');
|
||||||
|
const {toolCall /**toolCallCancellation*/} = evt;
|
||||||
|
|
||||||
|
if (toolCall) {
|
||||||
|
this.logger.debug({toolCall}, 'TaskLlmGoogle_S2S:_onServerEvent - toolCall');
|
||||||
|
if (!this.toolHook) {
|
||||||
|
this.logger.info({evt}, 'TaskLlmGoogle_S2S:_onServerEvent - no toolHook defined!');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const {functionCalls} = toolCall;
|
||||||
|
const mcpTools = this.parent.isMcpEnabled ? await this.parent.mcpService.getAvailableMcpTools() : [];
|
||||||
|
const functionResponses = [];
|
||||||
|
if (mcpTools && mcpTools.length > 0) {
|
||||||
|
for (const functionCall of functionCalls) {
|
||||||
|
const {name, args, id} = functionCall;
|
||||||
|
const tool = mcpTools.find((tool) => tool.name === name);
|
||||||
|
if (tool) {
|
||||||
|
const response = await this.parent.mcpService.callMcpTool(name, args);
|
||||||
|
functionResponses.push({
|
||||||
|
response: {
|
||||||
|
output: response,
|
||||||
|
},
|
||||||
|
id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (functionResponses && functionResponses.length > 0) {
|
||||||
|
this.logger.debug({functionResponses}, 'TaskLlmGoogle_S2S:_onServerEvent - function_call - mcp result');
|
||||||
|
this.processToolOutput(ep, 'tool_call_id', {
|
||||||
|
toolResponse: {
|
||||||
|
functionResponses
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await this.parent.sendToolHook('function_call_id', {type: 'toolCall', functionCalls});
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.info({err, evt}, 'TaskLlmGoogle_S2S - error calling function');
|
||||||
|
this.results = {
|
||||||
|
completionReason: 'client error calling function',
|
||||||
|
error: err
|
||||||
|
};
|
||||||
|
endConversation = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._sendLlmEvent('llm_event', evt);
|
||||||
|
|
||||||
|
if (endConversation) {
|
||||||
|
this.logger.info({results: this.results},
|
||||||
|
'TaskLlmGoogle_S2S:_onServerEvent - ending conversation due to error');
|
||||||
|
this.notifyTaskDone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_sendLlmEvent(type, evt) {
|
||||||
|
/* check whether we should notify on this event */
|
||||||
|
if (this.includeEvents.length > 0 ? this.includeEvents.includes(type) : !this.excludeEvents.includes(type)) {
|
||||||
|
this.parent.sendEventHook(evt)
|
||||||
|
.catch((err) => this.logger.info({err}, 'TaskLlmGoogle_S2S:_onServerEvent - error sending event hook'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async processLlmUpdate(ep, data, _callSid) {
|
||||||
|
try {
|
||||||
|
this.logger.debug({data, _callSid}, 'TaskLlmGoogle_S2S:processLlmUpdate');
|
||||||
|
|
||||||
|
await this._api(ep, [ep.uuid, ClientEvent, JSON.stringify(data)]);
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.info({err, data}, 'TaskLlmGoogle_S2S:processLlmUpdate - Error processing LLM update');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async processToolOutput(ep, tool_call_id, data) {
|
||||||
|
try {
|
||||||
|
this.logger.debug({tool_call_id, data}, 'TaskLlmGoogle_S2S:processToolOutput');
|
||||||
|
const {toolResponse} = data;
|
||||||
|
|
||||||
|
if (!toolResponse) {
|
||||||
|
this.logger.info({data},
|
||||||
|
'TaskLlmGoogle_S2S:processToolOutput - invalid tool output, must be functionResponses');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await this._api(ep, [ep.uuid, ClientEvent, JSON.stringify(data)]);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.info({err, data}, 'TaskLlmGoogle_S2S:processToolOutput - Error processing tool output');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = TaskLlmGoogle_S2S;
|
||||||
@@ -235,6 +235,23 @@ class TaskLlmOpenAI_S2S extends Task {
|
|||||||
|
|
||||||
/* send immediate session.update if present */
|
/* send immediate session.update if present */
|
||||||
else if (this.session_update) {
|
else if (this.session_update) {
|
||||||
|
if (this.parent.isMcpEnabled) {
|
||||||
|
this.logger.debug('TaskLlmOpenAI_S2S:_sendInitialMessage - mcp enabled');
|
||||||
|
const tools = await this.parent.mcpService.getAvailableMcpTools();
|
||||||
|
if (tools && tools.length > 0 && this.session_update) {
|
||||||
|
const convertedTools = tools.map((tool) => ({
|
||||||
|
name: tool.name,
|
||||||
|
type: 'function',
|
||||||
|
description: tool.description,
|
||||||
|
parameters: tool.inputSchema
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.session_update.tools = [
|
||||||
|
...convertedTools,
|
||||||
|
...(this.session_update.tools || [])
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
obj = {type: 'session.update', session: this.session_update};
|
obj = {type: 'session.update', session: this.session_update};
|
||||||
this.logger.debug({obj}, 'TaskLlmOpenAI_S2S:_sendInitialMessage - sending session.update');
|
this.logger.debug({obj}, 'TaskLlmOpenAI_S2S:_sendInitialMessage - sending session.update');
|
||||||
if (!await this._sendClientEvent(ep, obj)) {
|
if (!await this._sendClientEvent(ep, obj)) {
|
||||||
@@ -299,13 +316,37 @@ class TaskLlmOpenAI_S2S extends Task {
|
|||||||
/* tool calls */
|
/* tool calls */
|
||||||
else if (type === 'response.output_item.done' && evt.item?.type === 'function_call') {
|
else if (type === 'response.output_item.done' && evt.item?.type === 'function_call') {
|
||||||
this.logger.debug({evt}, 'TaskLlmOpenAI_S2S:_onServerEvent - function_call');
|
this.logger.debug({evt}, 'TaskLlmOpenAI_S2S:_onServerEvent - function_call');
|
||||||
if (!this.toolHook) {
|
const {name, call_id} = evt.item;
|
||||||
|
const args = JSON.parse(evt.item.arguments);
|
||||||
|
|
||||||
|
const mcpTools = this.parent.isMcpEnabled ? await this.parent.mcpService.getAvailableMcpTools() : [];
|
||||||
|
if (mcpTools.some((tool) => tool.name === name)) {
|
||||||
|
this.logger.debug({call_id, name, args}, 'TaskLlmOpenAI_S2S:_onServerEvent - calling mcp tool');
|
||||||
|
try {
|
||||||
|
const res = await this.parent.mcpService.callMcpTool(name, args);
|
||||||
|
this.logger.debug({res}, 'TaskLlmOpenAI_S2S:_onServerEvent - function_call - mcp result');
|
||||||
|
this.processToolOutput(ep, call_id, {
|
||||||
|
type: 'conversation.item.create',
|
||||||
|
item: {
|
||||||
|
type: 'function_call_output',
|
||||||
|
call_id,
|
||||||
|
output: res.content[0]?.text || 'There is no output from the function call',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.info({err, evt}, 'TaskLlmOpenAI_S2S - error calling function');
|
||||||
|
this.results = {
|
||||||
|
completionReason: 'client error calling mcp function',
|
||||||
|
error: err
|
||||||
|
};
|
||||||
|
endConversation = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!this.toolHook) {
|
||||||
this.logger.warn({evt}, 'TaskLlmOpenAI_S2S:_onServerEvent - no toolHook defined!');
|
this.logger.warn({evt}, 'TaskLlmOpenAI_S2S:_onServerEvent - no toolHook defined!');
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const {name, call_id} = evt.item;
|
|
||||||
const args = JSON.parse(evt.item.arguments);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.parent.sendToolHook(call_id, {name, args});
|
await this.parent.sendToolHook(call_id, {name, args});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ class TaskLlmUltravox_S2S extends Task {
|
|||||||
this.eventHook = this.data.eventHook;
|
this.eventHook = this.data.eventHook;
|
||||||
this.toolHook = this.data.toolHook;
|
this.toolHook = this.data.toolHook;
|
||||||
|
|
||||||
|
this.results = {
|
||||||
|
completionReason: 'normal conversation end'
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* only one of these will have items,
|
* only one of these will have items,
|
||||||
* if includeEvents, then these are the events to include
|
* if includeEvents, then these are the events to include
|
||||||
@@ -63,7 +67,50 @@ class TaskLlmUltravox_S2S extends Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a JSON Schema to the dynamic parameters format used in the Ultravox API
|
||||||
|
* @param {Object} jsonSchema - A JSON Schema object defining parameters
|
||||||
|
* @param {string} locationDefault - Default location value for parameters (default: 'PARAMETER_LOCATION_BODY')
|
||||||
|
* @returns {Array} Array of dynamic parameters objects
|
||||||
|
*/
|
||||||
|
transformSchemaToParameters(jsonSchema, locationDefault = 'PARAMETER_LOCATION_BODY') {
|
||||||
|
if (jsonSchema.properties) {
|
||||||
|
const required = jsonSchema.required || [];
|
||||||
|
|
||||||
|
return Object.entries(jsonSchema.properties).map(([name]) => {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
location: locationDefault,
|
||||||
|
required: required.includes(name)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
async createCall() {
|
async createCall() {
|
||||||
|
const mcpTools = this.parent.isMcpEnabled ? await this.parent.mcpService.getAvailableMcpTools() : [];
|
||||||
|
if (mcpTools && mcpTools.length > 0) {
|
||||||
|
const convertedTools = mcpTools.map((tool) => {
|
||||||
|
return {
|
||||||
|
temporaryTool: {
|
||||||
|
modelToolName: tool.name,
|
||||||
|
description: tool.description,
|
||||||
|
dynamicParameters: this.transformSchemaToParameters(tool.inputSchema),
|
||||||
|
// use client tool that ultravox call tool via freeswitch module.
|
||||||
|
client: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// merge with any existing tools
|
||||||
|
this.data.llmOptions.selectedTools = [
|
||||||
|
...convertedTools,
|
||||||
|
...(this.data.llmOptions.selectedTools || [])
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
...this.data.llmOptions,
|
...this.data.llmOptions,
|
||||||
model: this.model,
|
model: this.model,
|
||||||
@@ -86,7 +133,7 @@ class TaskLlmUltravox_S2S extends Task {
|
|||||||
const data = await body.json();
|
const data = await body.json();
|
||||||
if (statusCode !== 201 || !data?.joinUrl) {
|
if (statusCode !== 201 || !data?.joinUrl) {
|
||||||
this.logger.info({statusCode, data}, 'Ultravox Error registering call');
|
this.logger.info({statusCode, data}, 'Ultravox Error registering call');
|
||||||
throw new Error(`Ultravox Error registering call: ${data.message}`);
|
throw new Error(`Ultravox Error registering call:${statusCode} - ${data.detail}`);
|
||||||
}
|
}
|
||||||
this.logger.debug({joinUrl: data.joinUrl}, 'Ultravox Call registered');
|
this.logger.debug({joinUrl: data.joinUrl}, 'Ultravox Call registered');
|
||||||
return data;
|
return data;
|
||||||
@@ -106,12 +153,11 @@ class TaskLlmUltravox_S2S extends Task {
|
|||||||
async _startListening(cs, ep) {
|
async _startListening(cs, ep) {
|
||||||
this._registerHandlers(ep);
|
this._registerHandlers(ep);
|
||||||
|
|
||||||
const data = await this.createCall();
|
|
||||||
const {joinUrl} = data;
|
|
||||||
// split the joinUrl into host and path
|
|
||||||
const {host, pathname, search} = new URL(joinUrl);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const data = await this.createCall();
|
||||||
|
const {joinUrl} = data;
|
||||||
|
// split the joinUrl into host and path
|
||||||
|
const {host, pathname, search} = new URL(joinUrl);
|
||||||
const args = [ep.uuid, 'session.create', host, pathname + search];
|
const args = [ep.uuid, 'session.create', host, pathname + search];
|
||||||
await this._api(ep, args);
|
await this._api(ep, args);
|
||||||
// Notify the application that the session has been created with detail information
|
// Notify the application that the session has been created with detail information
|
||||||
@@ -121,6 +167,7 @@ class TaskLlmUltravox_S2S extends Task {
|
|||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.info({err}, 'TaskLlmUltraVox_S2S:_startListening - Error sending createCall');
|
this.logger.info({err}, 'TaskLlmUltraVox_S2S:_startListening - Error sending createCall');
|
||||||
|
this.results = {completionReason: `connection failure - ${err}`};
|
||||||
this.notifyTaskDone();
|
this.notifyTaskDone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,12 +225,35 @@ class TaskLlmUltravox_S2S extends Task {
|
|||||||
/* tool calls */
|
/* tool calls */
|
||||||
else if (type === 'client_tool_invocation') {
|
else if (type === 'client_tool_invocation') {
|
||||||
this.logger.debug({evt}, 'TaskLlmUltravox_S2S:_onServerEvent - function_call');
|
this.logger.debug({evt}, 'TaskLlmUltravox_S2S:_onServerEvent - function_call');
|
||||||
if (!this.toolHook) {
|
const {toolName: name, invocationId: call_id, parameters: args} = evt;
|
||||||
|
|
||||||
|
const mcpTools = this.parent.isMcpEnabled ? await this.parent.mcpService.getAvailableMcpTools() : [];
|
||||||
|
if (mcpTools.some((tool) => tool.name === name)) {
|
||||||
|
this.logger.debug({
|
||||||
|
name,
|
||||||
|
input: args
|
||||||
|
}, 'TaskLlmUltravox_S2S:_onServerEvent - function_call - mcp tool');
|
||||||
|
try {
|
||||||
|
const res = await this.parent.mcpService.callMcpTool(name, args);
|
||||||
|
this.logger.debug({res}, 'TaskLlmUltravox_S2S:_onServerEvent - function_call - mcp result');
|
||||||
|
this.processToolOutput(_ep, call_id, {
|
||||||
|
type: 'client_tool_result',
|
||||||
|
invocation_id: call_id,
|
||||||
|
result: res.content
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.info({err, evt}, 'TaskLlmUltravox_S2S - error calling mcp tool');
|
||||||
|
this.results = {
|
||||||
|
completionReason: 'client error calling mcp function',
|
||||||
|
error: err
|
||||||
|
};
|
||||||
|
endConversation = true;
|
||||||
|
}
|
||||||
|
} else if (!this.toolHook) {
|
||||||
this.logger.info({evt}, 'TaskLlmUltravox_S2S:_onServerEvent - no toolHook defined!');
|
this.logger.info({evt}, 'TaskLlmUltravox_S2S:_onServerEvent - no toolHook defined!');
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const {toolName: name, invocationId: call_id, parameters: args} = evt;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.parent.sendToolHook(call_id, {name, args});
|
await this.parent.sendToolHook(call_id, {name, args});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class TaskLlmVoiceAgent_S2S extends Task {
|
|||||||
this.parent = parentTask;
|
this.parent = parentTask;
|
||||||
|
|
||||||
this.vendor = this.parent.vendor;
|
this.vendor = this.parent.vendor;
|
||||||
this.model = this.parent.model;
|
this.model = this.parent.model || 'voice-agent';
|
||||||
this.auth = this.parent.auth;
|
this.auth = this.parent.auth;
|
||||||
this.connectionOptions = this.parent.connectOptions;
|
this.connectionOptions = this.parent.connectOptions;
|
||||||
|
|
||||||
@@ -41,25 +41,25 @@ class TaskLlmVoiceAgent_S2S extends Task {
|
|||||||
this.actionHook = this.data.actionHook;
|
this.actionHook = this.data.actionHook;
|
||||||
this.eventHook = this.data.eventHook;
|
this.eventHook = this.data.eventHook;
|
||||||
this.toolHook = this.data.toolHook;
|
this.toolHook = this.data.toolHook;
|
||||||
const {settingsConfiguration} = this.data.llmOptions;
|
const {Settings} = this.data.llmOptions;
|
||||||
|
|
||||||
if (typeof settingsConfiguration !== 'object') {
|
if (typeof Settings !== 'object') {
|
||||||
throw new Error('llmOptions with an initial settingsConfiguration is required for VoiceAgent S2S');
|
throw new Error('llmOptions with an initial Settings is required for VoiceAgent S2S');
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
const {audio, ...rest} = settingsConfiguration;
|
const {audio, ...rest} = Settings;
|
||||||
const cfg = this.settingsConfiguration = rest;
|
const cfg = this.Settings = rest;
|
||||||
|
|
||||||
if (!cfg.agent) throw new Error('llmOptions.settingsConfiguration.agent is required for VoiceAgent S2S');
|
if (!cfg.agent) throw new Error('llmOptions.Settings.agent is required for VoiceAgent S2S');
|
||||||
if (!cfg.agent.think) {
|
if (!cfg.agent.think) {
|
||||||
throw new Error('llmOptions.settingsConfiguration.agent.think is required for VoiceAgent S2S');
|
throw new Error('llmOptions.Settings.agent.think is required for VoiceAgent S2S');
|
||||||
}
|
}
|
||||||
if (!cfg.agent.think.model) {
|
if (!cfg.agent.think.provider?.model) {
|
||||||
throw new Error('llmOptions.settingsConfiguration.agent.think.model is required for VoiceAgent S2S');
|
throw new Error('llmOptions.Settings.agent.think.provider.model is required for VoiceAgent S2S');
|
||||||
}
|
}
|
||||||
if (!cfg.agent.think.provider?.type) {
|
if (!cfg.agent.think.provider?.type) {
|
||||||
throw new Error('llmOptions.settingsConfiguration.agent.think.provider.type is required for VoiceAgent S2S');
|
throw new Error('llmOptions.Settings.agent.think.provider.type is required for VoiceAgent S2S');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.results = {
|
this.results = {
|
||||||
@@ -92,7 +92,7 @@ class TaskLlmVoiceAgent_S2S extends Task {
|
|||||||
const {path} = this.connectionOptions || {};
|
const {path} = this.connectionOptions || {};
|
||||||
if (path) return path;
|
if (path) return path;
|
||||||
|
|
||||||
return '/agent';
|
return '/v1/agent/converse';
|
||||||
}
|
}
|
||||||
|
|
||||||
async _api(ep, args) {
|
async _api(ep, args) {
|
||||||
@@ -193,7 +193,20 @@ class TaskLlmVoiceAgent_S2S extends Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async _sendInitialMessage(ep) {
|
async _sendInitialMessage(ep) {
|
||||||
if (!await this._sendClientEvent(ep, this.settingsConfiguration)) {
|
const mcpTools = this.parent.isMcpEnabled ? await this.parent.mcpService.getAvailableMcpTools() : [];
|
||||||
|
if (mcpTools && mcpTools.length > 0 && this.Settings.agent?.think) {
|
||||||
|
const convertedTools = mcpTools.map((tool) => ({
|
||||||
|
name: tool.name,
|
||||||
|
description: tool.description,
|
||||||
|
parameters: tool.inputSchema
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.Settings.agent.think.functions = [
|
||||||
|
...convertedTools,
|
||||||
|
...(this.Settings.agent.think?.functions || [])
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (!await this._sendClientEvent(ep, this.Settings)) {
|
||||||
this.notifyTaskDone();
|
this.notifyTaskDone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -254,17 +267,43 @@ class TaskLlmVoiceAgent_S2S extends Task {
|
|||||||
/* tool calls */
|
/* tool calls */
|
||||||
else if (type === 'FunctionCallRequest') {
|
else if (type === 'FunctionCallRequest') {
|
||||||
this.logger.debug({evt}, 'TaskLlmVoiceAgent_S2S:_onServerEvent - function_call');
|
this.logger.debug({evt}, 'TaskLlmVoiceAgent_S2S:_onServerEvent - function_call');
|
||||||
if (!this.toolHook) {
|
|
||||||
|
const mcpTools = this.parent.isMcpEnabled ? await this.parent.mcpService.getAvailableMcpTools() : [];
|
||||||
|
if (!this.toolHook && mcpTools.length === 0) {
|
||||||
this.logger.warn({evt}, 'TaskLlmVoiceAgent_S2S:_onServerEvent - no toolHook defined!');
|
this.logger.warn({evt}, 'TaskLlmVoiceAgent_S2S:_onServerEvent - no toolHook defined!');
|
||||||
}
|
} else {
|
||||||
else {
|
const {functions} = evt;
|
||||||
const {function_name:name, function_call_id:call_id} = evt;
|
const handledFunctions = [];
|
||||||
const args = evt.input;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.parent.sendToolHook(call_id, {name, args});
|
if (mcpTools && mcpTools.length > 0) {
|
||||||
|
for (const func of functions) {
|
||||||
|
const {name, arguments: args, id} = func;
|
||||||
|
const tool = mcpTools.find((tool) => tool.name === name);
|
||||||
|
if (tool) {
|
||||||
|
handledFunctions.push(name);
|
||||||
|
const response = await this.parent.mcpService.callMcpTool(name, JSON.parse(args));
|
||||||
|
this.logger.debug({response}, 'TaskLlmVoiceAgent_S2S:_onServerEvent - function_call - mcp result');
|
||||||
|
this.processToolOutput(_ep, id, {
|
||||||
|
data: {
|
||||||
|
type: 'FunctionCallResponse',
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
content: response.length > 0 ? response[0].text : 'There is no output from the function call'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const func of functions) {
|
||||||
|
const {name, arguments: args, id} = func;
|
||||||
|
if (!handledFunctions.includes(name)) {
|
||||||
|
await this.parent.sendToolHook(id, {name, args: JSON.parse(args)});
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.info({err, evt}, 'TaskLlmVoiceAgent - error calling function');
|
this.logger.info({err, evt}, 'TaskLlmVoiceAgent_S2S:_onServerEvent - error calling function');
|
||||||
this.results = {
|
this.results = {
|
||||||
completionReason: 'client error calling function',
|
completionReason: 'client error calling function',
|
||||||
error: err
|
error: err
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
const Task = require('./task');
|
const Task = require('./task');
|
||||||
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
||||||
const { PlayFileNotFoundError } = require('../utils/error');
|
const { PlayFileNotFoundError } = require('../utils/error');
|
||||||
|
|
||||||
class TaskPlay extends Task {
|
class TaskPlay extends Task {
|
||||||
constructor(logger, opts) {
|
constructor(logger, opts) {
|
||||||
super(logger, opts);
|
super(logger, opts);
|
||||||
@@ -27,6 +26,7 @@ class TaskPlay extends Task {
|
|||||||
let playbackSeconds = 0;
|
let playbackSeconds = 0;
|
||||||
let playbackMilliseconds = 0;
|
let playbackMilliseconds = 0;
|
||||||
let completed = !(this.timeoutSecs > 0 || this.loop);
|
let completed = !(this.timeoutSecs > 0 || this.loop);
|
||||||
|
cs.playingAudio = true;
|
||||||
if (this.timeoutSecs > 0) {
|
if (this.timeoutSecs > 0) {
|
||||||
timeout = setTimeout(async() => {
|
timeout = setTimeout(async() => {
|
||||||
completed = true;
|
completed = true;
|
||||||
@@ -40,6 +40,22 @@ class TaskPlay extends Task {
|
|||||||
try {
|
try {
|
||||||
this.notifyStatus({event: 'start-playback'});
|
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) {
|
||||||
|
/* Listen for playback-start event and set up a one-time listener for uuid_break
|
||||||
|
* that will kill the audio playback if the taskIds match. This ensures that
|
||||||
|
* we only kill the currently playing audio and not audio from other tasks.
|
||||||
|
* As we are using stickyEventEmitter, even if the event is emitted before the listener is registered,
|
||||||
|
* the listener will receive the most recent event.
|
||||||
|
*/
|
||||||
|
ep.once('playback-start', (evt) => {
|
||||||
|
this.logger.debug({evt}, 'Play got playback-start');
|
||||||
|
this.cs.stickyEventEmitter.once('uuid_break', (t) => {
|
||||||
|
if (t?.taskId === this.taskId) {
|
||||||
|
this.logger.debug(`Play got kill-playback, executing uuid_break, taskId: ${t?.taskId}`);
|
||||||
|
this.ep.api('uuid_break', this.ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
|
||||||
|
this.notifyStatus({event: 'kill-playback'});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
if (cs.isInConference) {
|
if (cs.isInConference) {
|
||||||
const {memberId, confName, confUuid} = cs;
|
const {memberId, confName, confUuid} = cs;
|
||||||
if (Array.isArray(this.url)) {
|
if (Array.isArray(this.url)) {
|
||||||
@@ -87,15 +103,15 @@ class TaskPlay extends Task {
|
|||||||
|
|
||||||
async kill(cs) {
|
async kill(cs) {
|
||||||
super.kill(cs);
|
super.kill(cs);
|
||||||
if (this.ep.connected && !this.playComplete) {
|
if (this.ep?.connected && !this.playComplete) {
|
||||||
this.logger.debug('TaskPlay:kill - killing audio');
|
this.logger.debug('TaskPlay:kill - killing audio');
|
||||||
if (cs.isInConference) {
|
if (cs.isInConference) {
|
||||||
const {memberId, confName} = cs;
|
const {memberId, confName} = cs;
|
||||||
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).catch((err) => this.logger.info(err, 'Error killing audio'));
|
||||||
this.ep.api('uuid_break', this.ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
|
cs.stickyEventEmitter.emit('uuid_break', this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -223,7 +223,19 @@ class TaskSay extends TtsTask {
|
|||||||
});
|
});
|
||||||
ep.once('playback-stop', (evt) => {
|
ep.once('playback-stop', (evt) => {
|
||||||
this.logger.debug({evt}, 'Say got playback-stop');
|
this.logger.debug({evt}, 'Say got playback-stop');
|
||||||
if (evt.variable_tts_error) {
|
this.notifyStatus({event: 'stop-playback'});
|
||||||
|
this.notifiedPlayBackStop = true;
|
||||||
|
const tts_error = evt.variable_tts_error;
|
||||||
|
let response_code = 200;
|
||||||
|
// Check if any property ends with _response_code
|
||||||
|
for (const [key, value] of Object.entries(evt)) {
|
||||||
|
if (key.endsWith('_response_code')) {
|
||||||
|
response_code = parseInt(value, 10) || 200;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tts_error) {
|
||||||
writeAlerts({
|
writeAlerts({
|
||||||
account_sid,
|
account_sid,
|
||||||
alert_type: AlertType.TTS_FAILURE,
|
alert_type: AlertType.TTS_FAILURE,
|
||||||
@@ -232,7 +244,7 @@ class TaskSay extends TtsTask {
|
|||||||
target_sid
|
target_sid
|
||||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for no tts'));
|
}).catch((err) => this.logger.info({err}, 'Error generating alert for no tts'));
|
||||||
}
|
}
|
||||||
if (evt.variable_tts_cache_filename && !this.killed) {
|
if (!tts_error && response_code < 300 && evt.variable_tts_cache_filename && !this.killed) {
|
||||||
const text = parseTextFromSayString(this.text[segment]);
|
const text = parseTextFromSayString(this.text[segment]);
|
||||||
addFileToCache(evt.variable_tts_cache_filename, {
|
addFileToCache(evt.variable_tts_cache_filename, {
|
||||||
account_sid,
|
account_sid,
|
||||||
@@ -241,12 +253,14 @@ class TaskSay extends TtsTask {
|
|||||||
voice,
|
voice,
|
||||||
engine,
|
engine,
|
||||||
model: this.model || this.model_id,
|
model: this.model || this.model_id,
|
||||||
text
|
text,
|
||||||
|
instructions: this.instructions
|
||||||
}).catch((err) => this.logger.info({err}, 'Error adding file to cache'));
|
}).catch((err) => this.logger.info({err}, 'Error adding file to cache'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._playResolve) {
|
if (this._playResolve) {
|
||||||
evt.variable_tts_error ? this._playReject(new Error(evt.variable_tts_error)) : this._playResolve();
|
(tts_error || response_code >= 300) ? this._playReject(new Error(evt.variable_tts_error)) :
|
||||||
|
this._playResolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// wait for playback-stop event received to confirm if the playback is successful
|
// wait for playback-stop event received to confirm if the playback is successful
|
||||||
@@ -296,6 +310,9 @@ class TaskSay extends TtsTask {
|
|||||||
this.logger.debug('TaskSay:kill - clearing TTS stream for streaming audio');
|
this.logger.debug('TaskSay:kill - clearing TTS stream for streaming audio');
|
||||||
cs.clearTtsStream();
|
cs.clearTtsStream();
|
||||||
} else {
|
} else {
|
||||||
|
if (!this.notifiedPlayBackStop) {
|
||||||
|
this.notifyStatus({event: 'stop-playback'});
|
||||||
|
}
|
||||||
this.notifyStatus({event: 'kill-playback'});
|
this.notifyStatus({event: 'kill-playback'});
|
||||||
this.ep.api('uuid_break', this.ep.uuid);
|
this.ep.api('uuid_break', this.ep.uuid);
|
||||||
}
|
}
|
||||||
@@ -316,6 +333,7 @@ class TaskSay extends TtsTask {
|
|||||||
if (key.startsWith('variable_tts_')) {
|
if (key.startsWith('variable_tts_')) {
|
||||||
let newKey = key.substring('variable_tts_'.length)
|
let newKey = key.substring('variable_tts_'.length)
|
||||||
.replace('whisper_', 'whisper.')
|
.replace('whisper_', 'whisper.')
|
||||||
|
.replace('nvidia_', 'nvidia.')
|
||||||
.replace('deepgram_', 'deepgram.')
|
.replace('deepgram_', 'deepgram.')
|
||||||
.replace('playht_', 'playht.')
|
.replace('playht_', 'playht.')
|
||||||
.replace('cartesia_', 'cartesia.')
|
.replace('cartesia_', 'cartesia.')
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ class TaskSipDecline extends Task {
|
|||||||
super.exec(cs);
|
super.exec(cs);
|
||||||
res.send(this.data.status, this.data.reason, {
|
res.send(this.data.status, this.data.reason, {
|
||||||
headers: this.headers
|
headers: this.headers
|
||||||
|
}, (err) => {
|
||||||
|
if (!err) {
|
||||||
|
// Call was successfully declined
|
||||||
|
cs._callReleased();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
cs.emit('callStatusChange', {
|
cs.emit('callStatusChange', {
|
||||||
callStatus: CallStatus.Failed,
|
callStatus: CallStatus.Failed,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class Task extends Emitter {
|
|||||||
this.data = data;
|
this.data = data;
|
||||||
this.actionHook = this.data.actionHook;
|
this.actionHook = this.data.actionHook;
|
||||||
this.id = data.id;
|
this.id = data.id;
|
||||||
|
this.taskId = uuidv4();
|
||||||
|
|
||||||
this._killInProgress = false;
|
this._killInProgress = false;
|
||||||
this._completionPromise = new Promise((resolve) => this._completionResolver = resolve);
|
this._completionPromise = new Promise((resolve) => this._completionResolver = resolve);
|
||||||
|
|||||||
@@ -653,12 +653,21 @@ class TaskTranscribe extends SttTask {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_onMaxDurationExceeded(cs, ep, channel) {
|
_onMaxDurationExceeded(cs, ep, channel) {
|
||||||
this.logger.debug(`TaskTranscribe:_onMaxDurationExceeded on channel ${channel}`);
|
this.restartDueToError(ep, channel, 'Max duration exceeded');
|
||||||
|
}
|
||||||
|
|
||||||
|
_onMaxBufferExceeded(cs, ep, channel) {
|
||||||
|
this.restartDueToError(ep, channel, 'Max buffer exceeded');
|
||||||
|
}
|
||||||
|
|
||||||
|
restartDueToError(ep, channel, reason) {
|
||||||
|
this.logger.debug(`TaskTranscribe:${reason} on channel ${channel}`);
|
||||||
if (this.paused) return;
|
if (this.paused) return;
|
||||||
|
|
||||||
if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) {
|
if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) {
|
||||||
this.childSpan[channel - 1].span.setAttributes({
|
this.childSpan[channel - 1].span.setAttributes({
|
||||||
channel,
|
channel,
|
||||||
'stt.resolve': 'max duration exceeded',
|
'stt.resolve': reason,
|
||||||
'stt.label': this.label || 'None',
|
'stt.label': this.label || 'None',
|
||||||
});
|
});
|
||||||
this.childSpan[channel - 1].span.end();
|
this.childSpan[channel - 1].span.end();
|
||||||
@@ -715,6 +724,14 @@ class TaskTranscribe extends SttTask {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.logger.info({evt}, 'TaskTranscribe:_onJambonzError');
|
this.logger.info({evt}, 'TaskTranscribe:_onJambonzError');
|
||||||
|
if (this.vendor === 'microsoft' &&
|
||||||
|
evt.error?.includes('Due to service inactivity, the client buffer exceeded maximum size. Resetting the buffer')) {
|
||||||
|
let channel = 1;
|
||||||
|
if (this.ep !== _ep) {
|
||||||
|
channel = 2;
|
||||||
|
}
|
||||||
|
return this._onMaxBufferExceeded(cs, _ep, channel);
|
||||||
|
}
|
||||||
if (this.paused) return;
|
if (this.paused) return;
|
||||||
const {writeAlerts, AlertType} = cs.srf.locals;
|
const {writeAlerts, AlertType} = cs.srf.locals;
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class TtsTask extends Task {
|
|||||||
this.synthesizer = this.data.synthesizer || {};
|
this.synthesizer = this.data.synthesizer || {};
|
||||||
this.disableTtsCache = this.data.disableTtsCache;
|
this.disableTtsCache = this.data.disableTtsCache;
|
||||||
this.options = this.synthesizer.options || {};
|
this.options = this.synthesizer.options || {};
|
||||||
|
this.instructions = this.data.instructions;
|
||||||
}
|
}
|
||||||
|
|
||||||
async exec(cs) {
|
async exec(cs) {
|
||||||
@@ -262,6 +263,7 @@ class TtsTask extends Task {
|
|||||||
const {filePath, servedFromCache, rtt} = await synthAudio(stats, {
|
const {filePath, servedFromCache, rtt} = await synthAudio(stats, {
|
||||||
account_sid,
|
account_sid,
|
||||||
text,
|
text,
|
||||||
|
instructions: this.instructions,
|
||||||
vendor,
|
vendor,
|
||||||
language,
|
language,
|
||||||
voice,
|
voice,
|
||||||
|
|||||||
@@ -194,6 +194,13 @@
|
|||||||
"Disconnect": "openai_s2s::disconnect",
|
"Disconnect": "openai_s2s::disconnect",
|
||||||
"ServerEvent": "openai_s2s::server_event"
|
"ServerEvent": "openai_s2s::server_event"
|
||||||
},
|
},
|
||||||
|
"LlmEvents_Google": {
|
||||||
|
"Error": "error",
|
||||||
|
"Connect": "google_s2s::connect",
|
||||||
|
"ConnectFailure": "google_s2s::connect_failed",
|
||||||
|
"Disconnect": "google_s2s::disconnect",
|
||||||
|
"ServerEvent": "google_s2s::server_event"
|
||||||
|
},
|
||||||
"LlmEvents_Elevenlabs": {
|
"LlmEvents_Elevenlabs": {
|
||||||
"Error": "error",
|
"Error": "error",
|
||||||
"Connect": "elevenlabs_s2s::connect",
|
"Connect": "elevenlabs_s2s::connect",
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ class HttpRequestor extends BaseRequestor {
|
|||||||
|
|
||||||
assert(HookMsgTypes.includes(type));
|
assert(HookMsgTypes.includes(type));
|
||||||
|
|
||||||
const payload = params ? snakeCaseKeys(params, ['customerData', 'sip']) : null;
|
const payload = params ? snakeCaseKeys(params, ['customerData', 'sip', 'env_vars', 'args']) : null;
|
||||||
const url = hook.url || hook;
|
const url = hook.url || hook;
|
||||||
const method = hook.method || 'POST';
|
const method = hook.method || 'POST';
|
||||||
let buf = '';
|
let buf = '';
|
||||||
@@ -219,7 +219,7 @@ class HttpRequestor extends BaseRequestor {
|
|||||||
const rtt = this._roundTrip(startAt);
|
const rtt = this._roundTrip(startAt);
|
||||||
if (buf) this.stats.histogram('app.hook.response_time', rtt, ['hook_type:app']);
|
if (buf) this.stats.histogram('app.hook.response_time', rtt, ['hook_type:app']);
|
||||||
|
|
||||||
if (buf && Array.isArray(buf)) {
|
if (buf && (Array.isArray(buf) || type == 'llm:tool-call')) {
|
||||||
this.logger.info({response: buf}, `HttpRequestor:request ${method} ${url} succeeded in ${rtt}ms`);
|
this.logger.info({response: buf}, `HttpRequestor:request ${method} ${url} succeeded in ${rtt}ms`);
|
||||||
return buf;
|
return buf;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,18 +31,26 @@ function getLocalIp() {
|
|||||||
return '127.0.0.1'; // Fallback to localhost if no suitable interface found
|
return '127.0.0.1'; // Fallback to localhost if no suitable interface found
|
||||||
}
|
}
|
||||||
|
|
||||||
function initMS(logger, wrapper, ms) {
|
function initMS(logger, wrapper, ms, {
|
||||||
|
onFreeswitchConnect,
|
||||||
|
onFreeswitchDisconnect
|
||||||
|
}) {
|
||||||
Object.assign(wrapper, {ms, active: true, connects: 1});
|
Object.assign(wrapper, {ms, active: true, connects: 1});
|
||||||
logger.info(`connected to freeswitch at ${ms.address}`);
|
logger.info(`connected to freeswitch at ${ms.address}`);
|
||||||
|
|
||||||
|
onFreeswitchConnect(wrapper);
|
||||||
|
|
||||||
ms.conn
|
ms.conn
|
||||||
.on('esl::end', () => {
|
.on('esl::end', () => {
|
||||||
wrapper.active = false;
|
wrapper.active = false;
|
||||||
|
wrapper.connects = 0;
|
||||||
logger.info(`lost connection to freeswitch at ${ms.address}`);
|
logger.info(`lost connection to freeswitch at ${ms.address}`);
|
||||||
|
onFreeswitchDisconnect(wrapper);
|
||||||
|
ms.removeAllListeners();
|
||||||
})
|
})
|
||||||
.on('esl::ready', () => {
|
.on('esl::ready', () => {
|
||||||
if (wrapper.connects > 0) {
|
if (wrapper.connects > 0) {
|
||||||
logger.info(`connected to freeswitch at ${ms.address}`);
|
logger.info(`esl::ready connected to freeswitch at ${ms.address}`);
|
||||||
}
|
}
|
||||||
wrapper.connects = 1;
|
wrapper.connects = 1;
|
||||||
wrapper.active = true;
|
wrapper.active = true;
|
||||||
@@ -56,7 +64,10 @@ function initMS(logger, wrapper, ms) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function installSrfLocals(srf, logger) {
|
function installSrfLocals(srf, logger, {
|
||||||
|
onFreeswitchConnect = () => {},
|
||||||
|
onFreeswitchDisconnect = () => {}
|
||||||
|
}) {
|
||||||
logger.debug('installing srf locals');
|
logger.debug('installing srf locals');
|
||||||
assert(!srf.locals.dbHelpers);
|
assert(!srf.locals.dbHelpers);
|
||||||
const {tracer} = srf.locals.otel;
|
const {tracer} = srf.locals.otel;
|
||||||
@@ -91,7 +102,10 @@ function installSrfLocals(srf, logger) {
|
|||||||
mediaservers.push(val);
|
mediaservers.push(val);
|
||||||
try {
|
try {
|
||||||
const ms = await mrf.connect(fs);
|
const ms = await mrf.connect(fs);
|
||||||
initMS(logger, val, ms);
|
initMS(logger, val, ms, {
|
||||||
|
onFreeswitchConnect,
|
||||||
|
onFreeswitchDisconnect
|
||||||
|
});
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
logger.info({err}, `failed connecting to freeswitch at ${fs.address}, will retry shortly: ${err.message}`);
|
logger.info({err}, `failed connecting to freeswitch at ${fs.address}, will retry shortly: ${err.message}`);
|
||||||
@@ -102,9 +116,15 @@ function installSrfLocals(srf, logger) {
|
|||||||
for (const val of mediaservers) {
|
for (const val of mediaservers) {
|
||||||
if (val.connects === 0) {
|
if (val.connects === 0) {
|
||||||
try {
|
try {
|
||||||
|
// make sure all listeners are removed before reconnecting
|
||||||
|
val.ms?.disconnect();
|
||||||
|
val.ms = null;
|
||||||
logger.info({mediaserver: val.opts}, 'Retrying initial connection to media server');
|
logger.info({mediaserver: val.opts}, 'Retrying initial connection to media server');
|
||||||
const ms = await mrf.connect(val.opts);
|
const ms = await mrf.connect(val.opts);
|
||||||
initMS(logger, val, ms);
|
initMS(logger, val, ms, {
|
||||||
|
onFreeswitchConnect,
|
||||||
|
onFreeswitchDisconnect
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.info({err}, `failed connecting to freeswitch at ${val.opts.address}, will retry shortly`);
|
logger.info({err}, `failed connecting to freeswitch at ${val.opts.address}, will retry shortly`);
|
||||||
}
|
}
|
||||||
|
|||||||
103
lib/utils/llm-mcp.js
Normal file
103
lib/utils/llm-mcp.js
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
const { Client } = require('@modelcontextprotocol/sdk/client/index.js');
|
||||||
|
|
||||||
|
class LlmMcpService {
|
||||||
|
|
||||||
|
constructor(logger, mcpServers) {
|
||||||
|
this.logger = logger;
|
||||||
|
this.mcpServers = mcpServers || [];
|
||||||
|
this.mcpClients = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure we call init() before using any of the mcp clients
|
||||||
|
// this is to ensure that we have a valid connection to the MCP server
|
||||||
|
// and that we have collected the available tools.
|
||||||
|
async init() {
|
||||||
|
if (this.mcpClients.length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { SSEClientTransport } = await import('@modelcontextprotocol/sdk/client/sse.js');
|
||||||
|
for (const server of this.mcpServers) {
|
||||||
|
const { url } = server;
|
||||||
|
if (url) {
|
||||||
|
try {
|
||||||
|
const transport = new SSEClientTransport(new URL(url), {});
|
||||||
|
const client = new Client({ name: 'Jambonz MCP Client', version: '1.0.0' });
|
||||||
|
await client.connect(transport);
|
||||||
|
// collect available tools
|
||||||
|
const { tools } = await client.listTools();
|
||||||
|
this.mcpClients.push({
|
||||||
|
url,
|
||||||
|
client,
|
||||||
|
tools
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(`LlmMcpService: Failed to connect to MCP server at ${url}: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAvailableMcpTools() {
|
||||||
|
// returns a list of available tools from all MCP clients
|
||||||
|
const tools = [];
|
||||||
|
for (const mcpClient of this.mcpClients) {
|
||||||
|
const {tools: availableTools} = mcpClient;
|
||||||
|
if (availableTools) {
|
||||||
|
tools.push(...availableTools);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tools;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMcpClientByToolName(name) {
|
||||||
|
for (const mcpClient of this.mcpClients) {
|
||||||
|
const { tools } = mcpClient;
|
||||||
|
if (tools && tools.some((tool) => tool.name === name)) {
|
||||||
|
return mcpClient.client;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMcpClientByToolId(id) {
|
||||||
|
for (const mcpClient of this.mcpClients) {
|
||||||
|
const { tools } = mcpClient;
|
||||||
|
if (tools && tools.some((tool) => tool.id === id)) {
|
||||||
|
return mcpClient.client;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async callMcpTool(name, input) {
|
||||||
|
const client = await this.getMcpClientByToolName(name);
|
||||||
|
if (client) {
|
||||||
|
try {
|
||||||
|
const result = await client.callTool({
|
||||||
|
name,
|
||||||
|
arguments: input,
|
||||||
|
});
|
||||||
|
this.logger.debug({result}, 'LlmMcpService - result');
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error({err}, 'LlmMcpService - error calling tool');
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async close() {
|
||||||
|
for (const mcpClient of this.mcpClients) {
|
||||||
|
const { client } = mcpClient;
|
||||||
|
if (client) {
|
||||||
|
await client.close();
|
||||||
|
this.logger.debug({url: mcpClient.url}, 'LlmMcpService - mcp client closed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.mcpClients = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = LlmMcpService;
|
||||||
|
|
||||||
@@ -279,7 +279,12 @@ class SingleDialer extends Emitter {
|
|||||||
this.logger.info('dial is onhold, emit event');
|
this.logger.info('dial is onhold, emit event');
|
||||||
this.emit('reinvite', req, res);
|
this.emit('reinvite', req, res);
|
||||||
} else {
|
} else {
|
||||||
const newSdp = await this.ep.modify(req.body);
|
let newSdp = await this.ep.modify(req.body);
|
||||||
|
// in case of reINVITE if video call is enabled in FS and the call is not a video call,
|
||||||
|
// remove video media from the SDP
|
||||||
|
if (process.env.JAMBONES_VIDEO_CALLS_ENABLED_IN_FS && !this.opts?.isVideoCall) {
|
||||||
|
newSdp = removeVideoSdp(newSdp);
|
||||||
|
}
|
||||||
res.send(200, {body: newSdp});
|
res.send(200, {body: newSdp});
|
||||||
this.logger.info({offer: req.body, answer: newSdp}, 'SingleDialer:exec: handling reINVITE');
|
this.logger.info({offer: req.body, answer: newSdp}, 'SingleDialer:exec: handling reINVITE');
|
||||||
}
|
}
|
||||||
@@ -565,7 +570,8 @@ function placeOutdial({
|
|||||||
}) {
|
}) {
|
||||||
const myOpts = deepcopy(opts);
|
const myOpts = deepcopy(opts);
|
||||||
const sd = new SingleDialer({
|
const sd = new SingleDialer({
|
||||||
logger, sbcAddress, target, myOpts, application, callInfo, accountInfo, rootSpan, startSpan, dialTask, onHoldMusic
|
logger, sbcAddress, target, opts: myOpts, application, callInfo,
|
||||||
|
accountInfo, rootSpan, startSpan, dialTask, onHoldMusic
|
||||||
});
|
});
|
||||||
sd.exec(srf, ms, myOpts);
|
sd.exec(srf, ms, myOpts);
|
||||||
return sd;
|
return sd;
|
||||||
|
|||||||
70
lib/utils/sticky-event-emitter.js
Normal file
70
lib/utils/sticky-event-emitter.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
const EventEmitter = require('events');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A specialized EventEmitter that caches the most recent event emissions.
|
||||||
|
* When new listeners are added, they immediately receive the most recent
|
||||||
|
* event if it was previously emitted. This is useful for handling state
|
||||||
|
* changes where late subscribers need to know the current state.
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Caches the most recent emission for each event type
|
||||||
|
* - New listeners immediately receive the cached event if available
|
||||||
|
* - Supports both regular (on) and one-time (once) listeners
|
||||||
|
* - Maintains compatibility with Node's EventEmitter interface
|
||||||
|
*/
|
||||||
|
class StickyEventEmitter extends EventEmitter {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this._eventCache = new Map();
|
||||||
|
this._onceListeners = new Map(); // For storing once listeners if needed
|
||||||
|
}
|
||||||
|
destroy() {
|
||||||
|
this._eventCache.clear();
|
||||||
|
this._onceListeners.clear();
|
||||||
|
this.removeAllListeners();
|
||||||
|
}
|
||||||
|
emit(event, ...args) {
|
||||||
|
// Store the event and its args
|
||||||
|
this._eventCache.set(event, args);
|
||||||
|
|
||||||
|
// If there are any 'once' listeners waiting, call them
|
||||||
|
if (this._onceListeners.has(event)) {
|
||||||
|
const listeners = this._onceListeners.get(event);
|
||||||
|
for (const listener of listeners) {
|
||||||
|
listener(...args);
|
||||||
|
}
|
||||||
|
if (this.onSuccess) {
|
||||||
|
this.onSuccess();
|
||||||
|
}
|
||||||
|
this._onceListeners.delete(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.emit(event, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
on(event, listener) {
|
||||||
|
if (this._eventCache.has(event)) {
|
||||||
|
listener(...this._eventCache.get(event));
|
||||||
|
}
|
||||||
|
return super.on(event, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
once(event, listener) {
|
||||||
|
if (this._eventCache.has(event)) {
|
||||||
|
listener(...this._eventCache.get(event));
|
||||||
|
if (this.onSuccess) {
|
||||||
|
this.onSuccess();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Store listener in case emit comes before
|
||||||
|
if (!this._onceListeners.has(event)) {
|
||||||
|
this._onceListeners.set(event, []);
|
||||||
|
}
|
||||||
|
this._onceListeners.get(event).push(listener);
|
||||||
|
super.once(event, listener); // Also attach to native once
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = StickyEventEmitter;
|
||||||
@@ -8,7 +8,7 @@ const {
|
|||||||
const MAX_CHUNK_SIZE = 1800;
|
const MAX_CHUNK_SIZE = 1800;
|
||||||
const HIGH_WATER_BUFFER_SIZE = 1000;
|
const HIGH_WATER_BUFFER_SIZE = 1000;
|
||||||
const LOW_WATER_BUFFER_SIZE = 200;
|
const LOW_WATER_BUFFER_SIZE = 200;
|
||||||
const TIMEOUT_RETRY_MSECS = 3000;
|
const TIMEOUT_RETRY_MSECS = 1000; // 1 second
|
||||||
|
|
||||||
|
|
||||||
const isWhitespace = (str) => /^\s*$/.test(str);
|
const isWhitespace = (str) => /^\s*$/.test(str);
|
||||||
@@ -377,6 +377,7 @@ class TtsStreamingBuffer extends Emitter {
|
|||||||
|
|
||||||
_onTimeout() {
|
_onTimeout() {
|
||||||
this.logger.debug('TtsStreamingBuffer:_onTimeout Timeout waiting for sentence boundary');
|
this.logger.debug('TtsStreamingBuffer:_onTimeout Timeout waiting for sentence boundary');
|
||||||
|
this.timer = null;
|
||||||
// Check if new text has been added since the timer was set.
|
// Check if new text has been added since the timer was set.
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - this.lastUpdateTime < TIMEOUT_RETRY_MSECS) {
|
if (now - this.lastUpdateTime < TIMEOUT_RETRY_MSECS) {
|
||||||
@@ -384,7 +385,6 @@ class TtsStreamingBuffer extends Emitter {
|
|||||||
this._setTimerIfNeeded();
|
this._setTimerIfNeeded();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.timer = null;
|
|
||||||
this._feedQueue(true);
|
this._feedQueue(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ class WsRequestor extends BaseRequestor {
|
|||||||
assert(this.ws);
|
assert(this.ws);
|
||||||
|
|
||||||
/* prepare and send message */
|
/* prepare and send message */
|
||||||
let payload = params ? snakeCaseKeys(params, ['customerData', 'sip']) : null;
|
let payload = params ? snakeCaseKeys(params, ['customerData', 'sip', 'env_vars', 'args']) : null;
|
||||||
if (type === 'session:new' || type === 'session:adulting') this._sessionData = payload;
|
if (type === 'session:new' || type === 'session:adulting') this._sessionData = payload;
|
||||||
if (type === 'session:reconnect') payload = this._sessionData;
|
if (type === 'session:reconnect') payload = this._sessionData;
|
||||||
assert.ok(url, 'WsRequestor:request url was not provided');
|
assert.ok(url, 'WsRequestor:request url was not provided');
|
||||||
|
|||||||
768
package-lock.json
generated
768
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -27,14 +27,15 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-auto-scaling": "^3.549.0",
|
"@aws-sdk/client-auto-scaling": "^3.549.0",
|
||||||
"@aws-sdk/client-sns": "^3.549.0",
|
"@aws-sdk/client-sns": "^3.549.0",
|
||||||
"@jambonz/db-helpers": "^0.9.11",
|
"@jambonz/db-helpers": "^0.9.12",
|
||||||
"@jambonz/http-health-check": "^0.0.1",
|
"@jambonz/http-health-check": "^0.0.1",
|
||||||
"@jambonz/mw-registrar": "^0.2.7",
|
"@jambonz/mw-registrar": "^0.2.7",
|
||||||
"@jambonz/realtimedb-helpers": "^0.8.13",
|
"@jambonz/realtimedb-helpers": "^0.8.13",
|
||||||
"@jambonz/speech-utils": "^0.2.3",
|
"@jambonz/speech-utils": "^0.2.10",
|
||||||
"@jambonz/stats-collector": "^0.1.10",
|
"@jambonz/stats-collector": "^0.1.10",
|
||||||
"@jambonz/time-series": "^0.2.13",
|
"@jambonz/time-series": "^0.2.13",
|
||||||
"@jambonz/verb-specifications": "^0.0.102",
|
"@jambonz/verb-specifications": "^0.0.104",
|
||||||
|
"@modelcontextprotocol/sdk": "^1.9.0",
|
||||||
"@opentelemetry/api": "^1.8.0",
|
"@opentelemetry/api": "^1.8.0",
|
||||||
"@opentelemetry/exporter-jaeger": "^1.23.0",
|
"@opentelemetry/exporter-jaeger": "^1.23.0",
|
||||||
"@opentelemetry/exporter-trace-otlp-http": "^0.50.0",
|
"@opentelemetry/exporter-trace-otlp-http": "^0.50.0",
|
||||||
@@ -48,7 +49,7 @@
|
|||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"deepcopy": "^2.1.0",
|
"deepcopy": "^2.1.0",
|
||||||
"drachtio-fsmrf": "^4.0.3",
|
"drachtio-fsmrf": "^4.0.3",
|
||||||
"drachtio-srf": "^5.0.2",
|
"drachtio-srf": "^5.0.5",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"express-validator": "^7.0.1",
|
"express-validator": "^7.0.1",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
|
|||||||
Reference in New Issue
Block a user