@peopl-health/nexus 2.4.12 → 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.
- package/examples/basic-usage.js +81 -3
- package/lib/adapters/TwilioProvider.js +43 -2
- package/lib/controllers/conversationController.js +6 -8
- package/lib/controllers/messageStatusController.js +92 -0
- package/lib/helpers/llmsHelper.js +44 -77
- package/lib/helpers/messageStatusHelper.js +97 -0
- package/lib/models/messageModel.js +26 -1
- package/lib/routes/index.js +1 -1
- package/lib/services/conversationService.js +2 -2
- package/lib/services/twilioService.js +5 -0
- package/lib/storage/MongoStorage.js +7 -1
- package/package.json +1 -1
package/examples/basic-usage.js
CHANGED
|
@@ -9,8 +9,59 @@ app.use(express.json());
|
|
|
9
9
|
class GeneralAssistant extends BaseAssistant {
|
|
10
10
|
constructor(options = {}) {
|
|
11
11
|
super(options);
|
|
12
|
-
//
|
|
13
|
-
|
|
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 };
|
|
@@ -117,10 +117,8 @@ const getConversationMessagesController = async (req, res) => {
|
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
logger.info('Fetching conversation messages
|
|
121
|
-
logger.info('
|
|
122
|
-
|
|
123
|
-
logger.info('About to execute Message.find with query:', JSON.stringify(query));
|
|
120
|
+
logger.info('Fetching conversation messages', { query, limit });
|
|
121
|
+
logger.info('Executing Message.find', { query });
|
|
124
122
|
let messages = [];
|
|
125
123
|
|
|
126
124
|
try {
|
|
@@ -242,7 +240,7 @@ const getConversationReplyController = async (req, res) => {
|
|
|
242
240
|
messageData.contentSid = contentSid;
|
|
243
241
|
|
|
244
242
|
if (variables && Object.keys(variables).length > 0) {
|
|
245
|
-
logger.info('Template variables
|
|
243
|
+
logger.info('Template variables', { variables });
|
|
246
244
|
messageData.variables = variables;
|
|
247
245
|
}
|
|
248
246
|
|
|
@@ -275,7 +273,7 @@ const getConversationReplyController = async (req, res) => {
|
|
|
275
273
|
messageData.body = message;
|
|
276
274
|
}
|
|
277
275
|
|
|
278
|
-
logger.info('Sending message
|
|
276
|
+
logger.info('Sending message', { messageData });
|
|
279
277
|
await sendMessage(messageData);
|
|
280
278
|
logger.info('Message sent successfully');
|
|
281
279
|
|
|
@@ -285,7 +283,7 @@ const getConversationReplyController = async (req, res) => {
|
|
|
285
283
|
});
|
|
286
284
|
} catch (error) {
|
|
287
285
|
logger.error('Error sending reply:', error);
|
|
288
|
-
logger.info('Request body
|
|
286
|
+
logger.info('Request body', { body: req.body || {} });
|
|
289
287
|
const errorMsg = error.message || 'Failed to send reply';
|
|
290
288
|
logger.error('Responding with error:', errorMsg);
|
|
291
289
|
res.status(500).json({
|
|
@@ -615,7 +613,7 @@ const sendTemplateToNewNumberController = async (req, res) => {
|
|
|
615
613
|
|
|
616
614
|
if (variables && Object.keys(variables).length > 0) {
|
|
617
615
|
messageData.variables = variables;
|
|
618
|
-
logger.info('Template variables
|
|
616
|
+
logger.info('Template variables', { variables });
|
|
619
617
|
}
|
|
620
618
|
|
|
621
619
|
const message = await sendMessage(messageData);
|
|
@@ -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
|
+
|
|
@@ -71,7 +71,7 @@ async function analyzeImage(imagePath, isSticker = false, contentType = null) {
|
|
|
71
71
|
|
|
72
72
|
// Description of the image (for both stickers and regular images)
|
|
73
73
|
const imageDescription = 'Describe the image in detail.';
|
|
74
|
-
const
|
|
74
|
+
const descriptionPromise = anthropicClient.messages.create({
|
|
75
75
|
model: 'claude-sonnet-4-5',
|
|
76
76
|
max_tokens: 1024,
|
|
77
77
|
messages: [
|
|
@@ -94,11 +94,13 @@ async function analyzeImage(imagePath, isSticker = false, contentType = null) {
|
|
|
94
94
|
},
|
|
95
95
|
],
|
|
96
96
|
});
|
|
97
|
-
logger.info('[analyzeImage] Description received');
|
|
98
|
-
const description = messageDescription.content[0].text;
|
|
99
97
|
|
|
100
98
|
// For stickers, skip medical analysis and table extraction
|
|
101
99
|
if (isSticker) {
|
|
100
|
+
const messageDescription = await descriptionPromise;
|
|
101
|
+
const description = messageDescription.content[0].text;
|
|
102
|
+
logger.info('[analyzeImage] Description received (sticker)');
|
|
103
|
+
|
|
102
104
|
return {
|
|
103
105
|
description: description,
|
|
104
106
|
medical_analysis: 'NOT_MEDICAL',
|
|
@@ -108,7 +110,9 @@ async function analyzeImage(imagePath, isSticker = false, contentType = null) {
|
|
|
108
110
|
};
|
|
109
111
|
}
|
|
110
112
|
|
|
111
|
-
//
|
|
113
|
+
// Run all analysis calls in parallel
|
|
114
|
+
logger.info('[analyzeImage] Starting parallel analysis calls');
|
|
115
|
+
|
|
112
116
|
const tablePrompt = `Please analyze this image and respond in the following format:
|
|
113
117
|
1. First, determine if there is a table in the image.
|
|
114
118
|
2. If there is NO table, respond with exactly "NONE"
|
|
@@ -126,32 +130,6 @@ async function analyzeImage(imagePath, isSticker = false, contentType = null) {
|
|
|
126
130
|
|
|
127
131
|
Only extract tables - ignore any other content in the image.`;
|
|
128
132
|
|
|
129
|
-
// Create the message with the image
|
|
130
|
-
const messageTable = await anthropicClient.messages.create({
|
|
131
|
-
model: 'claude-3-7-sonnet-20250219',
|
|
132
|
-
max_tokens: 1024,
|
|
133
|
-
messages: [
|
|
134
|
-
{
|
|
135
|
-
role: 'user',
|
|
136
|
-
content: [
|
|
137
|
-
{
|
|
138
|
-
type: 'image',
|
|
139
|
-
source: {
|
|
140
|
-
type: 'base64',
|
|
141
|
-
media_type: mimeType,
|
|
142
|
-
data: base64Image,
|
|
143
|
-
},
|
|
144
|
-
},
|
|
145
|
-
{
|
|
146
|
-
type: 'text',
|
|
147
|
-
text: tablePrompt,
|
|
148
|
-
},
|
|
149
|
-
],
|
|
150
|
-
},
|
|
151
|
-
],
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
// Create a more specific prompt for table detection and extraction
|
|
155
133
|
const medImagePrompt = `
|
|
156
134
|
Eres un oncólogo clínico con experiencia. Se te proporcionará una imagen médica o laboratorio. Analízala y responde exactamente en este formato:
|
|
157
135
|
|
|
@@ -193,59 +171,48 @@ Ejemplo 1:
|
|
|
193
171
|
</EJEMPLOS>
|
|
194
172
|
`;
|
|
195
173
|
|
|
196
|
-
// Create the message with the image
|
|
197
|
-
const messageMedImage = await anthropicClient.messages.create({
|
|
198
|
-
model: 'claude-3-7-sonnet-20250219',
|
|
199
|
-
max_tokens: 1024,
|
|
200
|
-
messages: [
|
|
201
|
-
{
|
|
202
|
-
role: 'user',
|
|
203
|
-
content: [
|
|
204
|
-
{
|
|
205
|
-
type: 'image',
|
|
206
|
-
source: {
|
|
207
|
-
type: 'base64',
|
|
208
|
-
media_type: mimeType,
|
|
209
|
-
data: base64Image,
|
|
210
|
-
},
|
|
211
|
-
},
|
|
212
|
-
{
|
|
213
|
-
type: 'text',
|
|
214
|
-
text: medImagePrompt,
|
|
215
|
-
},
|
|
216
|
-
],
|
|
217
|
-
},
|
|
218
|
-
],
|
|
219
|
-
});
|
|
220
|
-
|
|
221
174
|
const relevancePrompt = `Please analyze this image and respond in this format:
|
|
222
175
|
Medical Relevance: [YES/NO]`;
|
|
223
176
|
|
|
224
|
-
//
|
|
225
|
-
const messageRelevance = await
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
177
|
+
// Execute all 4 API calls in parallel
|
|
178
|
+
const [messageDescription, messageTable, messageMedImage, messageRelevance] = await Promise.all([
|
|
179
|
+
descriptionPromise,
|
|
180
|
+
anthropicClient.messages.create({
|
|
181
|
+
model: 'claude-3-7-sonnet-20250219',
|
|
182
|
+
max_tokens: 1024,
|
|
183
|
+
messages: [{
|
|
230
184
|
role: 'user',
|
|
231
185
|
content: [
|
|
232
|
-
{
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
},
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
186
|
+
{ type: 'image', source: { type: 'base64', media_type: mimeType, data: base64Image } },
|
|
187
|
+
{ type: 'text', text: tablePrompt }
|
|
188
|
+
]
|
|
189
|
+
}]
|
|
190
|
+
}),
|
|
191
|
+
anthropicClient.messages.create({
|
|
192
|
+
model: 'claude-3-7-sonnet-20250219',
|
|
193
|
+
max_tokens: 1024,
|
|
194
|
+
messages: [{
|
|
195
|
+
role: 'user',
|
|
196
|
+
content: [
|
|
197
|
+
{ type: 'image', source: { type: 'base64', media_type: mimeType, data: base64Image } },
|
|
198
|
+
{ type: 'text', text: medImagePrompt }
|
|
199
|
+
]
|
|
200
|
+
}]
|
|
201
|
+
}),
|
|
202
|
+
anthropicClient.messages.create({
|
|
203
|
+
model: 'claude-3-7-sonnet-20250219',
|
|
204
|
+
max_tokens: 1024,
|
|
205
|
+
messages: [{
|
|
206
|
+
role: 'user',
|
|
207
|
+
content: [
|
|
208
|
+
{ type: 'image', source: { type: 'base64', media_type: mimeType, data: base64Image } },
|
|
209
|
+
{ type: 'text', text: relevancePrompt }
|
|
210
|
+
]
|
|
211
|
+
}]
|
|
212
|
+
})
|
|
213
|
+
]);
|
|
248
214
|
|
|
215
|
+
const description = messageDescription.content[0].text;
|
|
249
216
|
const messageTableStr = messageTable.content[0].text;
|
|
250
217
|
const messageRelevanceStr = messageRelevance.content[0].text;
|
|
251
218
|
const messageAnalysisStr = messageMedImage.content[0].text;
|
|
@@ -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(
|
package/lib/routes/index.js
CHANGED
|
@@ -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)) {
|
|
@@ -135,8 +135,8 @@ const fetchConversationData = async (filter, skip, limit) => {
|
|
|
135
135
|
}
|
|
136
136
|
return map;
|
|
137
137
|
}, {}) || {};
|
|
138
|
-
logger.info('
|
|
139
|
-
logger.info('
|
|
138
|
+
logger.info('Unread map calculated', { unreadMap });
|
|
139
|
+
logger.info('Conversations found', { count: conversations?.length || 0 });
|
|
140
140
|
|
|
141
141
|
// Calculate total count for pagination
|
|
142
142
|
let totalFilterConditions = { is_group: false };
|
|
@@ -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
|
|