botium-connector-voip 0.0.21 → 0.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,6 +21,18 @@ var debug__default = /*#__PURE__*/_interopDefaultLegacy(debug$4);
21
21
  var musicMetadata__default = /*#__PURE__*/_interopDefaultLegacy(musicMetadata);
22
22
 
23
23
  const debug$3 = debug__default["default"]('botium-connector-voip');
24
+
25
+ // Logging policy: info = rare, business-relevant lifecycle events (always visible).
26
+ // debug = high-frequency diagnostics (DEBUG=botium-connector-voip). warn = degraded but continuing.
27
+ // error = abort/failure. No secrets in info; STT text only as length or truncated in info.
28
+
29
+ const _info = (event, data) => {
30
+ const parts = Object.entries({
31
+ event,
32
+ ...data
33
+ }).filter(([, v]) => v != null && v !== '').map(([k, v]) => `${k}=${typeof v === 'string' && v.length > 80 ? `"${v.substring(0, 77)}..."` : JSON.stringify(v)}`);
34
+ console.info(`[botium-connector-voip] ${parts.join(' ')}`);
35
+ };
24
36
  const Capabilities = {
25
37
  VOIP_STT_URL_STREAM: 'VOIP_STT_URL_STREAM',
26
38
  VOIP_STT_PARAMS_STREAM: 'VOIP_STT_PARAMS_STREAM',
@@ -68,7 +80,8 @@ const Capabilities = {
68
80
  VOIP_SILENCE_DURATION_TIMEOUT_START_ENABLE: 'VOIP_SILENCE_DURATION_TIMEOUT_START_ENABLE',
69
81
  VOIP_SILENCE_DURATION_TIMEOUT_START: 'VOIP_SILENCE_DURATION_TIMEOUT_START',
70
82
  VOIP_STT_CONFIDENCE_THRESHOLD: 'VOIP_STT_CONFIDENCE_THRESHOLD',
71
- VOIP_USE_GLOBAL_VOIP_WORKER: 'VOIP_USE_GLOBAL_VOIP_WORKER'
83
+ VOIP_USE_GLOBAL_VOIP_WORKER: 'VOIP_USE_GLOBAL_VOIP_WORKER',
84
+ VOIP_USER_INPUT_PREFER_VOICE: 'VOIP_USER_INPUT_PREFER_VOICE'
72
85
  };
73
86
  const Defaults = {
74
87
  VOIP_STT_METHOD: 'POST',
@@ -90,7 +103,8 @@ const Defaults = {
90
103
  VOIP_SILENCE_DURATION_TIMEOUT_START_ENABLE: false,
91
104
  VOIP_STT_CONFIDENCE_THRESHOLD: 0.5,
92
105
  VOIP_USE_GLOBAL_VOIP_WORKER: false,
93
- VOIP_SIP_PROTOCOL: 'TCP'
106
+ VOIP_SIP_PROTOCOL: 'TCP',
107
+ VOIP_USER_INPUT_PREFER_VOICE: true
94
108
  };
95
109
  const TTS_HTTP_AGENT = new http__default["default"].Agent({
96
110
  keepAlive: true
@@ -213,7 +227,8 @@ class BotiumConnectorVoip {
213
227
  };
214
228
  let data = null;
215
229
  let headers = null;
216
- const connect = async retryIndex => {
230
+ let httpInitRetries = 0;
231
+ const connectHttp = async retryIndex => {
217
232
  retryIndex = retryIndex || 0;
218
233
  try {
219
234
  const workerUrl = this.caps[Capabilities.VOIP_USE_GLOBAL_VOIP_WORKER] ? process.env.BOTIUM_VOIP_WORKER_URL : this.caps[Capabilities.VOIP_WORKER_URL].replace('wss', 'https').replace('ws', 'http');
@@ -234,17 +249,24 @@ class BotiumConnectorVoip {
234
249
  if (retryIndex === this.caps[Capabilities.VOIP_WEBSOCKET_CONNECT_MAXRETRIES]) {
235
250
  throw new Error(`Connecting to VOIP Worker failed: ${err.message || 'Not reachable'}`);
236
251
  }
252
+ httpInitRetries = retryIndex + 1;
237
253
  // Retry after the defined timeout
238
254
  await new Promise(resolve => setTimeout(resolve, this.caps[Capabilities.VOIP_WEBSOCKET_CONNECT_TIMEOUT]));
239
255
 
240
256
  // Retry the connection
241
- await connect(retryIndex + 1);
257
+ await connectHttp(retryIndex + 1);
242
258
  }
243
259
  };
244
- await connect();
260
+ await connectHttp();
261
+ if (httpInitRetries > 0) {
262
+ _info('connected_after_retries', {
263
+ phase: 'initCall',
264
+ retries: httpInitRetries
265
+ });
266
+ }
245
267
  return new Promise((resolve, reject) => {
246
268
  const wsEndpoint = `${this.caps[Capabilities.VOIP_USE_GLOBAL_VOIP_WORKER] ? process.env.BOTIUM_VOIP_WORKER_URL : this.caps[Capabilities.VOIP_WORKER_URL]}/ws/${data.port}`;
247
- const connect = retryIndex => {
269
+ const connectWs = retryIndex => {
248
270
  retryIndex = retryIndex || 0;
249
271
  return new Promise((resolve, reject) => {
250
272
  if ('set-cookie' in headers) {
@@ -257,17 +279,23 @@ class BotiumConnectorVoip {
257
279
  this.ws = new ws__default["default"](wsEndpoint);
258
280
  }
259
281
  this.ws.on('open', () => {
260
- resolve();
282
+ resolve(retryIndex);
261
283
  });
262
284
  this.ws.on('error', () => {
263
285
  if (retryIndex === this.caps[Capabilities.VOIP_WEBSOCKET_CONNECT_MAXRETRIES]) {
264
286
  reject(new Error(`Websocket connection failed to ${wsEndpoint}`));
265
287
  }
266
- setTimeout(() => connect(retryIndex + 1).then(resolve).catch(reject), this.caps[Capabilities.VOIP_WEBSOCKET_CONNECT_TIMEOUT]);
288
+ setTimeout(() => connectWs(retryIndex + 1).then(resolve).catch(reject), this.caps[Capabilities.VOIP_WEBSOCKET_CONNECT_TIMEOUT]);
267
289
  });
268
290
  });
269
291
  };
270
- connect().then(() => {
292
+ connectWs().then(wsRetries => {
293
+ if (wsRetries > 0) {
294
+ _info('connected_after_retries', {
295
+ phase: 'websocket',
296
+ retries: wsRetries
297
+ });
298
+ }
271
299
  if (!lodash__default["default"].isArray(this.caps[Capabilities.VOIP_ICE_STUN_SERVERS])) {
272
300
  if (lodash__default["default"].isEmpty(this.caps[Capabilities.VOIP_ICE_STUN_SERVERS])) {
273
301
  this.caps[Capabilities.VOIP_ICE_STUN_SERVERS] = [];
@@ -309,6 +337,7 @@ class BotiumConnectorVoip {
309
337
  });
310
338
  this.silence = null;
311
339
  this.msgCount = 0;
340
+ this.sttPartialCount = 0;
312
341
  this.firstMsg = true;
313
342
  this.firstSttInfoReceived = false;
314
343
  this.silenceTimeout = null;
@@ -453,6 +482,10 @@ class BotiumConnectorVoip {
453
482
  };
454
483
  if (parsedData && parsedData.type === 'callinfo' && parsedData.status === 'initialized') {
455
484
  this.sessionId = parsedData.voipConfig.sessionId;
485
+ _info('callinfo_initialized', {
486
+ sessionId: this.sessionId,
487
+ sttHandling: this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING]
488
+ });
456
489
  }
457
490
 
458
491
  // if sessionId is not the same as the one in the callinfo, return
@@ -467,9 +500,17 @@ class BotiumConnectorVoip {
467
500
  reject(new Error('Error: Sip Registration failed'));
468
501
  }
469
502
  if (parsedData && parsedData.type === 'callinfo' && parsedData.status === 'connected') {
503
+ _info('callinfo_connected', {
504
+ sessionId: this.sessionId
505
+ });
470
506
  resolve();
471
507
  }
472
508
  if (parsedData && parsedData.type === 'callinfo' && parsedData.status === 'disconnected') {
509
+ _info('callinfo_disconnected', {
510
+ sessionId: this.sessionId,
511
+ connectDurationSec: parsedData.connectDuration || null,
512
+ sttPartialCount: this.sttPartialCount
513
+ });
473
514
  const apiKey = this._extractApiKey(this._getBody(Capabilities.VOIP_STT_BODY));
474
515
  if (parsedData.connectDuration && parsedData.connectDuration > 0) {
475
516
  this.eventEmitter.emit('CONSUMPTION_METADATA', this, {
@@ -499,7 +540,13 @@ class BotiumConnectorVoip {
499
540
  if (parsedData && parsedData.type === 'fullRecordEnd') {
500
541
  const tail = _extractFullRecordBase64(parsedData);
501
542
  if (tail) this.fullRecord = (this.fullRecord || '') + tail;
543
+ const base64Len = (this.fullRecord || tail || '').length;
502
544
  emitFullRecordAttachment(this.fullRecord || tail);
545
+ _info('recording_attached', {
546
+ sessionId: this.sessionId,
547
+ source: 'fullRecordEnd',
548
+ base64Len
549
+ });
503
550
  this.end = true;
504
551
  }
505
552
  if (parsedData && parsedData.type === 'silence') {
@@ -512,10 +559,17 @@ class BotiumConnectorVoip {
512
559
  }
513
560
  if (parsedData && parsedData.type === 'fullRecord') {
514
561
  // Non-chunked full record
515
- emitFullRecordAttachment(_extractFullRecordBase64(parsedData));
562
+ const base64 = _extractFullRecordBase64(parsedData);
563
+ emitFullRecordAttachment(base64);
564
+ _info('recording_attached', {
565
+ sessionId: this.sessionId,
566
+ source: 'fullRecord',
567
+ base64Len: (base64 || '').length
568
+ });
516
569
  this.end = true;
517
570
  }
518
571
  if (parsedData && parsedData.data && parsedData.data.final === false) {
572
+ this.sttPartialCount++;
519
573
  if (this.silenceTimeout) {
520
574
  clearTimeout(this.silenceTimeout);
521
575
  }
@@ -562,7 +616,18 @@ class BotiumConnectorVoip {
562
616
  if (parsedData && parsedData.data && parsedData.data.type === 'stt' && parsedData.data.final) {
563
617
  const confidenceThreshold = this._getConfidenceScoreLogicHook(this.convoStep) && this._getConfidenceScoreLogicHook(this.convoStep).args[0] || this.caps[Capabilities.VOIP_STT_CONFIDENCE_THRESHOLD];
564
618
  const successfulConfidenceScore = this._getConfidenceScore(parsedData) >= confidenceThreshold;
619
+ const msgText = parsedData.data.message || '';
620
+ const msgLen = typeof msgText === 'string' ? msgText.length : 0;
621
+ const msgPreview = typeof msgText === 'string' ? msgText.trim().substring(0, 80) : '';
565
622
  debug$3(`Message: ${parsedData.data.message} / Confidence Score: ${this._getConfidenceScore(parsedData)} (Threshold: ${confidenceThreshold})`);
623
+ _info('stt_final', {
624
+ sessionId: this.sessionId,
625
+ message: msgPreview,
626
+ messageLength: msgLen,
627
+ confidence: this._getConfidenceScore(parsedData),
628
+ threshold: confidenceThreshold,
629
+ accepted: successfulConfidenceScore
630
+ });
566
631
  // ORIGINAL: emit final message immediately, unless join hooks are active.
567
632
  if (this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'ORIGINAL' && lodash__default["default"].isNil(this._getJoinLogicHook(this.convoStep))) {
568
633
  let botMsg = {
@@ -663,6 +728,19 @@ class BotiumConnectorVoip {
663
728
  }
664
729
  async UserSays(msg) {
665
730
  debug$3('UserSays called');
731
+ const hasText = !!(msg && msg.messageText);
732
+ const hasVoiceMedia = !!(msg && msg.media && msg.media.length > 0 && msg.media[0].buffer);
733
+ const hasDtmf = !!(msg && msg.buttons && msg.buttons.length > 0);
734
+ const dtmfMatch = msg && msg.messageText && msg.messageText.match(/<DTMF>([^<]+)<\/DTMF>/i);
735
+ const inputType = hasDtmf || dtmfMatch ? 'dtmf' : hasText && hasVoiceMedia ? 'mixed' : hasText ? 'text' : hasVoiceMedia ? 'media' : 'unknown';
736
+ const msgPreview = hasText && msg.messageText ? String(msg.messageText).trim().substring(0, 80) : '';
737
+ _info('user_says', {
738
+ sessionId: this.sessionId,
739
+ inputType,
740
+ message: msgPreview || undefined,
741
+ messageLength: hasText && msg.messageText ? msg.messageText.length : undefined,
742
+ mediaSize: hasVoiceMedia && msg.media[0] && Buffer.isBuffer(msg.media[0].buffer) ? msg.media[0].buffer.length : undefined
743
+ });
666
744
  // Avoid logging large buffers/base64 (can break job logs and overwhelm stdout)
667
745
  try {
668
746
  const safeLog = {
@@ -687,6 +765,10 @@ class BotiumConnectorVoip {
687
765
  return new Promise((resolve, reject) => {
688
766
  setTimeout(async () => {
689
767
  let duration = 0;
768
+ const preferVoiceCapRaw = this.caps[Capabilities.VOIP_USER_INPUT_PREFER_VOICE];
769
+ const preferVoice = !!preferVoiceCapRaw;
770
+ const skipTtsForMixedInput = preferVoice && hasText && hasVoiceMedia;
771
+ debug$3(`UserSays routing: hasText=${hasText} hasVoiceMedia=${hasVoiceMedia} preferVoice=${preferVoice} preferVoiceRaw=${JSON.stringify(preferVoiceCapRaw)} skipTtsForMixedInput=${skipTtsForMixedInput}`);
690
772
  if (msg && msg.buttons && msg.buttons.length > 0) {
691
773
  const request = JSON.stringify({
692
774
  METHOD: 'sendDtmf',
@@ -708,55 +790,61 @@ class BotiumConnectorVoip {
708
790
  this.ws.send(request);
709
791
  return resolve();
710
792
  }
711
- if (!this.axiosTtsParams) {
712
- if (!(msg.media && msg.media.length > 0 && msg.media[0].buffer)) {
713
- return reject(new Error('TTS not configured, only audio input supported'));
714
- }
715
- } else {
716
- const ttsRequest = this._buildTtsRequest(msg.messageText);
717
- if (!ttsRequest) return reject(new Error('TTS not configured, only audio input supported'));
718
- msg.sourceData = ttsRequest;
719
- let ttsResult = null;
720
- try {
721
- ttsResult = await this._getTtsAudio(ttsRequest, msg.messageText);
722
- } catch (err) {
723
- return reject(new Error(`TTS "${msg.messageText}" failed - ${this._getAxiosErrOutput(err)}`));
724
- }
725
- if (msg && msg.messageText && msg.messageText.length > 0) {
726
- const apiKey = this._extractApiKey(this._getBody(Capabilities.VOIP_TTS_BODY));
727
- this.eventEmitter.emit('CONSUMPTION_METADATA', this.container, {
728
- type: lodash__default["default"].isNil(apiKey) ? 'INBUILT' : 'THIRD_PARTY',
729
- metricName: 'consumption.e2e.voip.tts.characters',
730
- transactions: msg.messageText.length,
731
- apiKey
732
- });
733
- }
734
- if (ttsResult && Buffer.isBuffer(ttsResult.buffer)) {
735
- duration = parseFloat(ttsResult.duration) || 0;
736
- const b64Buffer = ttsResult.buffer.toString('base64');
737
- const request = JSON.stringify({
738
- METHOD: 'sendAudio',
739
- PESQ: false,
740
- sessionId: this.sessionId,
741
- b64_buffer: b64Buffer
742
- });
743
- if (this._lastBotSaysQueuedAt) {
744
- const latencyMs = Date.now() - this._lastBotSaysQueuedAt;
745
- const botText = this._lastBotSaysText ? this._lastBotSaysText.substring(0, 120) : '<unknown>';
746
- debug$3(`Latency (bot says -> sendAudio/TTS): ${latencyMs} ms (last bot: ${botText})`);
793
+ if (!skipTtsForMixedInput) {
794
+ if (!this.axiosTtsParams) {
795
+ if (!(msg.media && msg.media.length > 0 && msg.media[0].buffer)) {
796
+ return reject(new Error('TTS not configured, only audio input supported'));
747
797
  }
748
- msg.attachments.push({
749
- name: 'tts.wav',
750
- mimeType: 'audio/wav',
751
- base64: b64Buffer
752
- });
753
- this.ws.send(request);
754
798
  } else {
755
- return reject(new Error('TTS failed, response is empty'));
799
+ debug$3('UserSays routing: executing TTS branch');
800
+ const ttsRequest = this._buildTtsRequest(msg.messageText);
801
+ if (!ttsRequest) return reject(new Error('TTS not configured, only audio input supported'));
802
+ msg.sourceData = ttsRequest;
803
+ let ttsResult = null;
804
+ try {
805
+ ttsResult = await this._getTtsAudio(ttsRequest, msg.messageText);
806
+ } catch (err) {
807
+ return reject(new Error(`TTS "${msg.messageText}" failed - ${this._getAxiosErrOutput(err)}`));
808
+ }
809
+ if (msg && msg.messageText && msg.messageText.length > 0) {
810
+ const apiKey = this._extractApiKey(this._getBody(Capabilities.VOIP_TTS_BODY));
811
+ this.eventEmitter.emit('CONSUMPTION_METADATA', this.container, {
812
+ type: lodash__default["default"].isNil(apiKey) ? 'INBUILT' : 'THIRD_PARTY',
813
+ metricName: 'consumption.e2e.voip.tts.characters',
814
+ transactions: msg.messageText.length,
815
+ apiKey
816
+ });
817
+ }
818
+ if (ttsResult && Buffer.isBuffer(ttsResult.buffer)) {
819
+ duration = parseFloat(ttsResult.duration) || 0;
820
+ const b64Buffer = ttsResult.buffer.toString('base64');
821
+ const request = JSON.stringify({
822
+ METHOD: 'sendAudio',
823
+ PESQ: false,
824
+ sessionId: this.sessionId,
825
+ b64_buffer: b64Buffer
826
+ });
827
+ if (this._lastBotSaysQueuedAt) {
828
+ const latencyMs = Date.now() - this._lastBotSaysQueuedAt;
829
+ const botText = this._lastBotSaysText ? this._lastBotSaysText.substring(0, 120) : '<unknown>';
830
+ debug$3(`Latency (bot says -> sendAudio/TTS): ${latencyMs} ms (last bot: ${botText})`);
831
+ }
832
+ msg.attachments.push({
833
+ name: 'tts.wav',
834
+ mimeType: 'audio/wav',
835
+ base64: b64Buffer
836
+ });
837
+ this.ws.send(request);
838
+ } else {
839
+ return reject(new Error('TTS failed, response is empty'));
840
+ }
756
841
  }
842
+ } else {
843
+ debug$3('UserSays routing: skipping TTS for mixed input because VOIP_USER_INPUT_PREFER_VOICE is enabled');
757
844
  }
758
845
  }
759
846
  if (msg && msg.media && msg.media.length > 0 && msg.media[0].buffer) {
847
+ debug$3('UserSays routing: executing MEDIA branch');
760
848
  const request = JSON.stringify({
761
849
  METHOD: 'sendAudio',
762
850
  sessionId: this.sessionId,
@@ -1075,6 +1163,12 @@ var botiumConnectorVoip = {
1075
1163
  label: 'Speech Synthesis Profile',
1076
1164
  type: 'speechsynthesisprofile',
1077
1165
  required: true
1166
+ }, {
1167
+ name: 'VOIP_USER_INPUT_PREFER_VOICE',
1168
+ label: 'Prefer voice media when both text and voice present',
1169
+ type: 'boolean',
1170
+ required: false,
1171
+ advanced: true
1078
1172
  }]
1079
1173
  },
1080
1174
  PluginLogicHooks: {