@peopl-health/nexus 3.1.2 → 3.1.4

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.
@@ -198,9 +198,12 @@ class BaseAssistant {
198
198
  // Messages with from_me: true are assistant messages, from_me: false are user messages
199
199
  const formattedMessages = messagesInOrder
200
200
  .filter(message => message && message.timestamp && message.body && message.body.trim() !== '')
201
- .map(message => {
202
- const formattedText = formatMessage(message);
203
- return formattedText ? { role: message.from_me ? 'assistant' : 'user', content: formattedText } : null;
201
+ .flatMap(message => {
202
+ const formattedTexts = formatMessage(message);
203
+ return formattedTexts.map(text => ({
204
+ role: message.from_me ? 'assistant' : 'user',
205
+ content: text
206
+ }));
204
207
  })
205
208
  .filter(message => message !== null);
206
209
 
@@ -480,7 +480,7 @@ class NexusMessaging {
480
480
  return;
481
481
  }
482
482
 
483
- const result = await replyAssistant(from, body);
483
+ const result = await this._processMessages(from, () => replyAssistant(from, body));
484
484
  const response = typeof result === 'string' ? result : result?.output;
485
485
  const tools_executed = typeof result === 'object' ? result?.tools_executed : undefined;
486
486
 
@@ -547,7 +547,7 @@ class NexusMessaging {
547
547
  ? body
548
548
  : `Media received (${mediaDescriptor || 'attachment'})`;
549
549
 
550
- const result = await replyAssistant(from, fallbackMessage);
550
+ const result = await this._processMessages(from, () => replyAssistant(from, fallbackMessage));
551
551
  const response = typeof result === 'string' ? result : result?.output;
552
552
  const tools_executed = typeof result === 'object' ? result?.tools_executed : undefined;
553
553
 
@@ -718,7 +718,7 @@ class NexusMessaging {
718
718
  runId = `run_${Date.now()}_${Math.random().toString(36).substring(7)}`;
719
719
  this.activeRequests.set(chatId, runId);
720
720
 
721
- const result = await replyAssistant(chatId, null, null, { runId });
721
+ const result = await this._processMessages(chatId, () => replyAssistant(chatId, null, null, { runId }));
722
722
 
723
723
  if (this.abandonedRuns.has(runId)) {
724
724
  logger.info(`[CheckAfter] Discarding abandoned run ${runId} for ${chatId}`);
@@ -732,6 +732,7 @@ class NexusMessaging {
732
732
  if (typingInterval) {
733
733
  clearInterval(typingInterval);
734
734
  typingInterval = null;
735
+ await new Promise(resolve => setTimeout(resolve, 100));
735
736
  }
736
737
 
737
738
  if (botResponse) {
@@ -743,7 +744,7 @@ class NexusMessaging {
743
744
  tools_executed
744
745
  });
745
746
  }
746
-
747
+
747
748
  this.events.emit('messages:processed', { chatId, response: botResponse });
748
749
 
749
750
  } catch (error) {
@@ -762,7 +763,6 @@ class NexusMessaging {
762
763
  * Get count of unprocessed messages for a chat
763
764
  */
764
765
  async _getUnprocessedMessageCount(chatId) {
765
- const { Message } = require('../models/messageModel');
766
766
  return await Message.countDocuments({
767
767
  numero: chatId,
768
768
  processed: false,
@@ -770,6 +770,34 @@ class NexusMessaging {
770
770
  });
771
771
  }
772
772
 
773
+ /**
774
+ * Central processing pipeline - handles assistant processing, preprocessing, and status updates
775
+ */
776
+ async _processMessages(chatId, processingFn) {
777
+ const unprocessedMessages = await Message.find({
778
+ numero: chatId,
779
+ from_me: false,
780
+ processed: false
781
+ }).select('_id');
782
+
783
+ try {
784
+ const result = await processingFn();
785
+
786
+ if (unprocessedMessages.length > 0) {
787
+ await Message.updateMany(
788
+ { _id: { $in: unprocessedMessages.map(m => m._id) } },
789
+ { $set: { processed: true } }
790
+ );
791
+ logger.info(`[_processMessages] Marked ${unprocessedMessages.length} specific messages as processed for ${chatId}`);
792
+ }
793
+
794
+ return result;
795
+ } catch (error) {
796
+ logger.debug(`[_processMessages] Processing failed, messages remain unprocessed for ${chatId}`);
797
+ throw error;
798
+ }
799
+ }
800
+
773
801
  /**
774
802
  * Start typing indicator refresh interval
775
803
  */
@@ -781,16 +809,16 @@ class NexusMessaging {
781
809
  const lastMessage = await Message.findOne({
782
810
  numero: chatId,
783
811
  from_me: false,
784
- message_id: { $exists: true, $ne: null, $not: /^pending-/ },
785
- createdAt: { $gte: new Date(Date.now() - 24 * 60 * 60 * 1000) }
812
+ processed: false,
813
+ message_id: { $exists: true, $ne: null, $not: /^pending-/ }
786
814
  }).sort({ createdAt: -1 });
787
815
 
788
816
  if (!lastMessage?.message_id) {
789
- logger.debug(`[_startTypingRefresh] No valid message for typing indicator: ${chatId}`);
817
+ logger.debug(`[_startTypingRefresh] No unprocessed message for typing indicator: ${chatId}`);
790
818
  return null;
791
819
  }
792
820
 
793
- logger.debug(`[_startTypingRefresh] Starting typing indicator for message: ${lastMessage.message_id}`);
821
+ logger.debug(`[_startTypingRefresh] Starting typing indicator for unprocessed message: ${lastMessage.message_id}`);
794
822
 
795
823
  return setInterval(() =>
796
824
  this.provider.sendTypingIndicator(lastMessage.message_id).catch(err =>
@@ -171,7 +171,7 @@ async function downloadMediaAndCreateFile(code, reply) {
171
171
  if (!resultMedia) return [];
172
172
 
173
173
  if (!resultMedia.media || !resultMedia.media.key) {
174
- logger.info('[downloadMediaAndCreateFile] No valid media found for message:', reply.message_id);
174
+ logger.info('[downloadMediaAndCreateFile] No valid media found for message:', { messageId: reply.message_id });
175
175
  return [];
176
176
  }
177
177
 
@@ -2,31 +2,34 @@ 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, processedContent = null) => {
5
+ /**
6
+ * Store processed media content (transcriptions, image analysis) before assistant runs
7
+ */
8
+ const storeProcessedContent = async (reply, thread, processedContent) => {
9
+ if (!processedContent || !reply.media) {
10
+ return;
11
+ }
12
+
6
13
  const threadId = thread.getConversationId();
7
14
 
8
- const updateData = {
15
+ const updateData = {
9
16
  assistant_id: thread.getAssistantId(),
10
17
  thread_id: threadId,
11
- processed: true
12
- };
13
-
14
- if (processedContent && reply.media) {
15
- updateData.media = {
18
+ media: {
16
19
  ...reply.media,
17
20
  metadata: {
18
21
  ...(reply.media.metadata || {}),
19
- processed_content: processedContent
22
+ processedContent
20
23
  }
21
- };
22
- }
24
+ }
25
+ };
23
26
 
24
27
  await Message.updateOne(
25
28
  { message_id: reply.message_id, timestamp: reply.timestamp },
26
29
  { $set: updateData }
27
30
  );
28
31
 
29
- logger.info(`[updateMessageRecord] Record updated - ID: ${reply.message_id}, Thread: ${threadId}, Processed: true, MediaContentUpdated: ${!!(processedContent && reply.media)}`);
32
+ logger.info(`[storeProcessedContent] Media content stored - ID: ${reply.message_id}`);
30
33
  };
31
34
 
32
35
 
@@ -79,13 +82,13 @@ async function getLastNMessages(code, n) {
79
82
  function formatMessage(reply) {
80
83
  try {
81
84
  if (!reply.createdAt) {
82
- return null;
85
+ return [];
83
86
  }
84
87
 
85
88
  const msgDate = new Date(reply.createdAt);
86
89
  if (isNaN(msgDate.getTime())) {
87
90
  logger.warn(`[formatMessage] Invalid timestamp for message ID: ${reply.message_id}`);
88
- return reply.body;
91
+ return reply.body ? [`[Invalid timestamp] ${reply.body}`] : [];
89
92
  }
90
93
 
91
94
  const mexicoCityTime = moment(msgDate)
@@ -93,15 +96,36 @@ function formatMessage(reply) {
93
96
  .locale('es')
94
97
  .format('dddd, D [de] MMMM [de] YYYY [a las] h:mm A');
95
98
 
96
- let messageContent = reply.body;
97
- if (reply.media?.metadata?.processed_content) {
98
- messageContent = reply.media.metadata.processed_content;
99
+ const messages = [];
100
+
101
+ const generateMediaCode = (messageId, mediaType) => {
102
+ if (!messageId || !mediaType) return null;
103
+ const shortId = messageId.slice(-6);
104
+ return `${mediaType.toUpperCase()}-${shortId}`;
105
+ };
106
+
107
+ if (reply.body && reply.body.trim()) {
108
+ const mediaCode = reply.media?.mediaType ?
109
+ generateMediaCode(reply.message_id, reply.media.mediaType) : null;
110
+ const mediaIndicator = mediaCode ? `[${mediaCode}] ` : '';
111
+ messages.push(`[${mexicoCityTime}] ${mediaIndicator}${reply.body}`);
112
+ }
113
+
114
+ if (reply.media?.metadata?.processedContent &&
115
+ reply.media.metadata.processedContent !== reply.body) {
116
+ const mediaCode = generateMediaCode(reply.message_id, reply.media.mediaType);
117
+ const processingType = reply.media.mediaType === 'audio' ? 'TRANSCRIPTION' :
118
+ reply.media.mediaType === 'image' ? 'ANALYSIS' :
119
+ reply.media.mediaType === 'document' ? 'DOCUMENT_ANALYSIS' :
120
+ 'PROCESSED';
121
+ const indicator = mediaCode ? `[${mediaCode} ${processingType}]` : `[${processingType}]`;
122
+ messages.push(`[${mexicoCityTime}] ${indicator} ${reply.media.metadata.processedContent}`);
99
123
  }
100
124
 
101
- return `[${mexicoCityTime}] ${messageContent}`;
125
+ return messages.length > 0 ? messages : [];
102
126
  } catch (error) {
103
127
  logger.error(`[formatMessage] Error for message ID: ${reply.message_id}:`, error?.message || String(error));
104
- return null;
128
+ return [];
105
129
  }
106
130
  }
107
131
 
@@ -117,7 +141,7 @@ async function isRecentMessage(chatId) {
117
141
  }
118
142
 
119
143
  module.exports = {
120
- updateMessageRecord,
144
+ storeProcessedContent,
121
145
  getLastMessages,
122
146
  getLastNMessages,
123
147
  formatMessage,
@@ -299,8 +299,8 @@ const processThreadMessageCore = async (code, replies, provider) => {
299
299
  const textMessages = processTextMessage(reply);
300
300
  const mediaResult = await processMediaFiles(code, reply, provider);
301
301
 
302
- const { messagesChat: mediaMessages, url, tempFiles: mediaFiles, timings: mediaTimings } = mediaResult;
303
- tempFiles = mediaFiles;
302
+ const { messagesChat: mediaMessages, url, tempFiles, timings: mediaTimings } = mediaResult;
303
+ console.log('[processThreadMessageCore] tempFiles', tempFiles);
304
304
 
305
305
  if (mediaTimings) {
306
306
  timings.download_ms += mediaTimings.download_ms || 0;
@@ -321,7 +321,7 @@ const processThreadMessageCore = async (code, replies, provider) => {
321
321
  hasMedia: !!reply.media,
322
322
  hasUrl: !!url
323
323
  });
324
-
324
+ await cleanupFiles(tempFiles);
325
325
  return { isPatient, url, messages, reply, tempFiles };
326
326
  } catch (error) {
327
327
  logger.error('processThreadMessage', error, {
@@ -11,7 +11,7 @@ class DefaultMemoryManager extends MemoryManager {
11
11
  this.maxHistoricalMessages = parseInt(process.env.MAX_HISTORICAL_MESSAGES || '50', 10);
12
12
  }
13
13
 
14
- async buildContext({ thread, assistant, config = {} }) {
14
+ async buildContext({ thread, config = {} }) {
15
15
  this._logActivity('Building context', { threadCode: thread.code });
16
16
 
17
17
  try {
@@ -22,14 +22,14 @@ class DefaultMemoryManager extends MemoryManager {
22
22
  return additionalMessages;
23
23
  }
24
24
 
25
- const messageContext = allMessages.reverse().map(msg => {
26
- const formattedContent = formatMessage(msg);
27
- return {
25
+ const messageContext = allMessages.reverse().flatMap(msg => {
26
+ const formattedContents = formatMessage(msg);
27
+ return formattedContents.map(content => ({
28
28
  role: msg.origin === 'patient' ? 'user' : 'assistant',
29
- content: formattedContent || msg.body || msg.content || ''
30
- };
31
- });
32
-
29
+ content: content || msg.body || msg.content || ''
30
+ }));
31
+ }).filter(msg => msg.content);
32
+
33
33
  return [...additionalMessages, ...messageContext];
34
34
  } catch (error) {
35
35
  logger.error('[DefaultMemoryManager] Context building failed', {
@@ -5,7 +5,7 @@ class MemoryManager {
5
5
  this.memorySystem = options.memorySystem || null;
6
6
  }
7
7
 
8
- async buildContext({ thread, assistant, config = {} }) {
8
+ async buildContext({ thread, config = {} }) {
9
9
  throw new Error('buildContext must be implemented by subclass');
10
10
  }
11
11
 
@@ -185,7 +185,6 @@ class OpenAIResponsesProvider {
185
185
  // Delegate context building to conversation manager
186
186
  const context = await this.conversationManager.buildContext({
187
187
  thread,
188
- assistant,
189
188
  config: {
190
189
  ...config,
191
190
  threadId: conversationId,
@@ -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 { getLastNMessages, updateMessageRecord } = require('../helpers/messageHelper.js');
13
+ const { getLastNMessages, storeProcessedContent } = require('../helpers/messageHelper.js');
14
14
  const { combineImagesToPDF, cleanupFiles } = require('../helpers/filesHelper.js');
15
15
  const { getAssistantById } = require('./assistantResolver');
16
16
 
@@ -197,8 +197,9 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
197
197
  .map(msg => msg.content.text)
198
198
  .join(' ')
199
199
  : null;
200
- return updateMessageRecord(r.reply, finalThread, processedContent);
201
- }));
200
+ return r.reply ? storeProcessedContent(r.reply, finalThread, processedContent) : null;
201
+ }).filter(Boolean));
202
+
202
203
  await cleanupFiles(allTempFiles);
203
204
 
204
205
  if (urls.length > 0) {
@@ -218,7 +219,7 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
218
219
  }
219
220
 
220
221
  if (processedFiles && processedFiles.length) {
221
- cleanupFiles(processedFiles);
222
+ await cleanupFiles(processedFiles);
222
223
  }
223
224
  }
224
225
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "3.1.2",
3
+ "version": "3.1.4",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",