@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.
- package/lib/adapters/TwilioProvider.js +1 -1
- package/lib/core/NexusMessaging.js +2 -2
- package/lib/helpers/filesHelper.js +1 -1
- package/lib/helpers/mediaHelper.js +1 -1
- package/lib/helpers/processHelper.js +1 -2
- package/lib/helpers/twilioHelper.js +73 -52
- package/lib/services/assistantServiceCore.js +16 -2
- package/lib/utils/outputSanitizer.js +23 -0
- package/package.json +2 -2
- /package/lib/utils/{sanitizer.js → inputSanitizer.js} +0 -0
|
@@ -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/
|
|
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
|
-
|
|
785
|
-
|
|
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/
|
|
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/
|
|
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 {
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
110
|
+
"twilio": "5.11.2"
|
|
111
111
|
},
|
|
112
112
|
"engines": {
|
|
113
113
|
"node": ">=20.0.0"
|
|
File without changes
|