@peopl-health/nexus 2.7.0 → 2.8.0-fix.1

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,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');
@@ -24,53 +23,86 @@ function convertTwilioToInternalFormat(twilioMessage) {
24
23
  }
25
24
 
26
25
 
27
- async function downloadMediaFromTwilio(mediaUrl, logger) {
28
- try {
29
- const authHeader = `Basic ${Buffer.from(
30
- `${process.env.TWILIO_ACCOUNT_SID}:${process.env.TWILIO_AUTH_TOKEN}`
31
- ).toString('base64')}`;
32
-
33
- logger.info('[TwilioMedia] Starting download', {
34
- url: mediaUrl,
35
- hasAccountSid: !!process.env.TWILIO_ACCOUNT_SID,
36
- hasAuthToken: !!process.env.TWILIO_AUTH_TOKEN,
37
- accountSidLength: process.env.TWILIO_ACCOUNT_SID?.length || 0,
38
- authHeaderSample: authHeader.substring(0, 20) + '...'
39
- });
40
-
41
- const response = await axios({
42
- method: 'GET',
43
- url: mediaUrl,
44
- responseType: 'arraybuffer',
45
- timeout: 30000,
46
- headers: {
47
- 'Authorization': authHeader
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
+ });
51
+
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
+ });
59
+
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;
48
75
  }
49
- });
50
-
51
- logger.info('[TwilioMedia] Download successful', {
52
- status: response.status,
53
- contentType: response.headers['content-type'],
54
- contentLength: response.headers['content-length'],
55
- dataSize: response.data?.length || 0
56
- });
57
-
58
- return Buffer.from(response.data);
59
- } catch (error) {
60
- logger.error('[TwilioMedia] Download failed', {
61
- message: error.message,
62
- status: error.response?.status,
63
- statusText: error.response?.statusText,
64
- responseHeaders: error.response?.headers,
65
- responseData: error.response?.data ? error.response.data.toString().substring(0, 500) : null,
66
- url: mediaUrl,
67
- hasCredentials: !!(process.env.TWILIO_ACCOUNT_SID && process.env.TWILIO_AUTH_TOKEN)
68
- });
69
- throw error;
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)
99
+ });
100
+
101
+ throw error;
102
+ }
70
103
  }
71
104
  }
72
105
 
73
-
74
106
  function getMediaTypeFromContentType(contentType) {
75
107
  if (contentType.startsWith('image/')) return 'imageMessage';
76
108
  if (contentType.startsWith('audio/')) return 'audioMessage';
@@ -89,16 +121,6 @@ function extractTitle(message, mediaType) {
89
121
  }
90
122
 
91
123
 
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
124
  async function downloadMedia(twilioMessage, logger) {
103
125
  try {
104
126
  const mediaUrl = twilioMessage.MediaUrl0;
@@ -145,7 +167,6 @@ module.exports = {
145
167
  getMediaTypeFromContentType,
146
168
  extractTitle,
147
169
  isRecentMessage,
148
- getLastMessages,
149
170
  downloadMedia,
150
171
  ensureWhatsAppFormat
151
172
  };
@@ -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');
2
- const { getLastNMessages } = require('../helpers/messageHelper');
1
+ const { MemoryManager } = require('./MemoryManager');
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,19 +18,21 @@ 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
  }
25
24
 
26
- const messageContext = allMessages.reverse().map(msg => ({
27
- role: msg.origin === 'patient' ? 'user' : 'assistant',
28
- content: msg.body || msg.content || ''
29
- }));
25
+ const messageContext = allMessages.reverse().map(msg => {
26
+ const formattedContent = formatMessage(msg);
27
+ return {
28
+ role: msg.origin === 'patient' ? 'user' : 'assistant',
29
+ content: formattedContent || msg.body || msg.content || ''
30
+ };
31
+ });
30
32
 
31
33
  return [...additionalMessages, ...messageContext];
32
34
  } catch (error) {
33
- logger.error('[DefaultConversationManager] Context building failed', {
35
+ logger.error('[DefaultMemoryManager] Context building failed', {
34
36
  threadCode: thread.code,
35
37
  error: error.message
36
38
  });
@@ -50,7 +52,7 @@ class DefaultConversationManager extends ConversationManager {
50
52
  responseId: response.id
51
53
  });
52
54
  } catch (error) {
53
- logger.error('[DefaultConversationManager] Response processing failed', {
55
+ logger.error('[DefaultMemoryManager] Response processing failed', {
54
56
  threadCode: thread.code,
55
57
  error: error.message
56
58
  });
@@ -63,7 +65,7 @@ class DefaultConversationManager extends ConversationManager {
63
65
  try {
64
66
  return await handlePendingFunctionCallsUtil(assistant, conversationMessages, toolMetadata);
65
67
  } catch (error) {
66
- logger.error('[DefaultConversationManager] Function call handling failed', { error: error.message });
68
+ logger.error('[DefaultMemoryManager] Function call handling failed', { error: error.message });
67
69
  return { outputs: [], toolsExecuted: [] };
68
70
  }
69
71
  }
@@ -119,7 +121,7 @@ class DefaultConversationManager extends ConversationManager {
119
121
 
120
122
  return { ...clinicalContext, symptoms };
121
123
  } catch (error) {
122
- logger.error('[DefaultConversationManager] Error fetching clinical context', { error: error.message });
124
+ logger.error('[DefaultMemoryManager] Error fetching clinical context', { error: error.message });
123
125
  return null;
124
126
  }
125
127
  }
@@ -198,10 +200,10 @@ class DefaultConversationManager extends ConversationManager {
198
200
  lastSymptoms: symptomParts.join('; ') || 'Sin síntomas reportados recientemente'
199
201
  };
200
202
  } catch (error) {
201
- logger.error('[DefaultConversationManager] Error getting clinical data', { error: error.message });
203
+ logger.error('[DefaultMemoryManager] Error getting clinical data', { error: error.message });
202
204
  return null;
203
205
  }
204
206
  }
205
207
  }
206
208
 
207
- 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 };
@@ -73,6 +73,18 @@ const messageSchema = new mongoose.Schema({
73
73
  errorCode: { type: String, default: null },
74
74
  errorMessage: { type: String, default: null },
75
75
  updatedAt: { type: Date, default: null }
76
+ },
77
+ recoveryInfo: {
78
+ status: {
79
+ type: String,
80
+ enum: ['in_progress', 'completed', 'failed', 'cancelled', null],
81
+ default: null
82
+ },
83
+ templateSid: { type: String, default: null },
84
+ lastCheckedAt: { type: Date, default: null },
85
+ startedAt: { type: Date, default: null },
86
+ completedAt: { type: Date, default: null },
87
+ error: { type: String, default: null }
76
88
  }
77
89
  }, { timestamps: true });
78
90
 
@@ -124,6 +136,13 @@ async function insertMessage(values) {
124
136
  updatedAt: values.delivery_status_updated_at || null
125
137
  };
126
138
  }
139
+ if (messageData.tools_executed === undefined) {
140
+ messageData.tools_executed = [];
141
+ }
142
+
143
+ if (messageData.tools_executed === undefined) {
144
+ messageData.tools_executed = [];
145
+ }
127
146
 
128
147
  await Message.findOneAndUpdate(
129
148
  { message_id: values.message_id, body: values.body },
@@ -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
+ lastContext: context[-1] || null
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
@@ -287,10 +295,7 @@ class OpenAIResponsesProvider {
287
295
  const makeAPICall = (inputData) => retryWithBackoff(() =>
288
296
  this.client.responses.create({
289
297
  prompt: promptConfig,
290
- model: model || this.defaults.responseModel,
291
- instructions: additionalInstructions || instructions,
292
298
  input: inputData,
293
- metadata, top_p: topP, temperature, max_output_tokens: maxOutputTokens,
294
299
  truncation: truncationStrategy,
295
300
  }), { providerName: PROVIDER_NAME });
296
301
 
@@ -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
 
@@ -39,6 +39,8 @@ const messageRouteDefinitions = {
39
39
  'GET /last': 'getLastInteractionController',
40
40
  'GET /scheduled-status': 'checkScheduledMessageStatusController',
41
41
  'GET /status': 'checkMessageStatusController',
42
+ 'GET /status/:messageSid': 'getMessageStatusController',
43
+ 'POST /status-callback': 'messageStatusCallbackController',
42
44
  'POST /quality': 'addQualityVoteController',
43
45
  'GET /quality/:message_id': 'getQualityVotesByMessageController',
44
46
  'GET /quality/:message_id/voter/:voter_username': 'getQualityVoteByMessageAndVoterController',
@@ -97,6 +99,7 @@ const conversationController = require('../controllers/conversationController');
97
99
  const interactionController = require('../controllers/interactionController');
98
100
  const mediaController = require('../controllers/mediaController');
99
101
  const messageController = require('../controllers/messageController');
102
+ const messageStatusController = require('../controllers/messageStatusController');
100
103
  const patientController = require('../controllers/patientController');
101
104
  const qualityMessageController = require('../controllers/qualityMessageController');
102
105
  const templateController = require('../controllers/templateController');
@@ -144,6 +147,8 @@ const builtInControllers = {
144
147
  getLastInteractionController: messageController.getLastInteractionController,
145
148
  checkScheduledMessageStatusController: messageController.checkScheduledMessageStatusController,
146
149
  checkMessageStatusController: messageController.checkMessageStatusController,
150
+ getMessageStatusController: messageStatusController.getMessageStatusController,
151
+ messageStatusCallbackController: messageStatusController.messageStatusCallbackController,
147
152
  addQualityVoteController: qualityMessageController.addQualityVoteController,
148
153
  getQualityVotesByMessageController: qualityMessageController.getQualityVotesByMessageController,
149
154
  getQualityVoteByMessageAndVoterController: qualityMessageController.getQualityVoteByMessageAndVoterController,
@@ -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,17 @@ 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
196
+ .filter(msg => msg.content.text !== r.reply?.body)
197
+ .map(msg => msg.content.text)
198
+ .join(' ')
199
+ : null;
200
+ return updateMessageRecord(r.reply, finalThread, processedContent);
201
+ }));
193
202
  await cleanupFiles(allTempFiles);
194
203
 
195
204
  if (urls.length > 0) {
@@ -217,15 +226,24 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
217
226
 
218
227
  const assistant = getAssistantById(finalThread.getAssistantId(), finalThread);
219
228
  const runStart = Date.now();
220
- const runResult = await runAssistantWithRetries(finalThread, assistant, runOptions, patientReply);
229
+ const runResult = await runAssistantWithRetries(finalThread, assistant, runOptions, lastMessage);
221
230
  timings.run_assistant_ms = Date.now() - runStart;
222
231
  timings.total_ms = Date.now() - startTotal;
223
232
 
224
- const { output, completed, retries, predictionTimeMs, tools_executed } = runResult;
233
+ const { output: rawOutput, completed, retries, predictionTimeMs, tools_executed } = runResult;
234
+
235
+ const output = sanitizeOutput(rawOutput);
236
+ if (rawOutput !== output) {
237
+ logger.debug('[replyAssistantCore] Output sanitized', {
238
+ originalLength: rawOutput?.length || 0,
239
+ sanitizedLength: output?.length || 0,
240
+ removedContent: rawOutput?.length ? 'brackets_removed' : 'none'
241
+ });
242
+ }
225
243
 
226
244
  logger.info('[Assistant Reply Complete]', {
227
245
  code: code ? `${code.substring(0, 3)}***${code.slice(-4)}` : 'unknown',
228
- messageCount: patientReply.length,
246
+ messageCount: lastMessage.length,
229
247
  hasMedia: urls.length > 0,
230
248
  retries,
231
249
  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,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
+ };