@peopl-health/nexus 3.1.3 → 3.1.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.
@@ -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
 
@@ -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) {
@@ -770,7 +771,7 @@ class NexusMessaging {
770
771
  }
771
772
 
772
773
  /**
773
- * Central processing pipeline - handles status updates for any processing function
774
+ * Central processing pipeline - handles assistant processing, preprocessing, and status updates
774
775
  */
775
776
  async _processMessages(chatId, processingFn) {
776
777
  const unprocessedMessages = await Message.find({
@@ -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
 
@@ -190,6 +190,16 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
190
190
  const urls = processResults.filter(r => r.url).map(r => ({ url: r.url }));
191
191
  const allTempFiles = processResults.flatMap(r => r.tempFiles || []);
192
192
 
193
+ await Promise.all(processResults.map(r => {
194
+ const processedContent = r.messages && r.messages.length > 0
195
+ ? r.messages
196
+ .filter(msg => msg.content.text !== r.reply?.body)
197
+ .map(msg => msg.content.text)
198
+ .join(' ')
199
+ : null;
200
+ return r.reply ? storeProcessedContent(r.reply, finalThread, processedContent) : null;
201
+ }).filter(Boolean));
202
+
193
203
  await cleanupFiles(allTempFiles);
194
204
 
195
205
  if (urls.length > 0) {
@@ -209,7 +219,7 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
209
219
  }
210
220
 
211
221
  if (processedFiles && processedFiles.length) {
212
- cleanupFiles(processedFiles);
222
+ await cleanupFiles(processedFiles);
213
223
  }
214
224
  }
215
225
 
@@ -7,17 +7,43 @@ function removeBracketContent(text) {
7
7
  return text.replace(/\[([^\]]*)\]/g, '').trim();
8
8
  }
9
9
 
10
+ function formatForWhatsApp(text) {
11
+ if (!text || typeof text !== 'string') return text;
12
+
13
+ let formatted = text;
14
+
15
+ // Add line breaks after dash items (bullet points)
16
+ formatted = formatted.replace(/(\.\s*-\s+)/g, '.\n\n- ');
17
+ formatted = formatted.replace(/(:)\s*(-\s+)/g, '$1\n- ');
18
+
19
+ // Add line breaks after semicolons when they separate items
20
+ formatted = formatted.replace(/;\s*-\s+/g, ';\n- ');
21
+
22
+ // Add line breaks after periods that end sentences before dashes
23
+ formatted = formatted.replace(/(\.\s+)(-\s+)/g, '$1\n$2');
24
+
25
+ // Convert dashes to bullet points
26
+ formatted = formatted.replace(/^-\s+/gm, '• ');
27
+ formatted = formatted.replace(/(\n|^)\s*-\s+/g, '$1• ');
28
+
29
+ // Clean up multiple consecutive line breaks
30
+ formatted = formatted.replace(/\n{3,}/g, '\n\n');
31
+
32
+ return formatted.trim();
33
+ }
34
+
10
35
  function sanitizeOutput(text) {
11
36
  if (!text || typeof text !== 'string') return text;
12
37
 
13
38
  let sanitized = text;
14
39
  sanitized = removeBracketContent(sanitized);
15
- sanitized = sanitized.replace(/\s+/g, ' ').trim();
40
+ sanitized = formatForWhatsApp(sanitized);
16
41
 
17
42
  return sanitized;
18
43
  }
19
44
 
20
45
  module.exports = {
21
46
  sanitizeOutput,
22
- removeBracketContent
47
+ removeBracketContent,
48
+ formatForWhatsApp
23
49
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "3.1.3",
3
+ "version": "3.1.5",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",