@peopl-health/nexus 3.0.4 → 3.0.6

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.
@@ -1,5 +1,6 @@
1
1
  const { MessageProvider } = require('../core/MessageProvider');
2
2
  const { logger } = require('../utils/logger');
3
+ const { calculateDelay } = require('../utils/scheduleUtils');
3
4
 
4
5
  /**
5
6
  * Baileys WhatsApp messaging provider
@@ -150,7 +151,7 @@ class BaileysProvider extends MessageProvider {
150
151
 
151
152
  async sendScheduledMessage(scheduledMessage) {
152
153
  const { sendTime, timeZone, __nexusSend } = scheduledMessage;
153
- const delay = this.calculateDelay(sendTime, timeZone);
154
+ const delay = calculateDelay(sendTime, timeZone);
154
155
 
155
156
  const sender = typeof __nexusSend === 'function'
156
157
  ? __nexusSend
@@ -181,11 +182,6 @@ class BaileysProvider extends MessageProvider {
181
182
  return { scheduled: true, delay };
182
183
  }
183
184
 
184
- calculateDelay(sendTime) {
185
- const now = new Date();
186
- const targetTime = new Date(sendTime);
187
- return Math.max(0, targetTime.getTime() - now.getTime());
188
- }
189
185
 
190
186
  getConnectionStatus() {
191
187
  return this.waSocket?.user ? true : false;
@@ -3,10 +3,11 @@ 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');
10
+ const { calculateDelay } = require('../utils/scheduleUtils');
10
11
  const { v4: uuidv4 } = require('uuid');
11
12
 
12
13
  /**
@@ -220,7 +221,7 @@ class TwilioProvider extends MessageProvider {
220
221
 
221
222
  async sendScheduledMessage(scheduledMessage) {
222
223
  const { sendTime, timeZone, __nexusSend } = scheduledMessage;
223
- const delay = this.calculateDelay(sendTime, timeZone);
224
+ const delay = calculateDelay(sendTime, timeZone);
224
225
 
225
226
  const sender = typeof __nexusSend === 'function'
226
227
  ? __nexusSend
@@ -423,11 +424,6 @@ class TwilioProvider extends MessageProvider {
423
424
  }
424
425
  }
425
426
 
426
- calculateDelay(sendTime) {
427
- const now = new Date();
428
- const targetTime = new Date(sendTime);
429
- return Math.max(0, targetTime.getTime() - now.getTime());
430
- }
431
427
 
432
428
  /**
433
429
  * Split a message into chunks at sentence boundaries, respecting Twilio's character limit
@@ -729,6 +729,11 @@ class NexusMessaging {
729
729
  const botResponse = typeof result === 'string' ? result : result?.output;
730
730
  const tools_executed = typeof result === 'object' ? result?.tools_executed : undefined;
731
731
 
732
+ if (typingInterval) {
733
+ clearInterval(typingInterval);
734
+ typingInterval = null;
735
+ }
736
+
732
737
  if (botResponse) {
733
738
  await this.sendMessage({
734
739
  code: chatId,
@@ -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) {
@@ -10,13 +10,13 @@ async function uploadMediaToS3(buffer, messageID, titleFile, bucketName, content
10
10
  const fileName = sanitizedTitle
11
11
  ? `${messageType}/${messageID}_${sanitizedTitle}.${extension}`
12
12
  : `${messageType}/${messageID}.${extension}`;
13
- logger.info(titleFile, messageType);
13
+ logger.info('[uploadMediaToS3] Media file prepared for upload', { fileName, messageType });
14
14
 
15
15
  try {
16
16
  await AWS.uploadBufferToS3(buffer, bucketName, fileName, contentType);
17
17
  return fileName;
18
18
  } catch (error) {
19
- logger.error('Failed to upload media to S3:', error.stack);
19
+ logger.error('[uploadMediaToS3] Failed to upload media to S3:', error.stack);
20
20
  throw error;
21
21
  }
22
22
  }
@@ -2,19 +2,31 @@ const moment = require('moment-timezone');
2
2
  const { Message } = require('../models/messageModel.js');
3
3
  const { logger } = require('../utils/logger');
4
4
 
5
- const updateMessageRecord = async (reply, thread) => {
5
+ const updateMessageRecord = async (reply, thread, processedContent = null) => {
6
6
  const threadId = thread.getConversationId();
7
7
 
8
+ const updateData = {
9
+ assistant_id: thread.getAssistantId(),
10
+ thread_id: threadId,
11
+ processed: true
12
+ };
13
+
14
+ if (processedContent && reply.media) {
15
+ updateData.media = {
16
+ ...reply.media,
17
+ metadata: {
18
+ ...(reply.media.metadata || {}),
19
+ processed_content: processedContent
20
+ }
21
+ };
22
+ }
23
+
8
24
  await Message.updateOne(
9
25
  { message_id: reply.message_id, timestamp: reply.timestamp },
10
- { $set: {
11
- assistant_id: thread.getAssistantId(),
12
- thread_id: threadId,
13
- processed: true
14
- } }
26
+ { $set: updateData }
15
27
  );
16
28
 
17
- logger.info(`[updateMessageRecord] Record updated - ID: ${reply.message_id}, Thread: ${threadId}, Processed: true`);
29
+ logger.info(`[updateMessageRecord] Record updated - ID: ${reply.message_id}, Thread: ${threadId}, Processed: true, MediaContentUpdated: ${!!(processedContent && reply.media)}`);
18
30
  };
19
31
 
20
32
 
@@ -81,7 +93,12 @@ function formatMessage(reply) {
81
93
  .locale('es')
82
94
  .format('dddd, D [de] MMMM [de] YYYY [a las] h:mm A');
83
95
 
84
- return `[${mexicoCityTime}] ${reply.body}`;
96
+ let messageContent = reply.body;
97
+ if (reply.media?.metadata?.processed_content) {
98
+ messageContent = reply.media.metadata.processed_content;
99
+ }
100
+
101
+ return `[${mexicoCityTime}] ${messageContent}`;
85
102
  } catch (error) {
86
103
  logger.error(`[formatMessage] Error for message ID: ${reply.message_id}:`, error?.message || String(error));
87
104
  return null;
@@ -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
  /**
@@ -35,23 +34,19 @@ const logger = {
35
34
 
36
35
  /**
37
36
  * Dedicated message processing utilities
38
- * Handles text messages, media files, audio transcription, and thread operations
39
37
  */
40
38
  const processTextMessage = (reply) => {
41
- const formattedMessage = formatMessage(reply);
42
39
  logger.info('processTextMessage', {
43
40
  message_id: reply.message_id,
44
41
  timestamp: reply.timestamp,
45
42
  from_me: reply.from_me,
46
43
  body: reply.body,
47
- hasContent: !!formattedMessage,
48
- formattedMessage
44
+ hasContent: !!reply.body
49
45
  });
50
- logger.debug('processTextMessage_content', { formattedMessage });
51
46
 
52
47
  const messagesChat = [];
53
- if (formattedMessage) {
54
- messagesChat.push({ type: 'text', text: formattedMessage });
48
+ if (reply.body) {
49
+ messagesChat.push({ type: 'text', text: reply.body });
55
50
  }
56
51
 
57
52
  return messagesChat;
@@ -121,8 +116,6 @@ const processImageFileCore = async (fileName, reply) => {
121
116
  ...timings
122
117
  });
123
118
 
124
- logger.debug('processImageFile_analysis', { imageAnalysis });
125
-
126
119
  } catch (error) {
127
120
  logger.error('processImageFile', error, {
128
121
  message_id: reply.message_id,
@@ -12,12 +12,6 @@ async function generateQRBuffer(text) {
12
12
  }
13
13
  }
14
14
 
15
- function bufferToBase64(buffer) {
16
- return buffer.toString('base64');
17
- }
18
-
19
-
20
15
  module.exports = {
21
- generateQRBuffer,
22
- bufferToBase64
16
+ generateQRBuffer
23
17
  };
@@ -1,4 +1,3 @@
1
- const { Message } = require('../models/messageModel.js');
2
1
  const { isRecentMessage } = require('./messageHelper.js');
3
2
  const axios = require('axios');
4
3
  const { v4: uuidv4 } = require('uuid');
@@ -57,15 +56,27 @@ async function downloadMediaFromTwilio(mediaUrl, logger) {
57
56
 
58
57
  return Buffer.from(response.data);
59
58
  } catch (error) {
59
+ const is404 = error.response?.status === 404;
60
+ const isMediaExpired = is404 && mediaUrl.includes('/Media/');
61
+
60
62
  logger.error('[TwilioMedia] Download failed', {
61
63
  message: error.message,
62
64
  status: error.response?.status,
63
65
  statusText: error.response?.statusText,
66
+ isMediaExpired,
64
67
  responseHeaders: error.response?.headers,
65
68
  responseData: error.response?.data ? error.response.data.toString().substring(0, 500) : null,
66
69
  url: mediaUrl,
67
70
  hasCredentials: !!(process.env.TWILIO_ACCOUNT_SID && process.env.TWILIO_AUTH_TOKEN)
68
71
  });
72
+
73
+ if (isMediaExpired) {
74
+ logger.warn('[TwilioMedia] Media expired (24h limit), skipping download', {
75
+ mediaId: mediaUrl.split('/').pop()
76
+ });
77
+ return null;
78
+ }
79
+
69
80
  throw error;
70
81
  }
71
82
  }
@@ -89,16 +100,6 @@ function extractTitle(message, mediaType) {
89
100
  }
90
101
 
91
102
 
92
- async function getLastMessages(chatId, n) {
93
- const messages = await Message.find({ numero: chatId })
94
- .sort({ createdAt: -1 })
95
- .limit(n)
96
- .select('timestamp numero nombre_whatsapp body');
97
-
98
- return messages.map(msg => `[${msg.timestamp}] ${msg.body}`);
99
- }
100
-
101
-
102
103
  async function downloadMedia(twilioMessage, logger) {
103
104
  try {
104
105
  const mediaUrl = twilioMessage.MediaUrl0;
@@ -145,7 +146,6 @@ module.exports = {
145
146
  getMediaTypeFromContentType,
146
147
  extractTitle,
147
148
  isRecentMessage,
148
- getLastMessages,
149
149
  downloadMedia,
150
150
  ensureWhatsAppFormat
151
151
  };
@@ -82,6 +82,11 @@ async function processTwilioMediaMessage(twilioMessage, bucketName) {
82
82
  continue;
83
83
  }
84
84
 
85
+ if (!mediaBuffer) {
86
+ logger.info('[TwilioMedia] Skipping expired media', { index: i, mediaUrl });
87
+ continue;
88
+ }
89
+
85
90
  const validationResult = validateMedia(mediaBuffer, contentType);
86
91
  if (!validationResult.valid) {
87
92
  logger.warn('[TwilioMedia] Media validation warning', { index: i, message: validationResult.message });
@@ -1,12 +1,6 @@
1
- const moment = require('moment-timezone');
2
1
  const { logger } = require('../utils/logger');
3
2
 
4
3
 
5
- function delay(ms) {
6
- return new Promise(resolve => setTimeout(resolve, ms));
7
- }
8
-
9
-
10
4
  function formatCode(codeBase) {
11
5
  logger.info(`formatCode ${codeBase}`);
12
6
 
@@ -37,40 +31,6 @@ function formatCode(codeBase) {
37
31
  }
38
32
 
39
33
 
40
- function calculateDelay(sendTime, timeZone) {
41
- if (sendTime !== undefined && timeZone !== undefined) {
42
- const sendMoment = moment.tz(sendTime, timeZone);
43
-
44
- if (!sendMoment.isValid()) {
45
- return { error: 'Invalid time format' };
46
- }
47
-
48
- // Get the current time and calculate the difference
49
- const now = moment().tz(timeZone);
50
- const randomDelay = Math.floor(Math.random() * 15001) + 15000;
51
- const delay = sendMoment.diff(now) + randomDelay;
52
-
53
- // Log the calculated details for debugging
54
- logger.info(
55
- 'Scheduled Time:', sendMoment.format(),
56
- 'Current Time:', now.format(),
57
- 'Delay (minutes):', delay / 60000,
58
- 'Remaining Seconds:', delay % 60000
59
- );
60
-
61
- if (delay <= 0) {
62
- return 2500;
63
- }
64
-
65
- return delay;
66
- } else {
67
- return 2500;
68
- }
69
- }
70
-
71
-
72
34
  module.exports = {
73
- delay,
74
- formatCode,
75
- calculateDelay
35
+ formatCode
76
36
  };
package/lib/index.d.ts CHANGED
@@ -283,7 +283,6 @@ declare module '@peopl-health/nexus' {
283
283
 
284
284
  // Utility Functions
285
285
  export function createLogger(config?: any): any;
286
- export function delay(ms: number): Promise<void>;
287
286
  export function formatCode(codeBase: string): string;
288
287
  export function calculateDelay(sendTime: string | Date, timeZone?: string): number;
289
288
  export function ensureWhatsAppFormat(phoneNumber: any): string | null;
@@ -1,11 +1,11 @@
1
- const { ConversationManager } = require('./ConversationManager');
1
+ const { MemoryManager } = require('./MemoryManager');
2
2
  const { getLastNMessages, formatMessage } = require('../helpers/messageHelper');
3
3
  const { handlePendingFunctionCalls: handlePendingFunctionCallsUtil } = require('../providers/OpenAIResponsesProviderTools');
4
- const { getRecordByFilter } = require('./airtableService');
4
+ const { getRecordByFilter } = require('../services/airtableService');
5
5
  const { Follow_Up_ID } = require('../config/airtableConfig');
6
6
  const { logger } = require('../utils/logger');
7
7
 
8
- class DefaultConversationManager extends ConversationManager {
8
+ class DefaultMemoryManager extends MemoryManager {
9
9
  constructor(options = {}) {
10
10
  super(options);
11
11
  this.maxHistoricalMessages = parseInt(process.env.MAX_HISTORICAL_MESSAGES || '50', 10);
@@ -18,7 +18,6 @@ class DefaultConversationManager extends ConversationManager {
18
18
  const allMessages = await getLastNMessages(thread.code, this.maxHistoricalMessages);
19
19
  const additionalMessages = config.additionalMessages || [];
20
20
 
21
- // New conversation - no history yet
22
21
  if (!allMessages?.length) {
23
22
  return additionalMessages;
24
23
  }
@@ -33,7 +32,7 @@ class DefaultConversationManager extends ConversationManager {
33
32
 
34
33
  return [...additionalMessages, ...messageContext];
35
34
  } catch (error) {
36
- logger.error('[DefaultConversationManager] Context building failed', {
35
+ logger.error('[DefaultMemoryManager] Context building failed', {
37
36
  threadCode: thread.code,
38
37
  error: error.message
39
38
  });
@@ -53,7 +52,7 @@ class DefaultConversationManager extends ConversationManager {
53
52
  responseId: response.id
54
53
  });
55
54
  } catch (error) {
56
- logger.error('[DefaultConversationManager] Response processing failed', {
55
+ logger.error('[DefaultMemoryManager] Response processing failed', {
57
56
  threadCode: thread.code,
58
57
  error: error.message
59
58
  });
@@ -66,7 +65,7 @@ class DefaultConversationManager extends ConversationManager {
66
65
  try {
67
66
  return await handlePendingFunctionCallsUtil(assistant, conversationMessages, toolMetadata);
68
67
  } catch (error) {
69
- logger.error('[DefaultConversationManager] Function call handling failed', { error: error.message });
68
+ logger.error('[DefaultMemoryManager] Function call handling failed', { error: error.message });
70
69
  return { outputs: [], toolsExecuted: [] };
71
70
  }
72
71
  }
@@ -122,7 +121,7 @@ class DefaultConversationManager extends ConversationManager {
122
121
 
123
122
  return { ...clinicalContext, symptoms };
124
123
  } catch (error) {
125
- logger.error('[DefaultConversationManager] Error fetching clinical context', { error: error.message });
124
+ logger.error('[DefaultMemoryManager] Error fetching clinical context', { error: error.message });
126
125
  return null;
127
126
  }
128
127
  }
@@ -201,10 +200,10 @@ class DefaultConversationManager extends ConversationManager {
201
200
  lastSymptoms: symptomParts.join('; ') || 'Sin síntomas reportados recientemente'
202
201
  };
203
202
  } catch (error) {
204
- logger.error('[DefaultConversationManager] Error getting clinical data', { error: error.message });
203
+ logger.error('[DefaultMemoryManager] Error getting clinical data', { error: error.message });
205
204
  return null;
206
205
  }
207
206
  }
208
207
  }
209
208
 
210
- module.exports = { DefaultConversationManager };
209
+ module.exports = { DefaultMemoryManager };
@@ -1,6 +1,6 @@
1
1
  const { logger } = require('../utils/logger');
2
2
 
3
- class ConversationManager {
3
+ class MemoryManager {
4
4
  constructor(options = {}) {
5
5
  this.memorySystem = options.memorySystem || null;
6
6
  }
@@ -36,8 +36,8 @@ class ConversationManager {
36
36
  }
37
37
 
38
38
  _logActivity(action, metadata = {}) {
39
- logger.info(`[ConversationManager] ${action}`, metadata);
39
+ logger.info(`[MemoryManager] ${action}`, metadata);
40
40
  }
41
41
  }
42
42
 
43
- module.exports = { ConversationManager };
43
+ module.exports = { MemoryManager };
@@ -4,7 +4,7 @@ const { retryWithBackoff } = require('../utils/retryHelper');
4
4
  const {
5
5
  handleFunctionCalls: handleFunctionCallsUtil,
6
6
  } = require('./OpenAIResponsesProviderTools');
7
- const { DefaultConversationManager } = require('../services/DefaultConversationManager');
7
+ const { DefaultMemoryManager } = require('../memory/DefaultMemoryManager');
8
8
  const { logger } = require('../utils/logger');
9
9
  const { getCurrentMexicoDateTime } = require('../utils/dateUtils');
10
10
 
@@ -42,7 +42,7 @@ class OpenAIResponsesProvider {
42
42
  };
43
43
 
44
44
  this.variant = 'responses';
45
- this.conversationManager = conversationManager || new DefaultConversationManager();
45
+ this.conversationManager = conversationManager || new DefaultMemoryManager();
46
46
 
47
47
  this.responses = this.client.responses;
48
48
  this.conversations = this.client.conversations;
@@ -197,6 +197,12 @@ class OpenAIResponsesProvider {
197
197
  }
198
198
  });
199
199
 
200
+ logger.info('[OpenAIResponsesProvider] Context built', {
201
+ conversationId,
202
+ assistantId,
203
+ context
204
+ });
205
+
200
206
  const filter = thread.code ? { code: thread.code, active: true } : null;
201
207
 
202
208
  // Get clinical context for prompt variables
@@ -1,6 +1,6 @@
1
1
  const { OpenAIAssistantsProvider } = require('./OpenAIAssistantsProvider');
2
2
  const { OpenAIResponsesProvider } = require('./OpenAIResponsesProvider');
3
- const { DefaultConversationManager } = require('../services/DefaultConversationManager');
3
+ const { DefaultMemoryManager } = require('../memory/DefaultMemoryManager');
4
4
  const { logger } = require('../utils/logger');
5
5
 
6
6
  const PROVIDER_VARIANTS = {
@@ -18,7 +18,7 @@ function createProvider(config = {}) {
18
18
  .toLowerCase();
19
19
 
20
20
  // Create conversation manager if not provided
21
- const conversationManager = config.conversationManager || new DefaultConversationManager({
21
+ const conversationManager = config.conversationManager || new DefaultMemoryManager({
22
22
  memorySystem: config.memorySystem
23
23
  });
24
24
 
@@ -10,10 +10,12 @@ const { Historial_Clinico_ID } = require('../config/airtableConfig');
10
10
  const { getCurRow, runAssistantWithRetries } = require('../helpers/assistantHelper.js');
11
11
  const { getThread } = require('../helpers/threadHelper.js');
12
12
  const { processThreadMessage } = require('../helpers/processHelper.js');
13
- const { getLastMessages, updateMessageRecord } = require('../helpers/messageHelper.js');
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 });
@@ -156,18 +158,18 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
156
158
  const finalThread = thread;
157
159
 
158
160
  const messagesStart = Date.now();
159
- const patientReply = await getLastMessages(code);
161
+ const lastMessage = await getLastNMessages(code, 1);
160
162
  timings.get_messages_ms = Date.now() - messagesStart;
161
163
 
162
- if (!patientReply) {
164
+ if (!lastMessage || lastMessage.length === 0 || lastMessage[0].from_me) {
163
165
  logger.info('[replyAssistantCore] No relevant data found for this assistant.');
164
166
  return null;
165
167
  }
166
168
 
167
169
  const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
168
- logger.info(`[replyAssistantCore] Processing ${patientReply.length} messages in parallel`);
170
+ logger.info(`[replyAssistantCore] Processing ${lastMessage.length} messages in parallel`);
169
171
  const processStart = Date.now();
170
- const processResult = await processThreadMessage(code, patientReply, provider);
172
+ const processResult = await processThreadMessage(code, lastMessage, provider);
171
173
 
172
174
  const { results: processResults, timings: processTimings } = processResult;
173
175
  timings.process_messages_ms = Date.now() - processStart;
@@ -186,10 +188,14 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
186
188
 
187
189
  const patientMsg = processResults.some(r => r.isPatient);
188
190
  const urls = processResults.filter(r => r.url).map(r => ({ url: r.url }));
189
- const allMessagesToAdd = processResults.flatMap(r => r.messages || []);
190
191
  const allTempFiles = processResults.flatMap(r => r.tempFiles || []);
191
192
 
192
- await Promise.all(processResults.map(r => updateMessageRecord(r.reply, finalThread)));
193
+ await Promise.all(processResults.map(r => {
194
+ const processedContent = r.messages && r.messages.length > 0
195
+ ? r.messages.map(msg => msg.content.text).join(' ')
196
+ : null;
197
+ return updateMessageRecord(r.reply, finalThread, processedContent);
198
+ }));
193
199
  await cleanupFiles(allTempFiles);
194
200
 
195
201
  if (urls.length > 0) {
@@ -217,15 +223,24 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
217
223
 
218
224
  const assistant = getAssistantById(finalThread.getAssistantId(), finalThread);
219
225
  const runStart = Date.now();
220
- const runResult = await runAssistantWithRetries(finalThread, assistant, runOptions, patientReply);
226
+ const runResult = await runAssistantWithRetries(finalThread, assistant, runOptions, lastMessage);
221
227
  timings.run_assistant_ms = Date.now() - runStart;
222
228
  timings.total_ms = Date.now() - startTotal;
223
229
 
224
- const { output, completed, retries, predictionTimeMs, tools_executed } = runResult;
230
+ const { output: rawOutput, completed, retries, predictionTimeMs, tools_executed } = runResult;
231
+
232
+ const output = sanitizeOutput(rawOutput);
233
+ if (rawOutput !== output) {
234
+ logger.debug('[replyAssistantCore] Output sanitized', {
235
+ originalLength: rawOutput?.length || 0,
236
+ sanitizedLength: output?.length || 0,
237
+ removedContent: rawOutput?.length ? 'brackets_removed' : 'none'
238
+ });
239
+ }
225
240
 
226
241
  logger.info('[Assistant Reply Complete]', {
227
242
  code: code ? `${code.substring(0, 3)}***${code.slice(-4)}` : 'unknown',
228
- messageCount: patientReply.length,
243
+ messageCount: lastMessage.length,
229
244
  hasMedia: urls.length > 0,
230
245
  retries,
231
246
  totalMs: timings.total_ms,
@@ -1,9 +1,11 @@
1
1
  const { MessageParser } = require('./messageParser');
2
2
  const { logger } = require('./logger');
3
3
  const { retryWithBackoff } = require('./retryHelper');
4
+ const { calculateDelay } = require('./scheduleUtils');
4
5
 
5
6
  module.exports = {
6
7
  MessageParser,
7
8
  logger,
8
9
  retryWithBackoff,
10
+ calculateDelay,
9
11
  };
@@ -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
+ };
@@ -0,0 +1,57 @@
1
+ const moment = require('moment-timezone');
2
+ const { logger } = require('./logger');
3
+
4
+ /**
5
+ * Calculate delay in milliseconds until a scheduled time
6
+ */
7
+ function calculateDelay(sendTime, timeZone) {
8
+ if (!sendTime) {
9
+ return 2500;
10
+ }
11
+
12
+ try {
13
+ if (timeZone && typeof sendTime === 'string') {
14
+ const sendMoment = moment.tz(sendTime, timeZone);
15
+
16
+ if (!sendMoment.isValid()) {
17
+ logger.warn('[calculateDelay] Invalid time format', { sendTime, timeZone });
18
+ return 2500;
19
+ }
20
+
21
+ const now = moment().tz(timeZone);
22
+ const randomDelay = Math.floor(Math.random() * 5001) + 10000;
23
+ const delay = sendMoment.diff(now) + randomDelay;
24
+
25
+ logger.debug('[calculateDelay] Timezone calculation', {
26
+ scheduledTime: sendMoment.format(),
27
+ currentTime: now.format(),
28
+ delayMinutes: delay / 60000,
29
+ timeZone
30
+ });
31
+
32
+ return Math.max(0, delay);
33
+ }
34
+
35
+ const now = new Date();
36
+ const targetTime = new Date(sendTime);
37
+
38
+ if (isNaN(targetTime.getTime())) {
39
+ logger.warn('[calculateDelay] Invalid date format', { sendTime });
40
+ return 2500;
41
+ }
42
+
43
+ return Math.max(0, targetTime.getTime() - now.getTime());
44
+
45
+ } catch (error) {
46
+ logger.error('[calculateDelay] Error calculating delay', {
47
+ error: error.message,
48
+ sendTime,
49
+ timeZone
50
+ });
51
+ return 2500;
52
+ }
53
+ }
54
+
55
+ module.exports = {
56
+ calculateDelay
57
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "3.0.4",
3
+ "version": "3.0.6",
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