@peopl-health/nexus 3.0.6 → 3.1.0

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.
@@ -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) {
@@ -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';
@@ -200,7 +200,7 @@ class OpenAIResponsesProvider {
200
200
  logger.info('[OpenAIResponsesProvider] Context built', {
201
201
  conversationId,
202
202
  assistantId,
203
- context
203
+ lastContext: context[-1] || null
204
204
  });
205
205
 
206
206
  const filter = thread.code ? { code: thread.code, active: true } : null;
@@ -192,7 +192,10 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
192
192
 
193
193
  await Promise.all(processResults.map(r => {
194
194
  const processedContent = r.messages && r.messages.length > 0
195
- ? 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(' ')
196
199
  : null;
197
200
  return updateMessageRecord(r.reply, finalThread, processedContent);
198
201
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "3.0.6",
3
+ "version": "3.1.0",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",