diff --git a/app.js b/app.js index b97bf877..e3969dc6 100644 --- a/app.js +++ b/app.js @@ -27,8 +27,61 @@ const pino = require('pino'); const logger = pino(opts, pino.destination({sync: false})); const {LifeCycleEvents, FS_UUID_SET_NAME, SystemState, FEATURE_SERVER} = require('./lib/utils/constants'); 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; if (writeSystemAlerts) { writeSystemAlerts({ @@ -54,24 +107,6 @@ const { const InboundCallSession = require('./lib/session/inbound-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', [ initLocals, createRootSpan, @@ -97,27 +132,20 @@ sessionTracker.on('idle', () => { } }); const getCount = () => sessionTracker.count; -const healthCheck = require('@jambonz/http-health-check'); + 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() => { srf.locals.stats.gauge('fs.sip.calls.count', sessionTracker.count); try { const systemInformation = await srf.locals.dbHelpers.lookupSystemInformation(); 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) { if (process.env.NODE_ENV === 'test') { @@ -133,6 +161,7 @@ const disconnect = () => { httpServer?.on('close', resolve); httpServer?.close(); srf.disconnect(); + srf.removeAllListeners(); srf.locals.mediaservers?.forEach((ms) => ms.disconnect()); }); }; diff --git a/lib/middleware.js b/lib/middleware.js index 9985f969..72774c50 100644 --- a/lib/middleware.js +++ b/lib/middleware.js @@ -15,6 +15,7 @@ const { JAMBONES_DISABLE_DIRECT_P2P_CALL } = require('./config'); const { createJambonzApp } = require('./dynamic-apps'); +const { decrypt } = require('./utils/encrypt-decrypt'); module.exports = function(srf, logger) { const { @@ -348,11 +349,10 @@ module.exports = function(srf, logger) { } req.locals.application = app2; - // eslint-disable-next-line no-unused-vars const {call_hook, call_status_hook, ...appInfo} = app; // mask sensitive data like user/pass on webhook // 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}`); req.locals.callInfo = new CallInfo({ req, @@ -417,10 +417,22 @@ module.exports = function(srf, logger) { ...(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 } : {}, req.locals.callInfo, { service_provider_sid: req.locals.service_provider_sid }, - { defaults }); + { defaults }, + { env_vars } + ); logger.debug({ params }, 'sending initial webhook'); const obj = rootSpan.startChildSpan('performAppWebhook'); span = obj.span; diff --git a/lib/session/call-session.js b/lib/session/call-session.js index f847ac5f..50ef3005 100644 --- a/lib/session/call-session.js +++ b/lib/session/call-session.js @@ -23,6 +23,7 @@ const HttpRequestor = require('../utils/http-requestor'); const WsRequestor = require('../utils/ws-requestor'); const ActionHookDelayProcessor = require('../utils/action-hook-delay'); const TtsStreamingBuffer = require('../utils/tts-streaming-buffer'); +const StickyEventEmitter = require('../utils/sticky-event-emitter'); const {parseUri} = require('drachtio-srf'); const { JAMBONES_INJECT_CONTENT, @@ -79,6 +80,10 @@ class CallSession extends Emitter { this.callGone = false; this.notifiedComplete = false; this.rootSpan = rootSpan; + this.stickyEventEmitter = new StickyEventEmitter(); + this.stickyEventEmitter.onSuccess = () => { + this.taskInProgress = null; + }; this.backgroundTaskManager = new BackgroundTaskManager({ cs: this, logger, @@ -1180,7 +1185,9 @@ class CallSession extends Emitter { const taskNum = ++this.taskIdx; const stackNum = this.stackIdx; 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'}); // Register verbhook span wait for end task.on('VerbHookSpanWaitForEnd', ({span}) => { @@ -1919,6 +1926,8 @@ Duration=${duration} ` this.logger.debug({tasks: listTaskNames(tasks)}, `CallSession:replaceApplication reset with ${tasks.length} new tasks, stack depth is ${this.stackIdx}`); 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 = null; } @@ -1927,6 +1936,10 @@ Duration=${duration} ` this.wakeupResolver({reason: 'new tasks'}); 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) { @@ -2387,6 +2400,9 @@ Duration=${duration} ` * Hang up the call and free the media endpoint */ async _clearResources() { + this.stickyEventEmitter.destroy(); + this.stickyEventEmitter = null; + this.taskInProgress = null; for (const resource of [this.dlg, this.ep, this.ep2]) { try { if (resource && resource.connected) await resource.destroy(); diff --git a/lib/tasks/gather.js b/lib/tasks/gather.js index 67802a2b..360cef90 100644 --- a/lib/tasks/gather.js +++ b/lib/tasks/gather.js @@ -685,7 +685,9 @@ class TaskGather extends SttTask { } _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); this._clearAsrTimer(); this._asrTimer = setTimeout(() => { @@ -845,7 +847,8 @@ class TaskGather extends SttTask { } else { 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'); } else { diff --git a/lib/tasks/llm/index.js b/lib/tasks/llm/index.js index 2491b687..60dcf12c 100644 --- a/lib/tasks/llm/index.js +++ b/lib/tasks/llm/index.js @@ -4,6 +4,8 @@ const TaskLlmOpenAI_S2S = require('./llms/openai_s2s'); const TaskLlmVoiceAgent_S2S = require('./llms/voice_agent_s2s'); const TaskLlmUltravox_S2S = require('./llms/ultravox_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 { constructor(logger, opts) { @@ -18,6 +20,8 @@ class TaskLlm extends Task { // delegate to the specific llm model this.llm = this.createSpecificLlm(); + // MCP + this.mcpServers = this.data.mcpServers || []; } get name() { return this.llm.name ; } @@ -28,14 +32,32 @@ class TaskLlm extends Task { get ep() { return this.cs.ep; } + get mcpService() { + return this.llmMcpService; + } + + get isMcpEnabled() { + return this.mcpServers.length > 0; + } + async 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}); } async kill(cs) { super.kill(cs); await this.llm.kill(cs); + // clean up MCP clients + if (this.isMcpEnabled) { + await this.mcpService.close(); + } } createSpecificLlm() { @@ -59,6 +81,10 @@ class TaskLlm extends Task { llm = new TaskLlmElevenlabs_S2S(this.logger, this.data, this); break; + case 'google': + llm = new TaskLlmGoogle_S2S(this.logger, this.data, this); + break; + default: 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); } + 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) { diff --git a/lib/tasks/llm/llms/elevenlabs_s2s.js b/lib/tasks/llm/llms/elevenlabs_s2s.js index 984addb7..58e2a0fb 100644 --- a/lib/tasks/llm/llms/elevenlabs_s2s.js +++ b/lib/tasks/llm/llms/elevenlabs_s2s.js @@ -244,13 +244,36 @@ class TaskLlmElevenlabs_S2S extends Task { /* tool calls */ else if (type === 'client_tool_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!'); } else { - const {client_tool_call} = evt; - const {tool_name: name, tool_call_id: call_id, parameters: args} = client_tool_call; - try { await this.parent.sendToolHook(call_id, {name, args}); } catch (err) { diff --git a/lib/tasks/llm/llms/google_s2s.js b/lib/tasks/llm/llms/google_s2s.js new file mode 100644 index 00000000..dcf3349a --- /dev/null +++ b/lib/tasks/llm/llms/google_s2s.js @@ -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; diff --git a/lib/tasks/llm/llms/openai_s2s.js b/lib/tasks/llm/llms/openai_s2s.js index a98376ad..36ceb19d 100644 --- a/lib/tasks/llm/llms/openai_s2s.js +++ b/lib/tasks/llm/llms/openai_s2s.js @@ -235,6 +235,23 @@ class TaskLlmOpenAI_S2S extends Task { /* send immediate session.update if present */ 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}; this.logger.debug({obj}, 'TaskLlmOpenAI_S2S:_sendInitialMessage - sending session.update'); if (!await this._sendClientEvent(ep, obj)) { @@ -299,13 +316,37 @@ class TaskLlmOpenAI_S2S extends Task { /* tool calls */ else if (type === 'response.output_item.done' && evt.item?.type === '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!'); } else { - const {name, call_id} = evt.item; - const args = JSON.parse(evt.item.arguments); - try { await this.parent.sendToolHook(call_id, {name, args}); } catch (err) { diff --git a/lib/tasks/llm/llms/ultravox_s2s.js b/lib/tasks/llm/llms/ultravox_s2s.js index 2624fe6d..1823c2ac 100644 --- a/lib/tasks/llm/llms/ultravox_s2s.js +++ b/lib/tasks/llm/llms/ultravox_s2s.js @@ -39,6 +39,10 @@ class TaskLlmUltravox_S2S extends Task { this.eventHook = this.data.eventHook; this.toolHook = this.data.toolHook; + this.results = { + completionReason: 'normal conversation end' + }; + /** * only one of these will have items, * 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() { + 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 = { ...this.data.llmOptions, model: this.model, @@ -86,7 +133,7 @@ class TaskLlmUltravox_S2S extends Task { const data = await body.json(); if (statusCode !== 201 || !data?.joinUrl) { 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'); return data; @@ -106,12 +153,11 @@ class TaskLlmUltravox_S2S extends Task { async _startListening(cs, 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 { + 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]; await this._api(ep, args); // Notify the application that the session has been created with detail information @@ -121,6 +167,7 @@ class TaskLlmUltravox_S2S extends Task { }); } catch (err) { this.logger.info({err}, 'TaskLlmUltraVox_S2S:_startListening - Error sending createCall'); + this.results = {completionReason: `connection failure - ${err}`}; this.notifyTaskDone(); } } @@ -178,12 +225,35 @@ class TaskLlmUltravox_S2S extends Task { /* tool calls */ else if (type === 'client_tool_invocation') { 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!'); } else { - const {toolName: name, invocationId: call_id, parameters: args} = evt; - try { await this.parent.sendToolHook(call_id, {name, args}); } catch (err) { diff --git a/lib/tasks/llm/llms/voice_agent_s2s.js b/lib/tasks/llm/llms/voice_agent_s2s.js index 6ff3e5a9..0968d8c0 100644 --- a/lib/tasks/llm/llms/voice_agent_s2s.js +++ b/lib/tasks/llm/llms/voice_agent_s2s.js @@ -29,7 +29,7 @@ class TaskLlmVoiceAgent_S2S extends Task { this.parent = parentTask; this.vendor = this.parent.vendor; - this.model = this.parent.model; + this.model = this.parent.model || 'voice-agent'; this.auth = this.parent.auth; this.connectionOptions = this.parent.connectOptions; @@ -41,25 +41,25 @@ class TaskLlmVoiceAgent_S2S extends Task { this.actionHook = this.data.actionHook; this.eventHook = this.data.eventHook; this.toolHook = this.data.toolHook; - const {settingsConfiguration} = this.data.llmOptions; + const {Settings} = this.data.llmOptions; - if (typeof settingsConfiguration !== 'object') { - throw new Error('llmOptions with an initial settingsConfiguration is required for VoiceAgent S2S'); + if (typeof Settings !== 'object') { + throw new Error('llmOptions with an initial Settings is required for VoiceAgent S2S'); } // eslint-disable-next-line no-unused-vars - const {audio, ...rest} = settingsConfiguration; - const cfg = this.settingsConfiguration = rest; + const {audio, ...rest} = Settings; + 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) { - 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) { - throw new Error('llmOptions.settingsConfiguration.agent.think.model is required for VoiceAgent S2S'); + if (!cfg.agent.think.provider?.model) { + throw new Error('llmOptions.Settings.agent.think.provider.model is required for VoiceAgent S2S'); } 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 = { @@ -92,7 +92,7 @@ class TaskLlmVoiceAgent_S2S extends Task { const {path} = this.connectionOptions || {}; if (path) return path; - return '/agent'; + return '/v1/agent/converse'; } async _api(ep, args) { @@ -193,7 +193,20 @@ class TaskLlmVoiceAgent_S2S extends Task { } 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(); } } @@ -254,17 +267,43 @@ class TaskLlmVoiceAgent_S2S extends Task { /* tool calls */ else if (type === 'FunctionCallRequest') { 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!'); - } - else { - const {function_name:name, function_call_id:call_id} = evt; - const args = evt.input; + } else { + const {functions} = evt; + const handledFunctions = []; 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) { - this.logger.info({err, evt}, 'TaskLlmVoiceAgent - error calling function'); + this.logger.info({err, evt}, 'TaskLlmVoiceAgent_S2S:_onServerEvent - error calling function'); this.results = { completionReason: 'client error calling function', error: err diff --git a/lib/tasks/play.js b/lib/tasks/play.js index 8e2ee956..ae220c10 100644 --- a/lib/tasks/play.js +++ b/lib/tasks/play.js @@ -1,7 +1,6 @@ const Task = require('./task'); const {TaskName, TaskPreconditions} = require('../utils/constants'); const { PlayFileNotFoundError } = require('../utils/error'); - class TaskPlay extends Task { constructor(logger, opts) { super(logger, opts); @@ -27,6 +26,7 @@ class TaskPlay extends Task { let playbackSeconds = 0; let playbackMilliseconds = 0; let completed = !(this.timeoutSecs > 0 || this.loop); + cs.playingAudio = true; if (this.timeoutSecs > 0) { timeout = setTimeout(async() => { completed = true; @@ -40,6 +40,22 @@ class TaskPlay extends Task { try { this.notifyStatus({event: 'start-playback'}); 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) { const {memberId, confName, confUuid} = cs; if (Array.isArray(this.url)) { @@ -87,15 +103,15 @@ class TaskPlay extends Task { async kill(cs) { super.kill(cs); - if (this.ep.connected && !this.playComplete) { + if (this.ep?.connected && !this.playComplete) { this.logger.debug('TaskPlay:kill - killing audio'); if (cs.isInConference) { const {memberId, confName} = cs; this.killPlayToConfMember(this.ep, memberId, confName); } 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); } } } diff --git a/lib/tasks/say.js b/lib/tasks/say.js index 705d17ea..65f3e234 100644 --- a/lib/tasks/say.js +++ b/lib/tasks/say.js @@ -223,7 +223,19 @@ class TaskSay extends TtsTask { }); ep.once('playback-stop', (evt) => { 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({ account_sid, alert_type: AlertType.TTS_FAILURE, @@ -232,7 +244,7 @@ class TaskSay extends TtsTask { target_sid }).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]); addFileToCache(evt.variable_tts_cache_filename, { account_sid, @@ -241,12 +253,14 @@ class TaskSay extends TtsTask { voice, engine, model: this.model || this.model_id, - text + text, + instructions: this.instructions }).catch((err) => this.logger.info({err}, 'Error adding file to cache')); } 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 @@ -296,6 +310,9 @@ class TaskSay extends TtsTask { this.logger.debug('TaskSay:kill - clearing TTS stream for streaming audio'); cs.clearTtsStream(); } else { + if (!this.notifiedPlayBackStop) { + this.notifyStatus({event: 'stop-playback'}); + } this.notifyStatus({event: 'kill-playback'}); this.ep.api('uuid_break', this.ep.uuid); } @@ -316,6 +333,7 @@ class TaskSay extends TtsTask { if (key.startsWith('variable_tts_')) { let newKey = key.substring('variable_tts_'.length) .replace('whisper_', 'whisper.') + .replace('nvidia_', 'nvidia.') .replace('deepgram_', 'deepgram.') .replace('playht_', 'playht.') .replace('cartesia_', 'cartesia.') diff --git a/lib/tasks/sip_decline.js b/lib/tasks/sip_decline.js index 8da23a45..d293bb79 100644 --- a/lib/tasks/sip_decline.js +++ b/lib/tasks/sip_decline.js @@ -18,6 +18,11 @@ class TaskSipDecline extends Task { super.exec(cs); res.send(this.data.status, this.data.reason, { headers: this.headers + }, (err) => { + if (!err) { + // Call was successfully declined + cs._callReleased(); + } }); cs.emit('callStatusChange', { callStatus: CallStatus.Failed, diff --git a/lib/tasks/task.js b/lib/tasks/task.js index 25605b8d..4f9bda9b 100644 --- a/lib/tasks/task.js +++ b/lib/tasks/task.js @@ -19,6 +19,7 @@ class Task extends Emitter { this.data = data; this.actionHook = this.data.actionHook; this.id = data.id; + this.taskId = uuidv4(); this._killInProgress = false; this._completionPromise = new Promise((resolve) => this._completionResolver = resolve); diff --git a/lib/tasks/transcribe.js b/lib/tasks/transcribe.js index 3d9b9ff5..7725cd93 100644 --- a/lib/tasks/transcribe.js +++ b/lib/tasks/transcribe.js @@ -653,12 +653,21 @@ class TaskTranscribe extends SttTask { } _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.childSpan[channel - 1] && this.childSpan[channel - 1].span) { this.childSpan[channel - 1].span.setAttributes({ channel, - 'stt.resolve': 'max duration exceeded', + 'stt.resolve': reason, 'stt.label': this.label || 'None', }); this.childSpan[channel - 1].span.end(); @@ -715,6 +724,14 @@ class TaskTranscribe extends SttTask { return; } 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; const {writeAlerts, AlertType} = cs.srf.locals; diff --git a/lib/tasks/tts-task.js b/lib/tasks/tts-task.js index 57e329bf..ad79f92d 100644 --- a/lib/tasks/tts-task.js +++ b/lib/tasks/tts-task.js @@ -21,6 +21,7 @@ class TtsTask extends Task { this.synthesizer = this.data.synthesizer || {}; this.disableTtsCache = this.data.disableTtsCache; this.options = this.synthesizer.options || {}; + this.instructions = this.data.instructions; } async exec(cs) { @@ -262,6 +263,7 @@ class TtsTask extends Task { const {filePath, servedFromCache, rtt} = await synthAudio(stats, { account_sid, text, + instructions: this.instructions, vendor, language, voice, diff --git a/lib/utils/constants.json b/lib/utils/constants.json index 43bc1ba3..88b89282 100644 --- a/lib/utils/constants.json +++ b/lib/utils/constants.json @@ -194,6 +194,13 @@ "Disconnect": "openai_s2s::disconnect", "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": { "Error": "error", "Connect": "elevenlabs_s2s::connect", diff --git a/lib/utils/http-requestor.js b/lib/utils/http-requestor.js index cbac3d45..0986c012 100644 --- a/lib/utils/http-requestor.js +++ b/lib/utils/http-requestor.js @@ -108,7 +108,7 @@ class HttpRequestor extends BaseRequestor { 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 method = hook.method || 'POST'; let buf = ''; @@ -219,7 +219,7 @@ class HttpRequestor extends BaseRequestor { const rtt = this._roundTrip(startAt); 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`); return buf; } diff --git a/lib/utils/install-srf-locals.js b/lib/utils/install-srf-locals.js index a28b9049..2e9ccf20 100644 --- a/lib/utils/install-srf-locals.js +++ b/lib/utils/install-srf-locals.js @@ -31,18 +31,26 @@ function getLocalIp() { 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}); logger.info(`connected to freeswitch at ${ms.address}`); + onFreeswitchConnect(wrapper); + ms.conn .on('esl::end', () => { wrapper.active = false; + wrapper.connects = 0; logger.info(`lost connection to freeswitch at ${ms.address}`); + onFreeswitchDisconnect(wrapper); + ms.removeAllListeners(); }) .on('esl::ready', () => { 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.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'); assert(!srf.locals.dbHelpers); const {tracer} = srf.locals.otel; @@ -91,7 +102,10 @@ function installSrfLocals(srf, logger) { mediaservers.push(val); try { const ms = await mrf.connect(fs); - initMS(logger, val, ms); + initMS(logger, val, ms, { + onFreeswitchConnect, + onFreeswitchDisconnect + }); } catch (err) { 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) { if (val.connects === 0) { 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'); const ms = await mrf.connect(val.opts); - initMS(logger, val, ms); + initMS(logger, val, ms, { + onFreeswitchConnect, + onFreeswitchDisconnect + }); } catch (err) { logger.info({err}, `failed connecting to freeswitch at ${val.opts.address}, will retry shortly`); } diff --git a/lib/utils/llm-mcp.js b/lib/utils/llm-mcp.js new file mode 100644 index 00000000..a77fed89 --- /dev/null +++ b/lib/utils/llm-mcp.js @@ -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; + diff --git a/lib/utils/place-outdial.js b/lib/utils/place-outdial.js index 6cbf7798..59414320 100644 --- a/lib/utils/place-outdial.js +++ b/lib/utils/place-outdial.js @@ -279,7 +279,12 @@ class SingleDialer extends Emitter { this.logger.info('dial is onhold, emit event'); this.emit('reinvite', req, res); } 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}); this.logger.info({offer: req.body, answer: newSdp}, 'SingleDialer:exec: handling reINVITE'); } @@ -565,7 +570,8 @@ function placeOutdial({ }) { const myOpts = deepcopy(opts); 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); return sd; diff --git a/lib/utils/sticky-event-emitter.js b/lib/utils/sticky-event-emitter.js new file mode 100644 index 00000000..caea154d --- /dev/null +++ b/lib/utils/sticky-event-emitter.js @@ -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; diff --git a/lib/utils/tts-streaming-buffer.js b/lib/utils/tts-streaming-buffer.js index f3813ef6..de55342d 100644 --- a/lib/utils/tts-streaming-buffer.js +++ b/lib/utils/tts-streaming-buffer.js @@ -8,7 +8,7 @@ const { const MAX_CHUNK_SIZE = 1800; const HIGH_WATER_BUFFER_SIZE = 1000; const LOW_WATER_BUFFER_SIZE = 200; -const TIMEOUT_RETRY_MSECS = 3000; +const TIMEOUT_RETRY_MSECS = 1000; // 1 second const isWhitespace = (str) => /^\s*$/.test(str); @@ -377,6 +377,7 @@ class TtsStreamingBuffer extends Emitter { _onTimeout() { 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. const now = Date.now(); if (now - this.lastUpdateTime < TIMEOUT_RETRY_MSECS) { @@ -384,7 +385,6 @@ class TtsStreamingBuffer extends Emitter { this._setTimerIfNeeded(); return; } - this.timer = null; this._feedQueue(true); } diff --git a/lib/utils/ws-requestor.js b/lib/utils/ws-requestor.js index ab434540..f04adab0 100644 --- a/lib/utils/ws-requestor.js +++ b/lib/utils/ws-requestor.js @@ -132,7 +132,7 @@ class WsRequestor extends BaseRequestor { assert(this.ws); /* 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:reconnect') payload = this._sessionData; assert.ok(url, 'WsRequestor:request url was not provided'); diff --git a/package-lock.json b/package-lock.json index 80d7dcb3..04e4b0db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,14 +11,15 @@ "dependencies": { "@aws-sdk/client-auto-scaling": "^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/mw-registrar": "^0.2.7", "@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/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/exporter-jaeger": "^1.23.0", "@opentelemetry/exporter-trace-otlp-http": "^0.50.0", @@ -32,7 +33,7 @@ "debug": "^4.3.4", "deepcopy": "^2.1.0", "drachtio-fsmrf": "^4.0.3", - "drachtio-srf": "^5.0.2", + "drachtio-srf": "^5.0.5", "express": "^4.19.2", "express-validator": "^7.0.1", "moment": "^2.30.1", @@ -1428,9 +1429,9 @@ } }, "node_modules/@jambonz/db-helpers": { - "version": "0.9.11", - "resolved": "https://registry.npmjs.org/@jambonz/db-helpers/-/db-helpers-0.9.11.tgz", - "integrity": "sha512-L6xw60J0sYEMlR0EkeqolKbk+g08yvN51V8D0+bBfpTxsJUGgHiYhDgeGeTLU5J1FQCGwmj2RI1AVW5aAu6Q1A==", + "version": "0.9.12", + "resolved": "https://registry.npmjs.org/@jambonz/db-helpers/-/db-helpers-0.9.12.tgz", + "integrity": "sha512-cmUuh6G2yqsPZxx0v0iKq0puCaWPz6RGaHvMxBZlolEfLHxTcHshgm+Y/L7J3/4bEKaovzP4z7TXh/pab7mZig==", "dependencies": { "cidr-matcher": "^2.1.1", "debug": "^4.3.4", @@ -1466,11 +1467,11 @@ } }, "node_modules/@jambonz/speech-utils": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@jambonz/speech-utils/-/speech-utils-0.2.3.tgz", - "integrity": "sha512-ffx0TYggGDeEEB5yZ9y+BkF6dtDmKvCEgyK0lmzjkRQ2h3xaIWao3c5VCU9WXhKb+d2o6mLL06GebBMdzWIGhg==", - "license": "MIT", + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@jambonz/speech-utils/-/speech-utils-0.2.10.tgz", + "integrity": "sha512-RyBSS/xWMG/Uo3RG+jtESKXz/afySe8qaTuuAP9iDzEVzEXtSdxSp5lwuBjA1LZV8FUeWGI7KOV23izGQeNtHg==", "dependencies": { + "23": "^0.0.0", "@aws-sdk/client-polly": "^3.496.0", "@aws-sdk/client-sts": "^3.496.0", "@cartesia/cartesia-js": "^2.1.0", @@ -1483,17 +1484,8 @@ "google-protobuf": "^3.21.2", "ibm-watson": "^8.0.0", "microsoft-cognitiveservices-speech-sdk": "1.38.0", - "openai": "^4.25.0", - "undici": "^6.4.0" - } - }, - "node_modules/@jambonz/speech-utils/node_modules/undici": { - "version": "6.21.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.2.tgz", - "integrity": "sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g==", - "license": "MIT", - "engines": { - "node": ">=18.17" + "openai": "^4.98.0", + "undici": "^7.5.0" } }, "node_modules/@jambonz/stats-collector": { @@ -1513,9 +1505,10 @@ } }, "node_modules/@jambonz/verb-specifications": { - "version": "0.0.102", - "resolved": "https://registry.npmjs.org/@jambonz/verb-specifications/-/verb-specifications-0.0.102.tgz", - "integrity": "sha512-hpxzrzmSv3uRjjI503lFsDddVhfI5oFHWfGaym2RCUuKhv+hX5UKjW1joU1MRXyG2ZX4Oh5BRMAcjEI9lTxoLA==", + "version": "0.0.104", + "resolved": "https://registry.npmjs.org/@jambonz/verb-specifications/-/verb-specifications-0.0.104.tgz", + "integrity": "sha512-G1LjK6ISujdg0zALudtUvdaPXmvA4FU6x3s8S9MwUbWbFo2WERMUcNOgQAutDZwOMrLH9DnbPL8ZIdnTCKnlkA==", + "license": "MIT", "dependencies": { "debug": "^4.3.4", "pino": "^8.8.0" @@ -1614,6 +1607,293 @@ "node": ">= 0.4" } }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.9.0.tgz", + "integrity": "sha512-Jq2EUCQpe0iyO5FGpzVYDNFR6oR53AIrwph9yWl7uSc7IWUMsrmpmSaTGra5hQNunXpM+9oit85p924jWuHzUA==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.3", + "eventsource": "^3.0.2", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/@opentelemetry/api": { "version": "1.8.0", "license": "Apache-2.0", @@ -2501,7 +2781,9 @@ "license": "MIT" }, "node_modules/@types/node-fetch": { - "version": "2.6.11", + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", "license": "MIT", "dependencies": { "@types/node": "*", @@ -2509,11 +2791,14 @@ } }, "node_modules/@types/node-fetch/node_modules/form-data": { - "version": "4.0.0", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" }, "engines": { @@ -2568,6 +2853,12 @@ "version": "21.0.3", "license": "MIT" }, + "node_modules/23": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/23/-/23-0.0.0.tgz", + "integrity": "sha512-uAETf9Okr72trtp1pNXYKhFCTTI1EKGcYMA8gw3jLGhlbaDX+grrNToEWrpt8luxRAvrZWBTvyB5wk3PpNjGQQ==", + "license": "ISC" + }, "node_modules/abort-controller": { "version": "3.0.0", "license": "MIT", @@ -2619,7 +2910,9 @@ } }, "node_modules/agentkeepalive": { - "version": "4.5.0", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", "license": "MIT", "dependencies": { "humanize-ms": "^1.2.1" @@ -2852,9 +3145,6 @@ "dev": true, "license": "MIT" }, - "node_modules/base-64": { - "version": "0.1.0" - }, "node_modules/base64-js": { "version": "1.5.1", "funding": [ @@ -3060,6 +3350,7 @@ }, "node_modules/call-bind": { "version": "1.0.7", + "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", @@ -3075,6 +3366,35 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "dev": true, @@ -3139,13 +3459,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/charenc": { - "version": "0.0.2", - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, "node_modules/cidr-matcher": { "version": "2.1.1", "license": "MIT", @@ -3270,9 +3583,21 @@ "version": "1.0.2", "license": "MIT" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -3283,13 +3608,6 @@ "node": ">= 8" } }, - "node_modules/crypt": { - "version": "0.0.2", - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, "node_modules/d": { "version": "1.0.1", "license": "ISC", @@ -3347,10 +3665,12 @@ } }, "node_modules/debug": { - "version": "4.3.4", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -3428,6 +3748,7 @@ }, "node_modules/define-data-property": { "version": "1.1.4", + "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", @@ -3512,14 +3833,6 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/digest-fetch": { - "version": "1.3.0", - "license": "ISC", - "dependencies": { - "base-64": "^0.1.0", - "md5": "^2.3.0" - } - }, "node_modules/doctrine": { "version": "3.0.0", "dev": true, @@ -3619,10 +3932,9 @@ } }, "node_modules/drachtio-srf": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/drachtio-srf/-/drachtio-srf-5.0.3.tgz", - "integrity": "sha512-gOeOmU3LsrDXAw8a9Vd+od6cJXyqqV5E+2LsCD2N1SjoJybJS72PHTN+GfKtk3fRhFYpww2325CO4pr/DK21cA==", - "license": "MIT", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/drachtio-srf/-/drachtio-srf-5.0.5.tgz", + "integrity": "sha512-dKdOf+zonTvk1eVu0IVc4a/Hdah2KspXngAeLQ3NZHdAkjQCJjYQZubVP6ho2QT1pzu8wA3U4M/atL1O99Ujzw==", "dependencies": { "debug": "^3.2.7", "delegates": "^0.1.0", @@ -3636,7 +3948,7 @@ "uuid-random": "^1.3.2" }, "engines": { - "node": ">= 10.x" + "node": ">= 18.x" } }, "node_modules/drachtio-srf/node_modules/debug": { @@ -3657,6 +3969,20 @@ "node": ">=8" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/duplexify": { "version": "4.1.3", "license": "MIT", @@ -3789,11 +4115,10 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, "engines": { "node": ">= 0.4" } @@ -3825,8 +4150,9 @@ } }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "dev": true, + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -3836,13 +4162,15 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "dev": true, + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -4170,6 +4498,27 @@ "node": ">=0.8.x" } }, + "node_modules/eventsource": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.6.tgz", + "integrity": "sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", + "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/expect": { "version": "27.5.1", "license": "MIT", @@ -4227,6 +4576,21 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.1" + } + }, "node_modules/express-validator": { "version": "7.0.1", "license": "MIT", @@ -4490,6 +4854,8 @@ }, "node_modules/form-data-encoder": { "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", "license": "MIT" }, "node_modules/form-urlencoded": { @@ -4498,6 +4864,8 @@ }, "node_modules/formdata-node": { "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", "license": "MIT", "dependencies": { "node-domexception": "1.0.0", @@ -4507,13 +4875,6 @@ "node": ">= 12.20" } }, - "node_modules/formdata-node/node_modules/web-streams-polyfill": { - "version": "4.0.0-beta.3", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, "node_modules/forwarded": { "version": "0.2.0", "license": "MIT", @@ -4649,14 +5010,21 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -4673,6 +5041,19 @@ "node": ">=8.0.0" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-symbol-description": { "version": "1.0.2", "dev": true, @@ -4799,10 +5180,12 @@ "license": "(BSD-3-Clause AND Apache-2.0)" }, "node_modules/gopd": { - "version": "1.0.1", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3" + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4855,6 +5238,7 @@ }, "node_modules/has-property-descriptors": { "version": "1.0.2", + "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" @@ -4865,6 +5249,7 @@ }, "node_modules/has-proto": { "version": "1.0.3", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4874,7 +5259,9 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -4885,7 +5272,6 @@ }, "node_modules/has-tostringtag": { "version": "1.0.2", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -5016,6 +5402,8 @@ }, "node_modules/humanize-ms": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", "license": "MIT", "dependencies": { "ms": "^2.0.0" @@ -5322,10 +5710,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-buffer": { - "version": "1.1.6", - "license": "MIT" - }, "node_modules/is-callable": { "version": "1.2.7", "dev": true, @@ -5451,6 +5835,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/is-property": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", @@ -5604,7 +5994,6 @@ }, "node_modules/isexe": { "version": "2.0.0", - "dev": true, "license": "ISC" }, "node_modules/isstream": { @@ -6055,13 +6444,13 @@ "semver": "bin/semver.js" } }, - "node_modules/md5": { - "version": "2.3.0", - "license": "BSD-3-Clause", - "dependencies": { - "charenc": "0.0.2", - "crypt": "0.0.2", - "is-buffer": "~1.1.6" + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" } }, "node_modules/media-typer": { @@ -6259,7 +6648,9 @@ } }, "node_modules/ms": { - "version": "2.1.2", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, "node_modules/mysql2": { @@ -6358,6 +6749,9 @@ }, "node_modules/node-domexception": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", "funding": [ { "type": "github", @@ -6540,6 +6934,15 @@ "node": ">=6" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-hash": { "version": "3.0.0", "license": "MIT", @@ -6548,8 +6951,13 @@ } }, "node_modules/object-inspect": { - "version": "1.13.1", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6619,25 +7027,39 @@ "version": "0.0.2" }, "node_modules/openai": { - "version": "4.25.0", + "version": "4.98.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.98.0.tgz", + "integrity": "sha512-TmDKur1WjxxMPQAtLG5sgBSCJmX7ynTsGmewKzoDwl1fRxtbLOsiR0FA/AOAAtYUmP6azal+MYQuOENfdU+7yg==", "license": "Apache-2.0", "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", - "digest-fetch": "^1.3.0", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", - "node-fetch": "^2.6.7", - "web-streams-polyfill": "^3.2.1" + "node-fetch": "^2.6.7" }, "bin": { "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } } }, "node_modules/openai/node_modules/@types/node": { - "version": "18.19.8", + "version": "18.19.100", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.100.tgz", + "integrity": "sha512-ojmMP8SZBKprc3qGrGk8Ujpo80AXkrP7G2tOT4VWr5jlr5DHjsJF+emXJz+Wm0glmy4Js62oKMdZZ6B9Y+tEcA==", "license": "MIT", "dependencies": { "undici-types": "~5.26.4" @@ -6787,7 +7209,6 @@ }, "node_modules/path-key": { "version": "3.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6879,6 +7300,15 @@ "version": "6.2.1", "license": "MIT" }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/pkg-dir": { "version": "4.2.0", "dev": true, @@ -7268,6 +7698,31 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/safe-array-concat": { "version": "1.1.2", "dev": true, @@ -7394,10 +7849,6 @@ "node": ">= 0.8" } }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "license": "MIT" - }, "node_modules/seq-queue": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", @@ -7423,6 +7874,7 @@ }, "node_modules/set-function-length": { "version": "1.2.2", + "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -7456,7 +7908,6 @@ }, "node_modules/shebang-command": { "version": "2.0.0", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -7467,7 +7918,6 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7500,13 +7950,69 @@ } }, "node_modules/side-channel": { - "version": "1.0.6", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -8210,6 +8716,8 @@ }, "node_modules/undici-types": { "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "license": "MIT" }, "node_modules/universalify": { @@ -8221,6 +8729,7 @@ }, "node_modules/unix-dgram": { "version": "2.0.6", + "hasInstallScript": true, "license": "ISC", "optional": true, "dependencies": { @@ -8368,10 +8877,12 @@ } }, "node_modules/web-streams-polyfill": { - "version": "3.3.2", + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", "license": "MIT", "engines": { - "node": ">= 8" + "node": ">= 14" } }, "node_modules/webidl-conversions": { @@ -8425,7 +8936,6 @@ }, "node_modules/which": { "version": "2.0.2", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -8618,6 +9128,24 @@ "engines": { "node": ">=12" } + }, + "node_modules/zod": { + "version": "3.24.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", + "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.5", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", + "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } } } } diff --git a/package.json b/package.json index e5508ea1..dcc627af 100644 --- a/package.json +++ b/package.json @@ -27,14 +27,15 @@ "dependencies": { "@aws-sdk/client-auto-scaling": "^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/mw-registrar": "^0.2.7", "@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/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/exporter-jaeger": "^1.23.0", "@opentelemetry/exporter-trace-otlp-http": "^0.50.0", @@ -48,7 +49,7 @@ "debug": "^4.3.4", "deepcopy": "^2.1.0", "drachtio-fsmrf": "^4.0.3", - "drachtio-srf": "^5.0.2", + "drachtio-srf": "^5.0.5", "express": "^4.19.2", "express-validator": "^7.0.1", "moment": "^2.30.1",