@peopl-health/nexus 3.0.5 → 3.0.7

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.
@@ -3,7 +3,7 @@ const axios = require('axios');
3
3
  const runtimeConfig = require('../config/runtimeConfig');
4
4
  const { uploadMediaToS3, getFileExtension } = require('../helpers/mediaHelper');
5
5
  const { ensureWhatsAppFormat } = require('../helpers/twilioHelper');
6
- const { sanitizeMediaFilename } = require('../utils/sanitizer');
6
+ const { sanitizeMediaFilename } = require('../utils/inputSanitizer');
7
7
  const { generatePresignedUrl } = require('../config/awsConfig');
8
8
  const { validateMedia, getMediaType } = require('../utils/mediaValidator');
9
9
  const { logger } = require('../utils/logger');
@@ -781,8 +781,8 @@ class NexusMessaging {
781
781
  const lastMessage = await Message.findOne({
782
782
  numero: chatId,
783
783
  from_me: false,
784
- processed: false,
785
- message_id: { $exists: true, $ne: null, $not: /^pending-/ }
784
+ message_id: { $exists: true, $ne: null, $not: /^pending-/ },
785
+ createdAt: { $gte: new Date(Date.now() - 24 * 60 * 60 * 1000) }
786
786
  }).sort({ createdAt: -1 });
787
787
 
788
788
  if (!lastMessage?.message_id) {
@@ -7,7 +7,7 @@ const sharp = require('sharp');
7
7
 
8
8
  const { downloadFileFromS3 } = require('../config/awsConfig.js');
9
9
  const { Message } = require('../models/messageModel.js');
10
- const { sanitizeFilename } = require('../utils/sanitizer.js');
10
+ const { sanitizeFilename } = require('../utils/inputSanitizer.js');
11
11
  const { logger } = require('../utils/logger');
12
12
 
13
13
  async function convertPdfToImages(pdfName, existingPdfPath = null) {
@@ -1,7 +1,7 @@
1
1
  const path = require('path');
2
2
  const fs = require('fs');
3
3
  const AWS = require('../config/awsConfig.js');
4
- const { sanitizeMediaFilename } = require('../utils/sanitizer.js');
4
+ const { sanitizeMediaFilename } = require('../utils/inputSanitizer.js');
5
5
  const { logger } = require('../utils/logger');
6
6
 
7
7
  async function uploadMediaToS3(buffer, messageID, titleFile, bucketName, contentType, messageType) {
@@ -2,8 +2,7 @@ const fs = require('fs');
2
2
  const { generatePresignedUrl } = require('../config/awsConfig.js');
3
3
  const { analyzeImage } = require('./llmsHelper.js');
4
4
  const { cleanupFiles, downloadMediaAndCreateFile } = require('./filesHelper.js');
5
- const { formatMessage } = require('./messageHelper.js');
6
- const { sanitizeLogMetadata } = require('../utils/sanitizer.js');
5
+ const { sanitizeLogMetadata } = require('../utils/inputSanitizer.js');
7
6
  const { withTracing } = require('../utils/tracingDecorator.js');
8
7
 
9
8
  /**
@@ -23,65 +23,86 @@ function convertTwilioToInternalFormat(twilioMessage) {
23
23
  }
24
24
 
25
25
 
26
- async function downloadMediaFromTwilio(mediaUrl, logger) {
27
- try {
28
- const authHeader = `Basic ${Buffer.from(
29
- `${process.env.TWILIO_ACCOUNT_SID}:${process.env.TWILIO_AUTH_TOKEN}`
30
- ).toString('base64')}`;
31
-
32
- logger.info('[TwilioMedia] Starting download', {
33
- url: mediaUrl,
34
- hasAccountSid: !!process.env.TWILIO_ACCOUNT_SID,
35
- hasAuthToken: !!process.env.TWILIO_AUTH_TOKEN,
36
- accountSidLength: process.env.TWILIO_ACCOUNT_SID?.length || 0,
37
- authHeaderSample: authHeader.substring(0, 20) + '...'
38
- });
39
-
40
- const response = await axios({
41
- method: 'GET',
42
- url: mediaUrl,
43
- responseType: 'arraybuffer',
44
- timeout: 30000,
45
- headers: {
46
- 'Authorization': authHeader
47
- }
48
- });
26
+ async function downloadMediaFromTwilio(mediaUrl, logger, maxRetries = 5) {
27
+ logger.info('[TwilioMedia] Starting download', {
28
+ url: mediaUrl,
29
+ maxRetries,
30
+ hasAccountSid: !!process.env.TWILIO_ACCOUNT_SID,
31
+ hasAuthToken: !!process.env.TWILIO_AUTH_TOKEN,
32
+ hasCredentials: !!(process.env.TWILIO_ACCOUNT_SID && process.env.TWILIO_AUTH_TOKEN)
33
+ });
34
+
35
+ const authorization = `Basic ${Buffer.from(
36
+ `${process.env.TWILIO_ACCOUNT_SID}:${process.env.TWILIO_AUTH_TOKEN}`
37
+ ).toString('base64')}`;
38
+
39
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
40
+ try {
41
+ logger.debug('[TwilioMedia] Download attempt', { attempt, maxRetries });
42
+
43
+ const response = await axios.get(mediaUrl, {
44
+ headers: {
45
+ 'Authorization': authorization,
46
+ 'User-Agent': 'Nexus-Media-Processor/1.0'
47
+ },
48
+ responseType: 'arraybuffer',
49
+ timeout: 30000
50
+ });
49
51
 
50
- logger.info('[TwilioMedia] Download successful', {
51
- status: response.status,
52
- contentType: response.headers['content-type'],
53
- contentLength: response.headers['content-length'],
54
- dataSize: response.data?.length || 0
55
- });
52
+ logger.info('[TwilioMedia] Download successful', {
53
+ status: response.status,
54
+ contentType: response.headers['content-type'],
55
+ contentLength: response.headers['content-length'],
56
+ dataSize: response.data?.length || 0,
57
+ attempt
58
+ });
56
59
 
57
- return Buffer.from(response.data);
58
- } catch (error) {
59
- const is404 = error.response?.status === 404;
60
- const isMediaExpired = is404 && mediaUrl.includes('/Media/');
61
-
62
- logger.error('[TwilioMedia] Download failed', {
63
- message: error.message,
64
- status: error.response?.status,
65
- statusText: error.response?.statusText,
66
- isMediaExpired,
67
- responseHeaders: error.response?.headers,
68
- responseData: error.response?.data ? error.response.data.toString().substring(0, 500) : null,
69
- url: mediaUrl,
70
- hasCredentials: !!(process.env.TWILIO_ACCOUNT_SID && process.env.TWILIO_AUTH_TOKEN)
71
- });
72
-
73
- if (isMediaExpired) {
74
- logger.warn('[TwilioMedia] Media expired (24h limit), skipping download', {
75
- mediaId: mediaUrl.split('/').pop()
60
+ return Buffer.from(response.data);
61
+ } catch (error) {
62
+ const is404 = error.response?.status === 404;
63
+ const isLastAttempt = attempt === maxRetries;
64
+
65
+ if (is404 && !isLastAttempt) {
66
+ const delay = Math.pow(2, attempt - 1) * 1000; // 1s, 2s, 4s
67
+ logger.info('[TwilioMedia] Media not ready, retrying after delay', {
68
+ attempt,
69
+ maxRetries,
70
+ delayMs: delay,
71
+ mediaId: mediaUrl.split('/').pop()
72
+ });
73
+ await new Promise(resolve => setTimeout(resolve, delay));
74
+ continue;
75
+ }
76
+
77
+ // Check if truly expired (after all retries)
78
+ const isMediaExpired = is404 && mediaUrl.includes('/Media/');
79
+
80
+ if (isMediaExpired && isLastAttempt) {
81
+ logger.info('[TwilioMedia] Media expired (24h limit), skipping download', {
82
+ mediaId: mediaUrl.split('/').pop(),
83
+ status: 404,
84
+ attemptsUsed: attempt
85
+ });
86
+ return null;
87
+ }
88
+
89
+ logger.error('[TwilioMedia] Download failed', {
90
+ message: error.message,
91
+ status: error.response?.status,
92
+ statusText: error.response?.statusText,
93
+ attempt,
94
+ maxRetries,
95
+ responseHeaders: error.response?.headers,
96
+ responseData: error.response?.data ? error.response.data.toString().substring(0, 500) : null,
97
+ url: mediaUrl,
98
+ hasCredentials: !!(process.env.TWILIO_ACCOUNT_SID && process.env.TWILIO_AUTH_TOKEN)
76
99
  });
77
- return null;
100
+
101
+ throw error;
78
102
  }
79
-
80
- throw error;
81
103
  }
82
104
  }
83
105
 
84
-
85
106
  function getMediaTypeFromContentType(contentType) {
86
107
  if (contentType.startsWith('image/')) return 'imageMessage';
87
108
  if (contentType.startsWith('audio/')) return 'audioMessage';
@@ -13,7 +13,9 @@ const { processThreadMessage } = require('../helpers/processHelper.js');
13
13
  const { getLastNMessages, updateMessageRecord } = require('../helpers/messageHelper.js');
14
14
  const { combineImagesToPDF, cleanupFiles } = require('../helpers/filesHelper.js');
15
15
  const { getAssistantById } = require('./assistantResolver');
16
+
16
17
  const { logger } = require('../utils/logger');
18
+ const { sanitizeOutput } = require('../utils/outputSanitizer');
17
19
 
18
20
  const createAssistantCore = async (code, assistant_id, messages = [], force = false) => {
19
21
  const findThread = await Thread.findOne({ code: code });
@@ -190,7 +192,10 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
190
192
 
191
193
  await Promise.all(processResults.map(r => {
192
194
  const processedContent = r.messages && r.messages.length > 0
193
- ? r.messages.map(msg => msg.content.text).join(' ')
195
+ ? r.messages
196
+ .filter(msg => msg.content.text !== r.reply?.body)
197
+ .map(msg => msg.content.text)
198
+ .join(' ')
194
199
  : null;
195
200
  return updateMessageRecord(r.reply, finalThread, processedContent);
196
201
  }));
@@ -225,7 +230,16 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
225
230
  timings.run_assistant_ms = Date.now() - runStart;
226
231
  timings.total_ms = Date.now() - startTotal;
227
232
 
228
- const { output, completed, retries, predictionTimeMs, tools_executed } = runResult;
233
+ const { output: rawOutput, completed, retries, predictionTimeMs, tools_executed } = runResult;
234
+
235
+ const output = sanitizeOutput(rawOutput);
236
+ if (rawOutput !== output) {
237
+ logger.debug('[replyAssistantCore] Output sanitized', {
238
+ originalLength: rawOutput?.length || 0,
239
+ sanitizedLength: output?.length || 0,
240
+ removedContent: rawOutput?.length ? 'brackets_removed' : 'none'
241
+ });
242
+ }
229
243
 
230
244
  logger.info('[Assistant Reply Complete]', {
231
245
  code: code ? `${code.substring(0, 3)}***${code.slice(-4)}` : 'unknown',
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Sanitize AI response output by removing unwanted content
3
+ */
4
+
5
+ function removeBracketContent(text) {
6
+ if (!text || typeof text !== 'string') return text;
7
+ return text.replace(/\[([^\]]*)\]/g, '').trim();
8
+ }
9
+
10
+ function sanitizeOutput(text) {
11
+ if (!text || typeof text !== 'string') return text;
12
+
13
+ let sanitized = text;
14
+ sanitized = removeBracketContent(sanitized);
15
+ sanitized = sanitized.replace(/\s+/g, ' ').trim();
16
+
17
+ return sanitized;
18
+ }
19
+
20
+ module.exports = {
21
+ sanitizeOutput,
22
+ removeBracketContent
23
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "3.0.5",
3
+ "version": "3.0.7",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",
@@ -107,7 +107,7 @@
107
107
  "baileys": "^6.4.0",
108
108
  "express": "^4.22.1",
109
109
  "openai": "6.7.0",
110
- "twilio": "5.6.0"
110
+ "twilio": "5.11.2"
111
111
  },
112
112
  "engines": {
113
113
  "node": ">=20.0.0"
File without changes