@peopl-health/nexus 2.4.13 → 2.5.0

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) {
@@ -215,7 +233,6 @@ class TwilioProvider extends MessageProvider {
215
233
  const payload = scheduledMessage.toObject ? scheduledMessage.toObject() : { ...scheduledMessage };
216
234
  delete payload.__nexusSend;
217
235
 
218
- // Map message field to body for consistency (scheduled messages use 'message' field)
219
236
  if (payload.message && !payload.body) {
220
237
  payload.body = payload.message;
221
238
  }
@@ -681,6 +698,30 @@ class TwilioProvider extends MessageProvider {
681
698
  }
682
699
  }
683
700
 
701
+ /**
702
+ * Check the status of a sent message using its SID
703
+ */
704
+ async getMessageStatus(messageSid) {
705
+ if (!this.isConnected || !this.twilioClient) {
706
+ throw new Error('Twilio provider not initialized');
707
+ }
708
+ if (!messageSid) {
709
+ throw new Error('Message SID is required');
710
+ }
711
+
712
+ try {
713
+ const message = await this.twilioClient.messages(messageSid).fetch();
714
+ // Returns the complete Twilio message object with all status information
715
+ // Status-related fields: status, errorCode, errorMessage, dateCreated, dateSent, dateUpdated
716
+ return message;
717
+ } catch (error) {
718
+ if (error.status === 404) {
719
+ throw new Error(`Message with SID ${messageSid} not found`);
720
+ }
721
+ throw new Error(`Failed to fetch message status: ${error.message}`);
722
+ }
723
+ }
724
+
684
725
  }
685
726
 
686
727
  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
+
@@ -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)) {
@@ -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.0",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",