@peopl-health/nexus 3.0.3 → 3.0.5

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;
@@ -7,6 +7,7 @@ const { sanitizeMediaFilename } = require('../utils/sanitizer');
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
@@ -43,9 +43,14 @@ class NexusMessaging {
43
43
  };
44
44
  // Message processing with check-after strategy
45
45
  this.processingLocks = new Map(); // Per-chat locks to prevent parallel processing
46
+ this.activeRequests = new Map(); // Track active AI requests per chat
47
+ this.abandonedRuns = new Set(); // Track runs that should be ignored
46
48
  this.batchingConfig = {
47
49
  enabled: config.messageBatching?.enabled ?? true, // Enabled by default with check-after
48
- checkDelayMs: config.messageBatching?.checkDelayMs ?? 100 // Delay before checking for new messages
50
+ abortOnNewMessage: config.messageBatching?.abortOnNewMessage ?? true, // Abort ongoing AI calls when new messages arrive
51
+ immediateRestart: config.messageBatching?.immediateRestart ?? true, // Start new processing immediately without waiting
52
+ batchWindowMs: config.messageBatching?.batchWindowMs ?? 2000, // Wait up to 2s for message bursts
53
+ maxBatchWait: config.messageBatching?.maxBatchWait ?? 5000 // Maximum time to wait for batching
49
54
  };
50
55
  }
51
56
 
@@ -656,6 +661,20 @@ class NexusMessaging {
656
661
 
657
662
  if (this.processingLocks.has(chatId)) {
658
663
  logger.info(`[CheckAfter] Already processing ${chatId}, new message will be included`);
664
+
665
+ if (this.batchingConfig.abortOnNewMessage && this.activeRequests.has(chatId)) {
666
+ const runId = this.activeRequests.get(chatId);
667
+ this.abandonedRuns.add(runId);
668
+ logger.info(`[CheckAfter] Marked run ${runId} as abandoned for ${chatId}`);
669
+
670
+ if (this.batchingConfig.immediateRestart) {
671
+ this.processingLocks.delete(chatId);
672
+ this.activeRequests.delete(chatId);
673
+
674
+ logger.info(`[CheckAfter] Starting immediate reprocessing for ${chatId}`);
675
+ await this._processWithLock(chatId, null);
676
+ }
677
+ }
659
678
  return;
660
679
  }
661
680
 
@@ -668,33 +687,53 @@ class NexusMessaging {
668
687
  async _processWithLock(chatId, existingTypingInterval = null) {
669
688
  this.processingLocks.set(chatId, true);
670
689
  let typingInterval = existingTypingInterval;
690
+ let runId = null;
671
691
 
672
692
  try {
673
693
  if (!typingInterval) {
674
694
  typingInterval = await this._startTypingRefresh(chatId);
675
695
  }
676
- logger.info(`[CheckAfter] Processing messages for ${chatId}`);
696
+
697
+ const startTime = Date.now();
698
+ let messageCount = await this._getUnprocessedMessageCount(chatId);
699
+ let lastCount = messageCount;
700
+
701
+ while (Date.now() - startTime < this.batchingConfig.batchWindowMs) {
702
+ await new Promise(resolve => setTimeout(resolve, 500));
703
+ const newCount = await this._getUnprocessedMessageCount(chatId);
704
+
705
+ if (newCount > lastCount) {
706
+ lastCount = newCount;
707
+ logger.info(`[Batching] New message detected for ${chatId}, extending wait`);
708
+ }
709
+
710
+ if (Date.now() - startTime >= this.batchingConfig.maxBatchWait) {
711
+ logger.info(`[Batching] Max wait reached for ${chatId}`);
712
+ break;
713
+ }
714
+ }
715
+
716
+ logger.info(`[CheckAfter] Processing ${lastCount} messages for ${chatId} after batching`);
717
+
718
+ runId = `run_${Date.now()}_${Math.random().toString(36).substring(7)}`;
719
+ this.activeRequests.set(chatId, runId);
677
720
 
678
- const result = await replyAssistant(chatId);
721
+ const result = await replyAssistant(chatId, null, null, { runId });
722
+
723
+ if (this.abandonedRuns.has(runId)) {
724
+ logger.info(`[CheckAfter] Discarding abandoned run ${runId} for ${chatId}`);
725
+ this.abandonedRuns.delete(runId);
726
+ return;
727
+ }
728
+
679
729
  const botResponse = typeof result === 'string' ? result : result?.output;
680
730
  const tools_executed = typeof result === 'object' ? result?.tools_executed : undefined;
681
731
 
682
- // Small delay to catch very recent DB writes
683
- await new Promise(resolve => setTimeout(resolve, this.batchingConfig.checkDelayMs));
684
-
685
- // Check for new unprocessed messages
686
- const hasNewMessages = await Message.exists({
687
- numero: chatId,
688
- processed: false,
689
- from_me: false
690
- });
691
-
692
- if (hasNewMessages) {
693
- logger.info(`[CheckAfter] New messages detected for ${chatId}, discarding response and reprocessing`);
694
- return await this._processWithLock(chatId, typingInterval);
732
+ if (typingInterval) {
733
+ clearInterval(typingInterval);
734
+ typingInterval = null;
695
735
  }
696
736
 
697
- // No new messages - send response and mark processed
698
737
  if (botResponse) {
699
738
  await this.sendMessage({
700
739
  code: chatId,
@@ -712,9 +751,25 @@ class NexusMessaging {
712
751
  } finally {
713
752
  if (typingInterval) clearInterval(typingInterval);
714
753
  this.processingLocks.delete(chatId);
754
+ this.activeRequests.delete(chatId);
755
+ if (this.abandonedRuns.size > 100) {
756
+ this.abandonedRuns.clear();
757
+ }
715
758
  }
716
759
  }
717
760
 
761
+ /**
762
+ * Get count of unprocessed messages for a chat
763
+ */
764
+ async _getUnprocessedMessageCount(chatId) {
765
+ const { Message } = require('../models/messageModel');
766
+ return await Message.countDocuments({
767
+ numero: chatId,
768
+ processed: false,
769
+ from_me: false
770
+ });
771
+ }
772
+
718
773
  /**
719
774
  * Start typing indicator refresh interval
720
775
  */
@@ -726,10 +781,16 @@ class NexusMessaging {
726
781
  const lastMessage = await Message.findOne({
727
782
  numero: chatId,
728
783
  from_me: false,
729
- message_id: { $exists: true, $ne: null }
784
+ processed: false,
785
+ message_id: { $exists: true, $ne: null, $not: /^pending-/ }
730
786
  }).sort({ createdAt: -1 });
731
787
 
732
- if (!lastMessage?.message_id) return null;
788
+ if (!lastMessage?.message_id) {
789
+ logger.debug(`[_startTypingRefresh] No valid message for typing indicator: ${chatId}`);
790
+ return null;
791
+ }
792
+
793
+ logger.debug(`[_startTypingRefresh] Starting typing indicator for message: ${lastMessage.message_id}`);
733
794
 
734
795
  return setInterval(() =>
735
796
  this.provider.sendTypingIndicator(lastMessage.message_id).catch(err =>
@@ -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;
@@ -35,23 +35,19 @@ const logger = {
35
35
 
36
36
  /**
37
37
  * Dedicated message processing utilities
38
- * Handles text messages, media files, audio transcription, and thread operations
39
38
  */
40
39
  const processTextMessage = (reply) => {
41
- const formattedMessage = formatMessage(reply);
42
40
  logger.info('processTextMessage', {
43
41
  message_id: reply.message_id,
44
42
  timestamp: reply.timestamp,
45
43
  from_me: reply.from_me,
46
44
  body: reply.body,
47
- hasContent: !!formattedMessage,
48
- formattedMessage
45
+ hasContent: !!reply.body
49
46
  });
50
- logger.debug('processTextMessage_content', { formattedMessage });
51
47
 
52
48
  const messagesChat = [];
53
- if (formattedMessage) {
54
- messagesChat.push({ type: 'text', text: formattedMessage });
49
+ if (reply.body) {
50
+ messagesChat.push({ type: 'text', text: reply.body });
55
51
  }
56
52
 
57
53
  return messagesChat;
@@ -121,8 +117,6 @@ const processImageFileCore = async (fileName, reply) => {
121
117
  ...timings
122
118
  });
123
119
 
124
- logger.debug('processImageFile_analysis', { imageAnalysis });
125
-
126
120
  } catch (error) {
127
121
  logger.error('processImageFile', error, {
128
122
  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,8 +4,9 @@ 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
+ const { getCurrentMexicoDateTime } = require('../utils/dateUtils');
9
10
 
10
11
  const CONVERSATION_PREFIX = 'conv_';
11
12
  const RESPONSE_PREFIX = 'resp_';
@@ -41,7 +42,7 @@ class OpenAIResponsesProvider {
41
42
  };
42
43
 
43
44
  this.variant = 'responses';
44
- this.conversationManager = conversationManager || new DefaultConversationManager();
45
+ this.conversationManager = conversationManager || new DefaultMemoryManager();
45
46
 
46
47
  this.responses = this.client.responses;
47
48
  this.conversations = this.client.conversations;
@@ -196,13 +197,20 @@ class OpenAIResponsesProvider {
196
197
  }
197
198
  });
198
199
 
200
+ logger.info('[OpenAIResponsesProvider] Context built', {
201
+ conversationId,
202
+ assistantId,
203
+ context
204
+ });
205
+
199
206
  const filter = thread.code ? { code: thread.code, active: true } : null;
200
207
 
201
208
  // Get clinical context for prompt variables
202
209
  const clinicalData = await this.conversationManager.getClinicalData(thread.code);
203
210
  const promptVariables = clinicalData ? {
204
211
  clinical_context: clinicalData.clinicalContext || '',
205
- last_symptoms: clinicalData.lastSymptoms || ''
212
+ last_symptoms: clinicalData.lastSymptoms || '',
213
+ current_date: getCurrentMexicoDateTime(),
206
214
  } : null;
207
215
 
208
216
  // Execute with built context
@@ -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,7 +10,7 @@ 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
  const { logger } = require('../utils/logger');
@@ -156,18 +156,18 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
156
156
  const finalThread = thread;
157
157
 
158
158
  const messagesStart = Date.now();
159
- const patientReply = await getLastMessages(code);
159
+ const lastMessage = await getLastNMessages(code, 1);
160
160
  timings.get_messages_ms = Date.now() - messagesStart;
161
161
 
162
- if (!patientReply) {
162
+ if (!lastMessage || lastMessage.length === 0 || lastMessage[0].from_me) {
163
163
  logger.info('[replyAssistantCore] No relevant data found for this assistant.');
164
164
  return null;
165
165
  }
166
166
 
167
167
  const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
168
- logger.info(`[replyAssistantCore] Processing ${patientReply.length} messages in parallel`);
168
+ logger.info(`[replyAssistantCore] Processing ${lastMessage.length} messages in parallel`);
169
169
  const processStart = Date.now();
170
- const processResult = await processThreadMessage(code, patientReply, provider);
170
+ const processResult = await processThreadMessage(code, lastMessage, provider);
171
171
 
172
172
  const { results: processResults, timings: processTimings } = processResult;
173
173
  timings.process_messages_ms = Date.now() - processStart;
@@ -186,10 +186,14 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
186
186
 
187
187
  const patientMsg = processResults.some(r => r.isPatient);
188
188
  const urls = processResults.filter(r => r.url).map(r => ({ url: r.url }));
189
- const allMessagesToAdd = processResults.flatMap(r => r.messages || []);
190
189
  const allTempFiles = processResults.flatMap(r => r.tempFiles || []);
191
190
 
192
- await Promise.all(processResults.map(r => updateMessageRecord(r.reply, finalThread)));
191
+ await Promise.all(processResults.map(r => {
192
+ const processedContent = r.messages && r.messages.length > 0
193
+ ? r.messages.map(msg => msg.content.text).join(' ')
194
+ : null;
195
+ return updateMessageRecord(r.reply, finalThread, processedContent);
196
+ }));
193
197
  await cleanupFiles(allTempFiles);
194
198
 
195
199
  if (urls.length > 0) {
@@ -217,7 +221,7 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
217
221
 
218
222
  const assistant = getAssistantById(finalThread.getAssistantId(), finalThread);
219
223
  const runStart = Date.now();
220
- const runResult = await runAssistantWithRetries(finalThread, assistant, runOptions, patientReply);
224
+ const runResult = await runAssistantWithRetries(finalThread, assistant, runOptions, lastMessage);
221
225
  timings.run_assistant_ms = Date.now() - runStart;
222
226
  timings.total_ms = Date.now() - startTotal;
223
227
 
@@ -225,7 +229,7 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
225
229
 
226
230
  logger.info('[Assistant Reply Complete]', {
227
231
  code: code ? `${code.substring(0, 3)}***${code.slice(-4)}` : 'unknown',
228
- messageCount: patientReply.length,
232
+ messageCount: lastMessage.length,
229
233
  hasMedia: urls.length > 0,
230
234
  retries,
231
235
  totalMs: timings.total_ms,
@@ -18,9 +18,17 @@ const dateAndTimeFromStart = (startTime) => {
18
18
  };
19
19
  };
20
20
 
21
+ const getCurrentMexicoDateTime = () => {
22
+ return moment()
23
+ .tz('America/Mexico_City')
24
+ .locale('es')
25
+ .format('dddd, D [de] MMMM [de] YYYY [a las] h:mm A');
26
+ };
27
+
21
28
  module.exports = {
22
29
  ISO_DATE,
23
30
  parseStartTime,
24
31
  addDays,
25
- dateAndTimeFromStart
32
+ dateAndTimeFromStart,
33
+ getCurrentMexicoDateTime
26
34
  };
@@ -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,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.3",
3
+ "version": "3.0.5",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",