@peopl-health/nexus 2.4.13 → 2.5.1-fix

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.
@@ -9,8 +9,59 @@ app.use(express.json());
9
9
  class GeneralAssistant extends BaseAssistant {
10
10
  constructor(options = {}) {
11
11
  super(options);
12
- // You can add custom tools or setup here
13
- // Example: this.registerTool('toolName', schema, handler);
12
+ // Register the reportUnresolvedRequests tool for testing
13
+ this.registerTool({
14
+ name: 'reportUnresolvedRequests',
15
+ definition: {
16
+ description: 'Identifica preguntas o síntomas que no se resolvieron completamente o que fueron escaladas al equipo médico.',
17
+ strict: false,
18
+ parameters: {
19
+ type: 'object',
20
+ required: [
21
+ 'unresolvedQuestion',
22
+ 'escalationDetails'
23
+ ],
24
+ properties: {
25
+ unresolvedQuestion: {
26
+ type: 'string',
27
+ description: 'La pregunta o síntoma que quedó sin resolver.'
28
+ },
29
+ escalationDetails: {
30
+ type: 'string',
31
+ description: 'Detalles sobre por qué se escaló o a quién fue escalada (equipo médico, etc.).'
32
+ },
33
+ resolutionAttempted: {
34
+ type: 'boolean',
35
+ description: 'Indica si se intentó ofrecer una resolución antes de escalar.'
36
+ },
37
+ triggerConditions: {
38
+ type: 'object',
39
+ description: 'Condiciones que activan la función cuando se detecta una pregunta o síntoma no resuelto en su totalidad.',
40
+ properties: {
41
+ responseAnalysis: {
42
+ type: 'string',
43
+ description: 'Análisis de la respuesta para determinar si es suficientemente específica o resuelve la pregunta.',
44
+ default: 'La respuesta contiene palabras como \'depende\', \'consulte a su equipo médico\', \'varía según el caso\', \'haremos las consultas al equipo médico de PEOPL\', \'puedo comunicarme con el equipo médico de PEOPL para ti\', \'Voy a escalar la consulta al equipo médico\' o no proporciona un plan de acción claro.'
45
+ }
46
+ },
47
+ required: [
48
+ 'responseAnalysis'
49
+ ]
50
+ }
51
+ },
52
+ additionalProperties: false
53
+ }
54
+ },
55
+ handler: async (args) => {
56
+ // Handler for testing - logs the unresolved request information
57
+ console.log('🔍 [reportUnresolvedRequests] Tool called with:', JSON.stringify(args, null, 2));
58
+ return {
59
+ success: true,
60
+ message: 'Unresolved request reported successfully',
61
+ data: args
62
+ };
63
+ }
64
+ });
14
65
  }
15
66
  }
16
67
 
@@ -35,7 +86,7 @@ async function startServer() {
35
86
  providerConfig: {
36
87
  accountSid: process.env.TWILIO_ACCOUNT_SID,
37
88
  authToken: process.env.TWILIO_AUTH_TOKEN,
38
- whatsappNumber: process.env.TWILIO_WHATSAPP_NUMBER
89
+ whatsappNumber: process.env.TWILIO_WHATSAPP_NUMBER,
39
90
  },
40
91
 
41
92
  // Media configuration for AWS S3 upload
@@ -56,6 +107,8 @@ async function startServer() {
56
107
  // Register the general assistant with its OpenAI assistant ID
57
108
  'asst_O6mAXAhf0xyVj3t4DHRs26uT': GeneralAssistant,
58
109
  'pmpt_68f844cd975481979c080431bde74f6e0adf01a52110813b': GeneralAssistant,
110
+ 'pmpt_68f8059adc7881938477ddbfb6e0d1970c2ed91d30c64be6': GeneralAssistant,
111
+ 'pmpt_68f649ce39ec81908fade27434c167e6014528b0dc5469c6': GeneralAssistant,
59
112
  }
60
113
  }
61
114
  });
@@ -75,6 +128,25 @@ async function startServer() {
75
128
  }
76
129
  });
77
130
 
131
+ // Add status callback endpoint for Twilio message status updates
132
+ // Twilio sends form-encoded data, so we need urlencoded middleware
133
+ const { handleStatusCallback } = require('../lib/helpers/messageStatusHelper');
134
+ app.use('/twilio/status_callback', express.urlencoded({ extended: true }));
135
+ app.post('/twilio/status_callback', async (req, res) => {
136
+ try {
137
+ const updated = await handleStatusCallback(req.body);
138
+ if (updated) {
139
+ console.log('✅ Status updated:', req.body.MessageSid, req.body.MessageStatus);
140
+ res.status(200).json({ success: true, message: 'Status updated' });
141
+ } else {
142
+ res.status(200).json({ success: false, message: 'Message not found' });
143
+ }
144
+ } catch (error) {
145
+ console.error('Status callback error:', error);
146
+ res.status(500).json({ success: false, error: error.message });
147
+ }
148
+ });
149
+
78
150
  // Custom endpoint example
79
151
  app.get('/status', (req, res) => {
80
152
  res.json({
@@ -107,6 +179,7 @@ async function startServer() {
107
179
  console.log('Available endpoints:');
108
180
  console.log('- POST /webhook - Webhook for incoming messages');
109
181
  console.log('- POST /twilio/webhook - Twilio webhook for incoming messages');
182
+ console.log('- POST /twilio/status_callback - Twilio status callback for message delivery status');
110
183
  console.log('- GET /status - Connection status');
111
184
  console.log('- GET /airtable-test - Test Airtable connection');
112
185
  console.log('- GET /media-test - Test AWS media upload configuration');
@@ -117,6 +190,11 @@ async function startServer() {
117
190
  console.log(' - Multiple messages from same sender will be batched');
118
191
  console.log(' - Use test-batching.js to test the functionality');
119
192
  console.log('');
193
+ console.log('🧪 Assistant Testing:');
194
+ console.log(' - Use test-assistant.js to test assistant functionality');
195
+ console.log(' - Run: node examples/test-assistant.js');
196
+ console.log(' - Tests basic messages, responses, batching, and retry logic');
197
+ console.log('');
120
198
  console.log('📸 Media Upload Setup:');
121
199
  console.log(' Add these to your .env file for media upload:');
122
200
  console.log(' - AWS_ACCESS_KEY_ID=your_access_key');
@@ -18,6 +18,7 @@ class TwilioProvider extends MessageProvider {
18
18
  this.accountSid = config.accountSid;
19
19
  this.authToken = config.authToken;
20
20
  this.whatsappNumber = config.whatsappNumber;
21
+ this.statusCallbackUrl = config.statusCallbackUrl || null;
21
22
  }
22
23
 
23
24
  async initialize() {
@@ -61,6 +62,17 @@ class TwilioProvider extends MessageProvider {
61
62
  to: formattedCode
62
63
  };
63
64
 
65
+ if (this.statusCallbackUrl) {
66
+ messageParams.statusCallback = this.statusCallbackUrl;
67
+ messageParams.statusCallbackMethod = messageData.statusCallbackMethod || 'POST';
68
+ logger.info('[TwilioProvider] Message will use status callback', {
69
+ callbackUrl: this.statusCallbackUrl,
70
+ method: messageParams.statusCallbackMethod
71
+ });
72
+ } else {
73
+ logger.debug('[TwilioProvider] No status callback URL configured');
74
+ }
75
+
64
76
  // Handle template messages
65
77
  if (contentSid) {
66
78
  const renderedMessage = await this.renderTemplate(contentSid, variables);
@@ -118,17 +130,23 @@ class TwilioProvider extends MessageProvider {
118
130
  provider: 'twilio',
119
131
  timestamp: new Date(),
120
132
  fromMe: true,
121
- processed: messageData.processed !== undefined ? messageData.processed : false
133
+ processed: messageData.processed !== undefined ? messageData.processed : false,
134
+ statusInfo: {
135
+ status: result.status ? result.status.toLowerCase() : null,
136
+ updatedAt: result.dateCreated || new Date()
137
+ }
122
138
  });
123
139
  logger.info('[TwilioProvider] Message persisted successfully', { messageId: result.sid });
124
140
  } catch (storageError) {
125
141
  logger.error('TwilioProvider storage failed:', storageError);
126
142
  }
127
143
  }
144
+
128
145
  return {
129
146
  success: true,
130
147
  messageId: result.sid,
131
148
  provider: 'twilio',
149
+ status: result.status,
132
150
  result
133
151
  };
134
152
  } catch (error) {
@@ -136,6 +154,41 @@ class TwilioProvider extends MessageProvider {
136
154
  }
137
155
  }
138
156
 
157
+ async sendTypingIndicator(messageId) {
158
+ try {
159
+ const response = await axios.post(
160
+ 'https://messaging.twilio.com/v2/Indicators/Typing.json',
161
+ new URLSearchParams({
162
+ messageId: messageId,
163
+ channel: 'whatsapp'
164
+ }),
165
+ {
166
+ auth: {
167
+ username: this.accountSid,
168
+ password: this.authToken
169
+ },
170
+ headers: {
171
+ 'Content-Type': 'application/x-www-form-urlencoded'
172
+ }
173
+ }
174
+ );
175
+ logger.info('[TwilioProvider] Typing indicator sent successfully', {
176
+ messageId,
177
+ success: response.data?.success,
178
+ status: response.status
179
+ });
180
+ return { success: true };
181
+ } catch (error) {
182
+ logger.error('[TwilioProvider] Failed to send typing indicator', {
183
+ error: error.message,
184
+ messageId,
185
+ status: error.response?.status,
186
+ data: error.response?.data
187
+ });
188
+ return { success: false };
189
+ }
190
+ }
191
+
139
192
  async sendScheduledMessage(scheduledMessage) {
140
193
  const { sendTime, timeZone, __nexusSend } = scheduledMessage;
141
194
  const delay = this.calculateDelay(sendTime, timeZone);
@@ -215,7 +268,6 @@ class TwilioProvider extends MessageProvider {
215
268
  const payload = scheduledMessage.toObject ? scheduledMessage.toObject() : { ...scheduledMessage };
216
269
  delete payload.__nexusSend;
217
270
 
218
- // Map message field to body for consistency (scheduled messages use 'message' field)
219
271
  if (payload.message && !payload.body) {
220
272
  payload.body = payload.message;
221
273
  }
@@ -681,6 +733,30 @@ class TwilioProvider extends MessageProvider {
681
733
  }
682
734
  }
683
735
 
736
+ /**
737
+ * Check the status of a sent message using its SID
738
+ */
739
+ async getMessageStatus(messageSid) {
740
+ if (!this.isConnected || !this.twilioClient) {
741
+ throw new Error('Twilio provider not initialized');
742
+ }
743
+ if (!messageSid) {
744
+ throw new Error('Message SID is required');
745
+ }
746
+
747
+ try {
748
+ const message = await this.twilioClient.messages(messageSid).fetch();
749
+ // Returns the complete Twilio message object with all status information
750
+ // Status-related fields: status, errorCode, errorMessage, dateCreated, dateSent, dateUpdated
751
+ return message;
752
+ } catch (error) {
753
+ if (error.status === 404) {
754
+ throw new Error(`Message with SID ${messageSid} not found`);
755
+ }
756
+ throw new Error(`Failed to fetch message status: ${error.message}`);
757
+ }
758
+ }
759
+
684
760
  }
685
761
 
686
762
  module.exports = { TwilioProvider };
@@ -0,0 +1,92 @@
1
+ const { handleStatusCallback, getMessageStatus } = require('../helpers/messageStatusHelper');
2
+ const { logger } = require('../utils/logger');
3
+
4
+ /**
5
+ * Handle Twilio status callback webhook
6
+ * POST /api/message/status-callback
7
+ */
8
+ async function messageStatusCallbackController(req, res) {
9
+ try {
10
+ logger.info('[MessageStatusController] Received status callback', {
11
+ method: req.method,
12
+ path: req.path,
13
+ headers: req.headers['content-type'],
14
+ bodyKeys: Object.keys(req.body || {}),
15
+ body: req.body
16
+ });
17
+
18
+ const updated = await handleStatusCallback(req.body);
19
+
20
+ if (updated) {
21
+ logger.info('[MessageStatusController] Status updated successfully', {
22
+ messageSid: req.body.MessageSid,
23
+ status: req.body.MessageStatus
24
+ });
25
+ res.status(200).json({
26
+ success: true,
27
+ message: 'Status updated',
28
+ messageSid: req.body.MessageSid,
29
+ status: req.body.MessageStatus
30
+ });
31
+ } else {
32
+ res.status(200).json({
33
+ success: false,
34
+ message: 'Message not found or invalid data',
35
+ messageSid: req.body.MessageSid
36
+ });
37
+ }
38
+ } catch (error) {
39
+ logger.error('[MessageStatusController] Error handling status callback', {
40
+ error: error.message,
41
+ body: req.body
42
+ });
43
+ res.status(500).json({
44
+ success: false,
45
+ error: 'Failed to process status callback'
46
+ });
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Get message status from database
52
+ * GET /api/message/status/:messageSid
53
+ */
54
+ async function getMessageStatusController(req, res) {
55
+ try {
56
+ const { messageSid } = req.params;
57
+
58
+ const message = await getMessageStatus(messageSid);
59
+
60
+ if (message) {
61
+ res.status(200).json({
62
+ success: true,
63
+ message: {
64
+ messageSid: message.message_id,
65
+ statusInfo: message.statusInfo || null,
66
+ numero: message.numero,
67
+ body: message.body
68
+ }
69
+ });
70
+ } else {
71
+ res.status(404).json({
72
+ success: false,
73
+ error: 'Message not found'
74
+ });
75
+ }
76
+ } catch (error) {
77
+ logger.error('[MessageStatusController] Error fetching message status', {
78
+ error: error.message,
79
+ messageSid: req.params.messageSid
80
+ });
81
+ res.status(500).json({
82
+ success: false,
83
+ error: 'Failed to fetch message status'
84
+ });
85
+ }
86
+ }
87
+
88
+ module.exports = {
89
+ messageStatusCallbackController,
90
+ getMessageStatusController
91
+ };
92
+
@@ -1,4 +1,5 @@
1
1
  const { airtable, getBase } = require('../config/airtableConfig');
2
+ const { Message } = require('../models/messageModel');
2
3
  const { addMsgAssistant, replyAssistant } = require('../services/assistantService');
3
4
  const { createProvider } = require('../adapters/registry');
4
5
  const runtimeConfig = require('../config/runtimeConfig');
@@ -42,8 +43,7 @@ class NexusMessaging {
42
43
  this.pendingResponses = new Map();
43
44
  this.batchingConfig = {
44
45
  enabled: config.messageBatching?.enabled ?? false,
45
- baseWaitTime: config.messageBatching?.baseWaitTime ?? 15000,
46
- randomVariation: config.messageBatching?.randomVariation ?? 5000
46
+ baseWaitTime: config.messageBatching?.baseWaitTime ?? 10000
47
47
  };
48
48
  }
49
49
 
@@ -373,6 +373,17 @@ class NexusMessaging {
373
373
  } else if (messageData.flow) {
374
374
  return await this.handleFlow(messageData);
375
375
  } else {
376
+ if (chatId && this.provider && typeof this.provider.sendTypingIndicator === 'function') {
377
+ const messageId = messageData.id || messageData.MessageSid || messageData.message_id;
378
+ if (messageId) {
379
+ setTimeout(() => {
380
+ this.provider.sendTypingIndicator(messageId).catch(err =>
381
+ logger.debug('[processIncomingMessage] Typing indicator failed', { error: err.message })
382
+ );
383
+ }, 3000);
384
+ }
385
+ }
386
+
376
387
  // For regular messages and media, use batching if enabled
377
388
  logger.info('Batching config:', this.batchingConfig);
378
389
  if (this.batchingConfig.enabled && chatId) {
@@ -635,17 +646,12 @@ class NexusMessaging {
635
646
  * Handle message with batching - waits for additional messages before processing
636
647
  */
637
648
  async _handleWithBatching(messageData, chatId) {
638
- // Clear existing timeout if there is one
639
649
  if (this.pendingResponses.has(chatId)) {
640
650
  clearTimeout(this.pendingResponses.get(chatId));
641
651
  logger.info(`Received additional message from ${chatId}, resetting wait timer`);
642
652
  }
643
-
644
- // Calculate wait time with random variation
645
- const waitTime = this.batchingConfig.baseWaitTime +
646
- Math.floor(Math.random() * this.batchingConfig.randomVariation);
647
-
648
- // Set new timeout
653
+
654
+ const waitTime = this.batchingConfig.baseWaitTime;
649
655
  const timeoutId = setTimeout(async () => {
650
656
  try {
651
657
  this.pendingResponses.delete(chatId);
@@ -659,13 +665,40 @@ class NexusMessaging {
659
665
  logger.info(`Waiting ${Math.round(waitTime/1000)} seconds for more messages from ${chatId}`);
660
666
  }
661
667
 
668
+ /**
669
+ * Start typing indicator refresh interval
670
+ */
671
+ async _startTypingRefresh(chatId) {
672
+ if (!this.provider || typeof this.provider.sendTypingIndicator !== 'function') {
673
+ return null;
674
+ }
675
+
676
+ const lastMessage = await Message.findOne({
677
+ numero: chatId,
678
+ from_me: false,
679
+ message_id: { $exists: true, $ne: null }
680
+ }).sort({ createdAt: -1 });
681
+
682
+ if (!lastMessage?.message_id) return null;
683
+
684
+ return setInterval(() =>
685
+ this.provider.sendTypingIndicator(lastMessage.message_id).catch(err =>
686
+ logger.debug('[_startTypingRefresh] Failed', { error: err.message })
687
+ ), 20000
688
+ );
689
+ }
690
+
662
691
  /**
663
692
  * Process all batched messages for a chat
664
693
  */
665
694
  async _handleBatchedMessages(chatId) {
695
+ let typingInterval = null;
696
+
666
697
  try {
667
698
  logger.info(`Processing batched messages from ${chatId} (including media if any)`);
668
699
 
700
+ typingInterval = await this._startTypingRefresh(chatId);
701
+
669
702
  // Get assistant response
670
703
  const result = await replyAssistant(chatId);
671
704
  const botResponse = typeof result === 'string' ? result : result?.output;
@@ -685,7 +718,11 @@ class NexusMessaging {
685
718
  this.events.emit('messages:batched', { chatId, response: botResponse });
686
719
 
687
720
  } catch (error) {
688
- logger.error('Error in batched message handling:', error);
721
+ logger.error('Error in batched message handling:', { error: error.message });
722
+ } finally {
723
+ if (typingInterval) {
724
+ clearInterval(typingInterval);
725
+ }
689
726
  }
690
727
  }
691
728
 
@@ -0,0 +1,97 @@
1
+ const { Message } = require('../models/messageModel');
2
+ const { logger } = require('../utils/logger');
3
+
4
+ /**
5
+ * Update message delivery status in the database based on Twilio status callback data
6
+ */
7
+ async function updateMessageStatus(messageSid, status, errorCode = null, errorMessage = null) {
8
+ try {
9
+ const statusInfo = {
10
+ status,
11
+ updatedAt: new Date()
12
+ };
13
+
14
+ if (errorCode) {
15
+ statusInfo.errorCode = errorCode;
16
+ }
17
+
18
+ if (errorMessage) {
19
+ statusInfo.errorMessage = errorMessage;
20
+ }
21
+
22
+ const updated = await Message.findOneAndUpdate(
23
+ { message_id: messageSid },
24
+ { $set: { statusInfo } },
25
+ { new: true }
26
+ );
27
+
28
+ if (updated) {
29
+ logger.info('[MessageStatus] Updated message status', {
30
+ messageSid,
31
+ status,
32
+ errorCode,
33
+ errorMessage
34
+ });
35
+ } else {
36
+ logger.warn('[MessageStatus] Message not found for status update', { messageSid });
37
+ }
38
+
39
+ return updated;
40
+ } catch (error) {
41
+ logger.error('[MessageStatus] Error updating message status', {
42
+ messageSid,
43
+ error: error.message
44
+ });
45
+ throw error;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Handle Twilio status callback webhook
51
+ */
52
+ async function handleStatusCallback(twilioStatusData) {
53
+ const {
54
+ MessageSid,
55
+ MessageStatus,
56
+ ErrorCode,
57
+ ErrorMessage
58
+ } = twilioStatusData;
59
+
60
+ if (!MessageSid || !MessageStatus) {
61
+ logger.warn('[MessageStatus] Invalid status callback data', twilioStatusData);
62
+ return null;
63
+ }
64
+
65
+ return await updateMessageStatus(
66
+ MessageSid,
67
+ MessageStatus.toLowerCase(),
68
+ ErrorCode || null,
69
+ ErrorMessage || null
70
+ );
71
+ }
72
+
73
+ /**
74
+ * Get message status from database
75
+ */
76
+ async function getMessageStatus(messageSid) {
77
+ try {
78
+ const message = await Message.findOne({ message_id: messageSid })
79
+ .select('statusInfo message_id numero body')
80
+ .lean();
81
+
82
+ return message;
83
+ } catch (error) {
84
+ logger.error('[MessageStatus] Error fetching message status from DB', {
85
+ messageSid,
86
+ error: error.message
87
+ });
88
+ return null;
89
+ }
90
+ }
91
+
92
+ module.exports = {
93
+ updateMessageStatus,
94
+ handleStatusCallback,
95
+ getMessageStatus
96
+ };
97
+
@@ -63,6 +63,25 @@ const messageSchema = new mongoose.Schema({
63
63
  read: {
64
64
  type: Boolean,
65
65
  default: false
66
+ },
67
+ statusInfo: {
68
+ status: {
69
+ type: String,
70
+ enum: ['queued', 'sending', 'sent', 'delivered', 'undelivered', 'failed', 'read', null],
71
+ default: null
72
+ },
73
+ errorCode: {
74
+ type: String,
75
+ default: null
76
+ },
77
+ errorMessage: {
78
+ type: String,
79
+ default: null
80
+ },
81
+ updatedAt: {
82
+ type: Date,
83
+ default: null
84
+ }
66
85
  }
67
86
  }, { timestamps: true });
68
87
 
@@ -118,7 +137,13 @@ async function insertMessage(values) {
118
137
  clinical_context: clinical_context,
119
138
  origin: values.origin,
120
139
  tools_executed: values.tools_executed || [],
121
- raw: values.raw || null
140
+ raw: values.raw || null,
141
+ statusInfo: values.statusInfo || (values.delivery_status ? {
142
+ status: values.delivery_status,
143
+ errorCode: values.delivery_error_code || null,
144
+ errorMessage: values.delivery_error_message || null,
145
+ updatedAt: values.delivery_status_updated_at || null
146
+ } : null)
122
147
  };
123
148
 
124
149
  await Message.findOneAndUpdate(
@@ -1,5 +1,6 @@
1
1
  // Export route definitions for customer servers to import
2
2
  // These are the route patterns without controller dependencies
3
+ const express = require('express');
3
4
 
4
5
  const assistantRouteDefinitions = {
5
6
  'POST /active': 'activeAssistantController',
@@ -65,7 +66,6 @@ const templateRouteDefinitions = {
65
66
 
66
67
  // Helper function to create Express router from route definitions
67
68
  const createRouter = (routeDefinitions, controllers) => {
68
- const express = require('express');
69
69
  const router = express.Router();
70
70
 
71
71
  for (const [route, controllerName] of Object.entries(routeDefinitions)) {
@@ -1,31 +1,18 @@
1
- const { Historial_Clinico_ID } = require('../config/airtableConfig.js');
2
- const AWS = require('../config/awsConfig.js');
1
+ const { withTracing } = require('../utils/tracingDecorator.js');
3
2
  const llmConfig = require('../config/llmConfig');
4
- const runtimeConfig = require('../config/runtimeConfig');
5
3
  const { BaseAssistant } = require('../assistants/BaseAssistant');
6
- const { createProvider } = require('../providers/createProvider');
7
-
8
- const { Thread } = require('../models/threadModel.js');
9
- const { PredictionMetrics } = require('../models/predictionMetricsModel');
10
- const { insertMessage } = require('../models/messageModel');
11
-
12
- const { getCurRow } = require('../helpers/assistantHelper.js');
13
- const { runAssistantAndWait, runAssistantWithRetries } = require('../helpers/assistantHelper.js');
14
- const { getThread, getThreadInfo } = require('../helpers/threadHelper.js');
15
- const { withTracing } = require('../utils/tracingDecorator.js');
16
- const { processThreadMessage } = require('../helpers/processHelper.js');
17
- const { getLastMessages, updateMessageRecord } = require('../helpers/messageHelper.js');
18
- const { withThreadRecovery } = require('../helpers/threadRecoveryHelper.js');
19
- const { combineImagesToPDF, cleanupFiles } = require('../helpers/filesHelper.js');
20
- const { logger } = require('../utils/logger');
21
-
22
- const DEFAULT_MAX_RETRIES = parseInt(process.env.MAX_RETRIES || '30', 10);
4
+ const {
5
+ createAssistantCore,
6
+ addMsgAssistantCore,
7
+ addInstructionCore,
8
+ replyAssistantCore,
9
+ switchAssistantCore
10
+ } = require('./assistantServiceCore');
23
11
 
24
12
  let assistantConfig = null;
25
13
  let assistantRegistry = {};
26
14
  let customGetAssistantById = null;
27
15
 
28
-
29
16
  const configureAssistants = (config) => {
30
17
  if (!config) {
31
18
  throw new Error('Assistant configuration is required');
@@ -98,224 +85,46 @@ const overrideGetAssistantById = (resolverFn) => {
98
85
 
99
86
  const getAssistantById = (assistant_id, thread) => {
100
87
  if (customGetAssistantById) {
101
- const inst = customGetAssistantById(assistant_id, thread);
102
- if (inst) return inst;
103
- }
104
-
105
- if (!assistantConfig) {
106
- assistantConfig = {};
107
- }
108
-
109
- const AssistantClass = assistantRegistry[assistant_id];
110
- if (!AssistantClass) {
111
- throw new Error(`Assistant '${assistant_id}' not found. Available assistants: ${Object.keys(assistantRegistry).join(', ')}`);
88
+ return customGetAssistantById(assistant_id, thread);
112
89
  }
113
90
 
114
- const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
115
- const sharedClient = provider?.getClient?.() || llmConfig.openaiClient || null;
116
-
117
- if (AssistantClass.prototype instanceof BaseAssistant) {
118
- return new AssistantClass({
119
- assistantId: assistant_id,
120
- thread,
121
- client: sharedClient,
122
- provider
123
- });
91
+ if (assistantRegistry[assistant_id]) {
92
+ const AssistantClass = assistantRegistry[assistant_id];
93
+ return new AssistantClass({ thread });
124
94
  }
125
95
 
126
- try {
127
- return new AssistantClass(thread);
128
- } catch (error) {
129
- return new AssistantClass({
130
- thread,
96
+ if (assistantConfig && assistantConfig[assistant_id]) {
97
+ const config = assistantConfig[assistant_id];
98
+ return new BaseAssistant({
99
+ ...config,
131
100
  assistantId: assistant_id,
132
- client: sharedClient,
133
- provider
134
- });
135
- }
136
- };
137
-
138
-
139
- const createAssistant = async (code, assistant_id, messages=[], force=false) => {
140
- const findThread = await Thread.findOne({ code: code });
141
- logger.info('[createAssistant] findThread', findThread);
142
- if (findThread && findThread.getConversationId() && !force) {
143
- logger.info('[createAssistant] Thread already exists');
144
- const updateFields = { active: true, stopped: false };
145
- Thread.setAssistantId(updateFields, assistant_id);
146
- await Thread.updateOne({ code: code }, { $set: updateFields });
147
- return findThread;
148
- }
149
-
150
- if (force && findThread?.getConversationId()) {
151
- const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
152
- await provider.deleteConversation(findThread.getConversationId());
153
- logger.info('[createAssistant] Deleted old conversation, will create new one');
154
- }
155
-
156
- const curRow = await getCurRow(Historial_Clinico_ID, code);
157
- logger.info('[createAssistant] curRow', curRow[0]);
158
- const nombre = curRow?.[0]?.['name'] || null;
159
- const patientId = curRow?.[0]?.['record_id'] || null;
160
-
161
- const assistant = getAssistantById(assistant_id, null);
162
- const initialThread = await assistant.create(code, curRow[0]);
163
-
164
- const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
165
- for (const message of messages) {
166
- await provider.addMessage({
167
- threadId: initialThread.id,
168
- role: 'assistant',
169
- content: message
101
+ thread
170
102
  });
171
103
  }
172
-
173
- const thread = {
174
- code: code,
175
- patient_id: patientId,
176
- nombre: nombre,
177
- active: true
178
- };
179
- Thread.setAssistantId(thread, assistant_id);
180
- Thread.setConversationId(thread, initialThread.id);
181
-
182
- const condition = { $or: [{ code: code }] };
183
- const options = { new: true, upsert: true };
184
- const updatedThread = await Thread.findOneAndUpdate(condition, {run_id: null, ...thread}, options);
185
- logger.info('[createAssistant] Updated thread:', updatedThread);
186
-
187
- return thread;
188
- };
189
104
 
190
- const addMsgAssistant = async (code, inMessages, role = 'user', reply = false, skipSystemMessage = false) => {
191
- try {
192
- let thread = await Thread.findOne({ code: code });
193
- logger.info(thread);
194
- if (thread === null) return null;
195
-
196
- const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
197
-
198
- await withThreadRecovery(
199
- async (recoveredThread = thread) => {
200
- thread = recoveredThread;
201
- for (const message of inMessages) {
202
- logger.info(message);
203
- await provider.addMessage({
204
- threadId: thread.getConversationId(),
205
- role: role,
206
- content: message
207
- });
208
-
209
- // Save system message to database for frontend visibility
210
- // Skip if message is already saved (e.g., from getConversationReplyController)
211
- if (!skipSystemMessage) {
212
- try {
213
- const message_id = `system_${Date.now()}_${Math.random().toString(36).substring(7)}`;
214
- await insertMessage({
215
- nombre_whatsapp: 'System',
216
- numero: code,
217
- body: message,
218
- timestamp: new Date(),
219
- message_id: message_id,
220
- is_group: false,
221
- is_media: false,
222
- from_me: true,
223
- processed: true,
224
- origin: 'system',
225
- thread_id: thread.getConversationId(),
226
- assistant_id: thread.getAssistantId(),
227
- raw: { role: role }
228
- });
229
- } catch (err) {
230
- // Don't throw - we don't want to break the flow if logging fails
231
- logger.error('[addMsgAssistant] Error saving system message:', err);
232
- }
233
- }
234
- }
235
- },
236
- thread,
237
- process.env.VARIANT || 'assistants'
238
- );
239
-
240
- if (!reply) return null;
241
-
242
- let output, completed;
243
- let retries = 0;
244
- const maxRetries = DEFAULT_MAX_RETRIES;
245
- const assistant = getAssistantById(thread.getAssistantId(), thread);
246
- do {
247
- ({ output, completed } = await runAssistantAndWait({ thread, assistant }));
248
- logger.info(`Attempt ${retries + 1}: completed=${completed}, output=${output || '(empty)'}`);
249
-
250
- if (completed && output) break;
251
- if (retries < maxRetries) await new Promise(resolve => setTimeout(resolve, 2000));
252
- retries++;
253
- } while (retries <= maxRetries && (!completed || !output));
254
-
255
- logger.info('THE ANS IS', output);
256
- return output;
257
- } catch (error) {
258
- logger.info(error);
259
- return null;
260
- }
105
+ throw new Error(`Assistant with ID "${assistant_id}" not found`);
261
106
  };
262
107
 
263
- const addInstructionCore = async (code, instruction, role = 'user') => {
264
- const thread = await withTracing(getThread, 'get_thread_operation',
265
- (threadCode) => ({
266
- 'thread.code': threadCode,
267
- 'operation.type': 'thread_retrieval'
268
- })
269
- )(code);
270
- if (thread === null) return null;
271
-
272
- const assistant = getAssistantById(thread.getAssistantId(), thread);
273
- const { output, completed, retries } = await withTracing(
274
- runAssistantWithRetries,
275
- 'run_assistant_with_retries',
276
- (thread, assistant, runConfig, patientReply) => ({
277
- 'assistant.id': thread.getAssistantId(),
278
- 'assistant.max_retries': DEFAULT_MAX_RETRIES,
279
- 'assistant.has_patient_reply': !!patientReply
280
- })
281
- )(
282
- thread,
283
- assistant,
284
- {
285
- additionalInstructions: instruction,
286
- additionalMessages: [
287
- { role: role, content: instruction }
288
- ]
289
- },
290
- null // no patientReply for instructions
291
- );
292
-
293
- // Save instruction to database for frontend visibility
294
- try {
295
- const message_id = `instruction_${Date.now()}_${Math.random().toString(36).substring(7)}`;
296
- await insertMessage({
297
- nombre_whatsapp: 'Instruction',
298
- numero: code,
299
- body: instruction,
300
- timestamp: new Date(),
301
- message_id: message_id,
302
- is_group: false,
303
- is_media: false,
304
- from_me: true,
305
- processed: true,
306
- origin: 'instruction',
307
- thread_id: thread.getConversationId(),
308
- assistant_id: thread.getAssistantId(),
309
- raw: { role: role }
310
- });
311
- } catch (err) {
312
- // Don't throw - we don't want to break the flow if logging fails
313
- logger.error('[addInstructionCore] Error saving instruction message:', err);
314
- }
108
+ const createAssistant = withTracing(
109
+ (code, assistant_id) => createAssistantCore(code, assistant_id, getAssistantById),
110
+ 'create_assistant',
111
+ (code, assistant_id) => ({
112
+ 'assistant.thread_code': code,
113
+ 'assistant.id': assistant_id,
114
+ 'operation.type': 'create_assistant'
115
+ })
116
+ );
315
117
 
316
- logger.info('RUN RESPONSE', output);
317
- return output;
318
- };
118
+ const addMsgAssistant = withTracing(
119
+ addMsgAssistantCore,
120
+ 'add_message_assistant',
121
+ (code, message, role) => ({
122
+ 'message.thread_code': code,
123
+ 'message.content_length': message?.length || 0,
124
+ 'message.role': role,
125
+ 'operation.type': 'add_message'
126
+ })
127
+ );
319
128
 
320
129
  const addInsAssistant = withTracing(
321
130
  addInstructionCore,
@@ -328,199 +137,28 @@ const addInsAssistant = withTracing(
328
137
  })
329
138
  );
330
139
 
331
- const replyAssistantCore = async (code, message_ = null, thread_ = null, runOptions = {}) => {
332
- const timings = {};
333
- const startTotal = Date.now();
334
-
335
- const { result: thread, duration: getThreadMs } = await withTracing(
336
- getThread,
337
- 'get_thread_operation',
338
- (threadCode) => ({
339
- 'thread.code': threadCode,
340
- 'operation.type': 'thread_retrieval',
341
- 'thread.provided': !!thread_
342
- }),
343
- { returnTiming: true }
344
- )(code);
345
- timings.get_thread_ms = getThreadMs;
346
-
347
- if (!thread_ && !thread) return null;
348
- const finalThread = thread_ || thread;
349
-
350
- const { result: patientReply, duration: getMessagesMs } = await withTracing(
351
- getLastMessages,
352
- 'get_last_messages',
353
- (code) => ({ 'thread.code': code }),
354
- { returnTiming: true }
355
- )(code);
356
- timings.get_messages_ms = getMessagesMs;
357
-
358
- if (!patientReply) {
359
- logger.info('[replyAssistantCore] No relevant data found for this assistant.');
360
- return null;
361
- }
362
-
363
- const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
364
-
365
- logger.info(`[replyAssistantCore] Processing ${patientReply.length} messages in parallel`);
366
-
367
- const { result: processResult, duration: processMessagesMs } = await withTracing(
368
- processThreadMessage,
369
- 'process_thread_messages',
370
- (code, patientReply, provider) => ({
371
- 'messages.count': patientReply.length,
372
- 'thread.code': code
373
- }),
374
- { returnTiming: true }
375
- )(code, patientReply, provider);
376
-
377
- const { results: processResults, timings: processTimings } = processResult;
378
- timings.process_messages_ms = processMessagesMs;
379
-
380
- logger.debug('[replyAssistantCore] Process timings breakdown', { processTimings });
381
-
382
- if (processTimings) {
383
- timings.process_messages_breakdown = {
384
- download_ms: processTimings.download_ms || 0,
385
- image_analysis_ms: processTimings.image_analysis_ms || 0,
386
- audio_transcription_ms: processTimings.audio_transcription_ms || 0,
387
- url_generation_ms: processTimings.url_generation_ms || 0,
388
- total_media_ms: processTimings.total_media_ms || 0
389
- };
390
- }
391
-
392
- const patientMsg = processResults.some(r => r.isPatient);
393
- const urls = processResults.filter(r => r.url).map(r => ({ url: r.url }));
394
- const allMessagesToAdd = processResults.flatMap(r => r.messages || []);
395
- const allTempFiles = processResults.flatMap(r => r.tempFiles || []);
396
-
397
- if (allMessagesToAdd.length > 0) {
398
- logger.info(`[replyAssistantCore] Adding ${allMessagesToAdd.length} messages to thread in batch`);
399
- await withThreadRecovery(
400
- async (thread = finalThread) => {
401
- const threadId = thread.getConversationId();
402
- await provider.addMessage({ threadId, messages: allMessagesToAdd });
403
- },
404
- finalThread,
405
- process.env.VARIANT || 'assistants'
406
- );
407
- }
408
-
409
- await Promise.all(processResults.map(r => updateMessageRecord(r.reply, finalThread)));
410
- await cleanupFiles(allTempFiles);
411
-
412
- if (urls.length > 0) {
413
- logger.info(`[replyAssistantCore] Processing ${urls.length} URLs for PDF combination`);
414
- const { result: pdfResult, duration: pdfCombinationMs } = await withTracing(
415
- combineImagesToPDF,
416
- 'combine_images_to_pdf',
417
- ({ code }) => ({
418
- 'pdf.thread_code': code,
419
- 'pdf.url_count': urls.length
420
- }),
421
- { returnTiming: true }
422
- )({ code });
423
- timings.pdf_combination_ms = pdfCombinationMs;
424
- const { pdfBuffer, processedFiles } = pdfResult;
425
- logger.info(`[replyAssistantCore] PDF combination complete: ${processedFiles?.length || 0} files processed`);
426
-
427
- if (pdfBuffer) {
428
- const key = `${code}-${Date.now()}-combined.pdf`;
429
- const bucket = runtimeConfig.get('AWS_S3_BUCKET_NAME');
430
- if (bucket) {
431
- await AWS.uploadBufferToS3(pdfBuffer, bucket, key, 'application/pdf');
432
- }
433
- }
434
-
435
- if (processedFiles && processedFiles.length) {
436
- cleanupFiles(processedFiles);
437
- }
438
- }
439
-
440
- if (!patientMsg || finalThread.stopped) return null;
441
-
442
- const assistant = getAssistantById(finalThread.getAssistantId(), finalThread);
443
- const { result: runResult, duration: runAssistantMs } = await withTracing(
444
- runAssistantWithRetries,
445
- 'run_assistant_with_retries',
446
- (thread, assistant, runConfig, patientReply) => ({
447
- 'assistant.id': thread.getAssistantId(),
448
- 'assistant.max_retries': DEFAULT_MAX_RETRIES,
449
- 'assistant.has_patient_reply': !!patientReply
450
- }),
451
- { returnTiming: true }
452
- )(finalThread, assistant, runOptions, patientReply);
453
- timings.run_assistant_ms = runAssistantMs;
454
- timings.total_ms = Date.now() - startTotal;
455
-
456
- const { run, output, completed, retries, predictionTimeMs, tools_executed } = runResult;
457
-
458
- logger.info('[Assistant Reply Complete]', {
459
- code: code ? `${code.substring(0, 3)}***${code.slice(-4)}` : 'unknown',
460
- messageCount: patientReply.length,
461
- hasMedia: urls.length > 0,
462
- retries,
463
- totalMs: timings.total_ms,
464
- toolsExecuted: tools_executed?.length || 0
465
- });
466
-
467
- if (output && predictionTimeMs) {
468
- logger.debug('[replyAssistantCore] Storing metrics with timing_breakdown', {
469
- timing_breakdown: timings,
470
- has_breakdown: !!timings.process_messages_breakdown
471
- });
472
-
473
- await PredictionMetrics.create({
474
- message_id: `${code}-${Date.now()}`,
475
- numero: code,
476
- assistant_id: finalThread.getAssistantId(),
477
- thread_id: finalThread.getConversationId(),
478
- prediction_time_ms: predictionTimeMs,
479
- retry_count: retries,
480
- completed: completed,
481
- timing_breakdown: timings
482
- }).catch(err => logger.error('[replyAssistantCore] Failed to store metrics:', err));
483
- }
484
-
485
- return { output, tools_executed };
486
- };
487
-
488
140
  const replyAssistant = withTracing(
489
- replyAssistantCore,
141
+ (code, message_, thread_, runOptions) => replyAssistantCore(code, message_, thread_, runOptions, getAssistantById),
490
142
  'assistant_reply',
491
143
  (code, message_, thread_, runOptions) => ({
492
144
  'assistant.thread_code': code,
493
145
  'assistant.has_message': !!message_,
494
146
  'assistant.has_custom_thread': !!thread_,
495
- 'assistant.run_options': JSON.stringify(runOptions)
147
+ 'assistant.has_run_options': !!runOptions && Object.keys(runOptions).length > 0
496
148
  })
497
149
  );
498
150
 
499
- const switchAssistant = async (code, assistant_id) => {
500
- try {
501
- const thread = await Thread.findOne({ code: code });
502
- logger.info('Inside thread', thread);
503
- if (thread === null) return;
504
-
505
- const variant = process.env.VARIANT || 'assistants';
506
- const updateFields = { active: true, stopped: false };
507
-
508
- if (variant === 'responses') {
509
- updateFields.prompt_id = assistant_id;
510
- } else {
511
- updateFields.assistant_id = assistant_id;
512
- }
513
-
514
- await Thread.updateOne({ code }, { $set: updateFields });
515
- } catch (error) {
516
- logger.info(error);
517
- return null;
518
- }
519
- };
151
+ const switchAssistant = withTracing(
152
+ switchAssistantCore,
153
+ 'switch_assistant',
154
+ (code, assistant_id) => ({
155
+ 'assistant.thread_code': code,
156
+ 'assistant.new_id': assistant_id,
157
+ 'operation.type': 'switch_assistant'
158
+ })
159
+ );
520
160
 
521
161
  module.exports = {
522
- getThread,
523
- getThreadInfo,
524
162
  getAssistantById,
525
163
  createAssistant,
526
164
  replyAssistant,
@@ -529,6 +167,5 @@ module.exports = {
529
167
  switchAssistant,
530
168
  configureAssistants,
531
169
  registerAssistant,
532
- overrideGetAssistantById,
533
- runAssistantAndWait
170
+ overrideGetAssistantById
534
171
  };
@@ -0,0 +1,215 @@
1
+ const AWS = require('../config/awsConfig.js');
2
+ const runtimeConfig = require('../config/runtimeConfig');
3
+ const { createProvider } = require('../providers/createProvider');
4
+
5
+ const { Thread } = require('../models/threadModel.js');
6
+ const { PredictionMetrics } = require('../models/predictionMetricsModel');
7
+ const { insertMessage } = require('../models/messageModel');
8
+
9
+ const { getCurRow, runAssistantWithRetries } = require('../helpers/assistantHelper.js');
10
+ const { getThread } = require('../helpers/threadHelper.js');
11
+ const { processThreadMessage } = require('../helpers/processHelper.js');
12
+ const { getLastMessages, updateMessageRecord } = require('../helpers/messageHelper.js');
13
+ const { withThreadRecovery } = require('../helpers/threadRecoveryHelper.js');
14
+ const { combineImagesToPDF, cleanupFiles } = require('../helpers/filesHelper.js');
15
+ const { logger } = require('../utils/logger');
16
+
17
+ const createAssistantCore = async (code, assistant_id, getAssistantById) => {
18
+ const thread = await getThread(code);
19
+ if (!thread) return null;
20
+
21
+ const assistant = getAssistantById(assistant_id, thread);
22
+ const curRow = await getCurRow(code);
23
+ const context = { curRow };
24
+
25
+ try {
26
+ await assistant.create(code, context);
27
+ return { success: true, assistant_id };
28
+ } catch (error) {
29
+ logger.error('[createAssistantCore] Error:', error);
30
+ return { success: false, error: error.message };
31
+ }
32
+ };
33
+
34
+ const addMsgAssistantCore = async (code, message, role = 'user') => {
35
+ const thread = await getThread(code);
36
+ if (!thread) return null;
37
+
38
+ const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
39
+ const threadId = thread.getConversationId();
40
+
41
+ try {
42
+ await provider.addMessage({ threadId, messages: [{ role, content: message }] });
43
+ await insertMessage({ code, message, role });
44
+ return { success: true };
45
+ } catch (error) {
46
+ logger.error('[addMsgAssistantCore] Error:', error);
47
+ return { success: false, error: error.message };
48
+ }
49
+ };
50
+
51
+ const addInstructionCore = async (code, instruction, role = 'user') => {
52
+ const thread = await getThread(code);
53
+ if (!thread) return null;
54
+
55
+ const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
56
+ const threadId = thread.getConversationId();
57
+
58
+ try {
59
+ await provider.addMessage({ threadId, messages: [{ role, content: instruction }] });
60
+ return { success: true };
61
+ } catch (error) {
62
+ logger.error('[addInstructionCore] Error:', error);
63
+ return { success: false, error: error.message };
64
+ }
65
+ };
66
+
67
+ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOptions = {}, getAssistantById) => {
68
+ const timings = {};
69
+ const startTotal = Date.now();
70
+
71
+ try {
72
+ const thread = thread_ || await getThread(code);
73
+ timings.get_thread_ms = 0;
74
+
75
+ if (!thread) return null;
76
+ const finalThread = thread;
77
+
78
+ const patientReply = await getLastMessages(code);
79
+ timings.get_messages_ms = 0;
80
+
81
+ if (!patientReply) {
82
+ logger.info('[replyAssistantCore] No relevant data found for this assistant.');
83
+ return null;
84
+ }
85
+
86
+ const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
87
+ logger.info(`[replyAssistantCore] Processing ${patientReply.length} messages in parallel`);
88
+ const processResult = await processThreadMessage(code, patientReply, provider);
89
+
90
+ const { results: processResults, timings: processTimings } = processResult;
91
+ timings.process_messages_ms = 0;
92
+
93
+ logger.debug('[replyAssistantCore] Process timings breakdown', { processTimings });
94
+
95
+ if (processTimings) {
96
+ timings.process_messages_breakdown = {
97
+ download_ms: processTimings.download_ms || 0,
98
+ image_analysis_ms: processTimings.image_analysis_ms || 0,
99
+ audio_transcription_ms: processTimings.audio_transcription_ms || 0,
100
+ url_generation_ms: processTimings.url_generation_ms || 0,
101
+ total_media_ms: processTimings.total_media_ms || 0
102
+ };
103
+ }
104
+
105
+ const patientMsg = processResults.some(r => r.isPatient);
106
+ const urls = processResults.filter(r => r.url).map(r => ({ url: r.url }));
107
+ const allMessagesToAdd = processResults.flatMap(r => r.messages || []);
108
+ const allTempFiles = processResults.flatMap(r => r.tempFiles || []);
109
+
110
+ if (allMessagesToAdd.length > 0) {
111
+ logger.info(`[replyAssistantCore] Adding ${allMessagesToAdd.length} messages to thread in batch`);
112
+ await withThreadRecovery(
113
+ async (thread = finalThread) => {
114
+ const threadId = thread.getConversationId();
115
+ await provider.addMessage({ threadId, messages: allMessagesToAdd });
116
+ },
117
+ finalThread,
118
+ process.env.VARIANT || 'assistants'
119
+ );
120
+ }
121
+
122
+ await Promise.all(processResults.map(r => updateMessageRecord(r.reply, finalThread)));
123
+ await cleanupFiles(allTempFiles);
124
+
125
+ if (urls.length > 0) {
126
+ logger.info(`[replyAssistantCore] Processing ${urls.length} URLs for PDF combination`);
127
+ const pdfResult = await combineImagesToPDF({ code });
128
+ timings.pdf_combination_ms = 0;
129
+ const { pdfBuffer, processedFiles } = pdfResult;
130
+ logger.info(`[replyAssistantCore] PDF combination complete: ${processedFiles?.length || 0} files processed`);
131
+
132
+ if (pdfBuffer) {
133
+ const key = `${code}-${Date.now()}-combined.pdf`;
134
+ const bucket = runtimeConfig.get('AWS_S3_BUCKET_NAME');
135
+ if (bucket) {
136
+ await AWS.uploadBufferToS3(pdfBuffer, bucket, key, 'application/pdf');
137
+ }
138
+ }
139
+
140
+ if (processedFiles && processedFiles.length) {
141
+ cleanupFiles(processedFiles);
142
+ }
143
+ }
144
+
145
+ if (!patientMsg || finalThread.stopped) return null;
146
+
147
+ const assistant = getAssistantById(finalThread.getAssistantId(), finalThread);
148
+ const runResult = await runAssistantWithRetries(finalThread, assistant, runOptions, patientReply);
149
+ timings.run_assistant_ms = 0;
150
+ timings.total_ms = Date.now() - startTotal;
151
+
152
+ const { run, output, completed, retries, predictionTimeMs, tools_executed } = runResult;
153
+
154
+ logger.info('[Assistant Reply Complete]', {
155
+ code: code ? `${code.substring(0, 3)}***${code.slice(-4)}` : 'unknown',
156
+ messageCount: patientReply.length,
157
+ hasMedia: urls.length > 0,
158
+ retries,
159
+ totalMs: timings.total_ms,
160
+ toolsExecuted: tools_executed?.length || 0
161
+ });
162
+
163
+ if (output && predictionTimeMs) {
164
+ logger.debug('[replyAssistantCore] Storing metrics with timing_breakdown', {
165
+ timing_breakdown: timings,
166
+ has_breakdown: !!timings.process_messages_breakdown
167
+ });
168
+
169
+ await PredictionMetrics.create({
170
+ message_id: `${code}-${Date.now()}`,
171
+ numero: code,
172
+ assistant_id: finalThread.getAssistantId(),
173
+ thread_id: finalThread.getConversationId(),
174
+ prediction_time_ms: predictionTimeMs,
175
+ retry_count: retries,
176
+ completed: completed,
177
+ timing_breakdown: timings
178
+ }).catch(err => logger.error('[replyAssistantCore] Failed to store metrics:', err));
179
+ }
180
+
181
+ return { output, tools_executed };
182
+ } catch (error) {
183
+ logger.error('[replyAssistantCore] Error:', { error: error.message });
184
+ throw error;
185
+ }
186
+ };
187
+
188
+ const switchAssistantCore = async (code, assistant_id) => {
189
+ try {
190
+ const thread = await Thread.findOne({ code });
191
+ if (!thread) {
192
+ return null;
193
+ }
194
+
195
+ const updateFields = {
196
+ assistant_id,
197
+ stopped: false,
198
+ updatedAt: new Date()
199
+ };
200
+
201
+ await Thread.updateOne({ code }, { $set: updateFields });
202
+ return { success: true, assistant_id };
203
+ } catch (error) {
204
+ logger.info(error);
205
+ return null;
206
+ }
207
+ };
208
+
209
+ module.exports = {
210
+ createAssistantCore,
211
+ addMsgAssistantCore,
212
+ addInstructionCore,
213
+ replyAssistantCore,
214
+ switchAssistantCore
215
+ };
@@ -49,6 +49,11 @@ const TwilioService = {
49
49
  return await nexusProvider.createTemplate(templateData);
50
50
  },
51
51
 
52
+ async getMessageStatus(messageSid) {
53
+ checkTwilioSupport();
54
+ return await nexusProvider.getMessageStatus(messageSid);
55
+ },
56
+
52
57
  // Add any other Twilio operations as needed
53
58
  configureNexusProvider
54
59
  };
@@ -187,7 +187,13 @@ class MongoStorage {
187
187
  template_variables: messageData.variables ? JSON.stringify(messageData.variables) : null,
188
188
  raw: messageData.raw || null,
189
189
  origin,
190
- tools_executed: messageData.tools_executed || []
190
+ tools_executed: messageData.tools_executed || [],
191
+ statusInfo: messageData.statusInfo || (messageData.delivery_status ? {
192
+ status: messageData.delivery_status,
193
+ errorCode: messageData.delivery_error_code || null,
194
+ errorMessage: messageData.delivery_error_message || null,
195
+ updatedAt: messageData.delivery_status_updated_at || null
196
+ } : null)
191
197
  };
192
198
  }
193
199
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "2.4.13",
3
+ "version": "2.5.1-fix",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",