@peopl-health/nexus 2.0.5 → 2.0.7
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/lib/config/interactionConfig.js +3 -0
- package/lib/controllers/interactionController.js +33 -0
- package/lib/controllers/messageController.js +30 -0
- package/lib/controllers/patientController.js +23 -0
- package/lib/core/NexusMessaging.js +6 -8
- package/lib/models/index.js +2 -0
- package/lib/models/interactionModel.js +17 -0
- package/lib/models/messageModel.js +33 -3
- package/lib/routes/index.js +27 -1
- package/lib/storage/MongoStorage.js +5 -21
- package/lib/utils/messageParser.js +2 -1
- package/package.json +1 -1
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const { Interaction } = require('../models/interactionModel');
|
|
2
|
+
const { INTERACTION_QUALITY_VALUES } = require('../config/interactionConfig');
|
|
3
|
+
|
|
4
|
+
const addInteractionController = async (req, res) => {
|
|
5
|
+
try {
|
|
6
|
+
const { messages, whatsapp_id, quality, description } = req.body;
|
|
7
|
+
if (!messages || !Array.isArray(messages) || messages.length === 0) {
|
|
8
|
+
return res.status(400).json({ success: false, error: 'Messages array is required and must not be empty' });
|
|
9
|
+
}
|
|
10
|
+
if (!whatsapp_id) return res.status(400).json({ success: false, error: 'WhatsApp ID is required' });
|
|
11
|
+
if (!quality || !INTERACTION_QUALITY_VALUES.includes(quality)) {
|
|
12
|
+
return res.status(400).json({ success: false, error: `Quality must be one of: ${INTERACTION_QUALITY_VALUES.join(', ')}` });
|
|
13
|
+
}
|
|
14
|
+
const interaction = await Interaction.create({ messages, whatsapp_id, quality, description });
|
|
15
|
+
res.status(201).json({ success: true, interaction });
|
|
16
|
+
} catch (error) {
|
|
17
|
+
console.error('Error adding interaction:', error);
|
|
18
|
+
res.status(500).json({ success: false, error: error.message });
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const getInteractionsByWhatsappIdController = async (req, res) => {
|
|
23
|
+
try {
|
|
24
|
+
const { whatsapp_id } = req.params;
|
|
25
|
+
const interactions = await Interaction.find({ whatsapp_id }).populate('messages').sort({ createdAt: -1 });
|
|
26
|
+
res.status(200).json({ success: true, whatsappId: whatsapp_id, count: interactions.length, interactions });
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error('Error fetching interactions:', error);
|
|
29
|
+
res.status(500).json({ success: false, error: error.message });
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
module.exports = { addInteractionController, getInteractionsByWhatsappIdController };
|
|
@@ -348,10 +348,40 @@ const getLastInteractionController = async (req, res) => {
|
|
|
348
348
|
}
|
|
349
349
|
};
|
|
350
350
|
|
|
351
|
+
const getMessageQualityController = async (req, res) => {
|
|
352
|
+
try {
|
|
353
|
+
const { id } = req.params;
|
|
354
|
+
const message = await Message.findById(id);
|
|
355
|
+
if (!message) return res.status(404).json({ success: false, error: 'Message not found' });
|
|
356
|
+
res.status(200).json({ success: true, messageId: id, quality: message.quality });
|
|
357
|
+
} catch (error) {
|
|
358
|
+
console.error('Error fetching message quality:', error);
|
|
359
|
+
res.status(500).json({ success: false, error: error.message });
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const updateMessageQualityController = async (req, res) => {
|
|
364
|
+
try {
|
|
365
|
+
const { id } = req.params;
|
|
366
|
+
const { quality } = req.body;
|
|
367
|
+
if (!['low', 'medium', 'high'].includes(quality)) {
|
|
368
|
+
return res.status(400).json({ success: false, error: 'Invalid quality value. Must be low, medium, or high' });
|
|
369
|
+
}
|
|
370
|
+
const message = await Message.findByIdAndUpdate(id, { quality }, { new: true });
|
|
371
|
+
if (!message) return res.status(404).json({ success: false, error: 'Message not found' });
|
|
372
|
+
res.status(200).json({ success: true, messageId: id, quality: message.quality });
|
|
373
|
+
} catch (error) {
|
|
374
|
+
console.error('Error updating message quality:', error);
|
|
375
|
+
res.status(500).json({ success: false, error: error.message });
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
|
|
351
379
|
module.exports = {
|
|
352
380
|
sendMessageController,
|
|
353
381
|
sendBulkMessageController,
|
|
354
382
|
sendBulkMessageAirtableController,
|
|
355
383
|
getLastInteractionController,
|
|
384
|
+
getMessageQualityController,
|
|
385
|
+
updateMessageQualityController,
|
|
356
386
|
configureMessageController
|
|
357
387
|
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const { getRecordByFilter } = require('../services/airtableService');
|
|
2
|
+
const { Monitoreo_ID } = require('../config/airtableConfig');
|
|
3
|
+
|
|
4
|
+
const getPatientInfoController = async (req, res) => {
|
|
5
|
+
try {
|
|
6
|
+
const { id } = req.params;
|
|
7
|
+
if (!id) return res.status(400).json({ success: false, error: 'WhatsApp ID is required' });
|
|
8
|
+
|
|
9
|
+
const records = await getRecordByFilter(Monitoreo_ID, 'estado_general', `{whatsapp_id}='${id}'`);
|
|
10
|
+
|
|
11
|
+
if (!records || records.length === 0) {
|
|
12
|
+
return res.status(404).json({ success: false, error: 'Patient not found' });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const clinicalContext = records[0]['clinical-context-json'];
|
|
16
|
+
res.status(200).json({ success: true, whatsappId: id, clinicalContext });
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.error('Error fetching patient info:', error);
|
|
19
|
+
res.status(500).json({ success: false, error: error.message });
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
module.exports = { getPatientInfoController };
|
|
@@ -434,7 +434,8 @@ class NexusMessaging {
|
|
|
434
434
|
await this.sendMessage({
|
|
435
435
|
code: from,
|
|
436
436
|
body: response,
|
|
437
|
-
processed: true
|
|
437
|
+
processed: true,
|
|
438
|
+
origin: 'assistant'
|
|
438
439
|
});
|
|
439
440
|
}
|
|
440
441
|
} catch (error) {
|
|
@@ -443,13 +444,8 @@ class NexusMessaging {
|
|
|
443
444
|
}
|
|
444
445
|
|
|
445
446
|
async handleInteractive(messageData) {
|
|
446
|
-
if (this.messageStorage) {
|
|
447
|
-
await this.messageStorage.saveInteractive(messageData);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
447
|
this.events.emit('interactive:received', messageData);
|
|
451
448
|
|
|
452
|
-
// Run interactive handler if available
|
|
453
449
|
const result = await this._runPipeline('interactive', messageData, async (ctx) => {
|
|
454
450
|
const handler = this.handlers.onInteractive;
|
|
455
451
|
if (handler) {
|
|
@@ -502,7 +498,8 @@ class NexusMessaging {
|
|
|
502
498
|
await this.sendMessage({
|
|
503
499
|
code: from,
|
|
504
500
|
body: response,
|
|
505
|
-
processed: true
|
|
501
|
+
processed: true,
|
|
502
|
+
origin: 'assistant'
|
|
506
503
|
});
|
|
507
504
|
}
|
|
508
505
|
} catch (error) {
|
|
@@ -642,7 +639,8 @@ class NexusMessaging {
|
|
|
642
639
|
await this.sendMessage({
|
|
643
640
|
code: chatId,
|
|
644
641
|
body: botResponse,
|
|
645
|
-
processed: true
|
|
642
|
+
processed: true,
|
|
643
|
+
origin: 'assistant'
|
|
646
644
|
});
|
|
647
645
|
}
|
|
648
646
|
|
package/lib/models/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
const { Message, getMessageValues, formatTimestamp } = require('./messageModel');
|
|
2
2
|
const { Thread } = require('./threadModel');
|
|
3
|
+
const { Interaction } = require('./interactionModel');
|
|
3
4
|
|
|
4
5
|
module.exports = {
|
|
5
6
|
Message,
|
|
6
7
|
Thread,
|
|
8
|
+
Interaction,
|
|
7
9
|
getMessageValues,
|
|
8
10
|
formatTimestamp
|
|
9
11
|
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const mongoose = require('mongoose');
|
|
2
|
+
const { INTERACTION_QUALITY_VALUES } = require('../config/interactionConfig');
|
|
3
|
+
|
|
4
|
+
const interactionSchema = new mongoose.Schema({
|
|
5
|
+
messages: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Message', required: true }],
|
|
6
|
+
whatsapp_id: { type: String, required: true, index: true },
|
|
7
|
+
quality: {
|
|
8
|
+
type: String,
|
|
9
|
+
enum: INTERACTION_QUALITY_VALUES,
|
|
10
|
+
required: true
|
|
11
|
+
},
|
|
12
|
+
description: { type: String, default: null }
|
|
13
|
+
}, { timestamps: true });
|
|
14
|
+
|
|
15
|
+
const Interaction = mongoose.model('Interaction', interactionSchema);
|
|
16
|
+
|
|
17
|
+
module.exports = { Interaction, interactionSchema };
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
const mongoose = require('mongoose');
|
|
2
2
|
const moment = require('moment-timezone');
|
|
3
|
+
const { getRecordByFilter } = require('../services/airtableService');
|
|
4
|
+
const { Monitoreo_ID } = require('../config/airtableConfig');
|
|
3
5
|
|
|
4
6
|
|
|
5
7
|
const messageSchema = new mongoose.Schema({
|
|
8
|
+
raw: { type: Object, required: false },
|
|
6
9
|
nombre_whatsapp: { type: String, required: true },
|
|
7
10
|
numero: { type: String, required: true },
|
|
8
11
|
body: { type: String, required: true },
|
|
@@ -11,11 +14,12 @@ const messageSchema = new mongoose.Schema({
|
|
|
11
14
|
is_group: { type: Boolean, required: true },
|
|
12
15
|
is_media: { type: Boolean, required: true },
|
|
13
16
|
is_interactive: { type: Boolean, default: false },
|
|
14
|
-
|
|
17
|
+
interactive_type: {
|
|
15
18
|
type: String,
|
|
16
19
|
enum: ['button', 'list', 'flow', 'quick_reply'],
|
|
17
20
|
default: null
|
|
18
21
|
},
|
|
22
|
+
interactive_data: { type: Object, default: null },
|
|
19
23
|
group_id: { type: String, default: null },
|
|
20
24
|
reply_id: { type: String, default: null },
|
|
21
25
|
processed: { type: Boolean, default: false },
|
|
@@ -23,6 +27,10 @@ const messageSchema = new mongoose.Schema({
|
|
|
23
27
|
assistant_id: { type: String, default: null },
|
|
24
28
|
content_sid: { type: String, default: null },
|
|
25
29
|
from_me: { type: Boolean, default: false },
|
|
30
|
+
origin: {
|
|
31
|
+
type: String,
|
|
32
|
+
enum: ['whatsapp', 'assistant', 'patient'],
|
|
33
|
+
default: 'whatsapp' },
|
|
26
34
|
media: {
|
|
27
35
|
contentType: { type: String, default: null },
|
|
28
36
|
bucketName: { type: String, default: null },
|
|
@@ -35,6 +43,12 @@ const messageSchema = new mongoose.Schema({
|
|
|
35
43
|
thumbnail: { type: String, default: null },
|
|
36
44
|
metadata: { type: Object, default: null }
|
|
37
45
|
},
|
|
46
|
+
clinical_context: { type: Object, default: null },
|
|
47
|
+
quality: {
|
|
48
|
+
type: String,
|
|
49
|
+
enum: ['low', 'medium', 'high'],
|
|
50
|
+
default: 'medium'
|
|
51
|
+
},
|
|
38
52
|
memoryType: {
|
|
39
53
|
type: String,
|
|
40
54
|
enum: ['active', 'archived'],
|
|
@@ -58,10 +72,23 @@ messageSchema.pre('save', function (next) {
|
|
|
58
72
|
|
|
59
73
|
const Message = mongoose.model('Message', messageSchema);
|
|
60
74
|
|
|
75
|
+
async function getClinicalContext(whatsappId) {
|
|
76
|
+
try {
|
|
77
|
+
const records = await getRecordByFilter(Monitoreo_ID, 'estado_general', `{whatsapp_id}='${whatsappId}'`);
|
|
78
|
+
if (records && records.length > 0 && records[0]['clinical-context-json']) {
|
|
79
|
+
return records[0]['clinical-context-json'];
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error('Error fetching clinical context from Airtable:', error);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
61
87
|
|
|
62
88
|
async function insertMessage(values) {
|
|
63
89
|
try {
|
|
64
90
|
const skipNumbers = ['5215592261426@s.whatsapp.net', '5215547411345@s.whatsapp.net', '51985959446@s.whatsapp.net'];
|
|
91
|
+
const clinical_context = await getClinicalContext(values.numero);
|
|
65
92
|
const messageData = {
|
|
66
93
|
nombre_whatsapp: values.nombre_whatsapp,
|
|
67
94
|
numero: values.numero,
|
|
@@ -71,13 +98,16 @@ async function insertMessage(values) {
|
|
|
71
98
|
is_group: values.is_group,
|
|
72
99
|
is_media: values.is_media,
|
|
73
100
|
is_interactive: values.is_interactive || false,
|
|
74
|
-
|
|
101
|
+
interactive_type: values.interactive_type || null,
|
|
102
|
+
interactive_data: values.interactive_data || null,
|
|
75
103
|
group_id: values.group_id,
|
|
76
104
|
reply_id: values.reply_id,
|
|
77
105
|
from_me: values.from_me,
|
|
78
106
|
processed: values.processed || skipNumbers.includes(values.numero),
|
|
79
107
|
media: values.media ? values.media : null,
|
|
80
|
-
content_sid: values.content_sid || null
|
|
108
|
+
content_sid: values.content_sid || null,
|
|
109
|
+
clinical_context: clinical_context,
|
|
110
|
+
raw: values.raw || null
|
|
81
111
|
};
|
|
82
112
|
console.log('Inserting message', messageData);
|
|
83
113
|
|
package/lib/routes/index.js
CHANGED
|
@@ -32,7 +32,18 @@ const messageRouteDefinitions = {
|
|
|
32
32
|
'POST /send': 'sendMessageController',
|
|
33
33
|
'POST /send-bulk': 'sendBulkMessageController',
|
|
34
34
|
'POST /send-bulk-airtable': 'sendBulkMessageAirtableController',
|
|
35
|
-
'GET /last': 'getLastInteractionController'
|
|
35
|
+
'GET /last': 'getLastInteractionController',
|
|
36
|
+
'GET /:id/quality': 'getMessageQualityController',
|
|
37
|
+
'PUT /:id/quality': 'updateMessageQualityController'
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const interactionRouteDefinitions = {
|
|
41
|
+
'POST /': 'addInteractionController',
|
|
42
|
+
'GET /:whatsapp_id': 'getInteractionsByWhatsappIdController'
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const patientRouteDefinitions = {
|
|
46
|
+
'GET /:id': 'getPatientInfoController'
|
|
36
47
|
};
|
|
37
48
|
|
|
38
49
|
const templateRouteDefinitions = {
|
|
@@ -68,8 +79,10 @@ const createRouter = (routeDefinitions, controllers) => {
|
|
|
68
79
|
// Import built-in controllers
|
|
69
80
|
const assistantController = require('../controllers/assistantController');
|
|
70
81
|
const conversationController = require('../controllers/conversationController');
|
|
82
|
+
const interactionController = require('../controllers/interactionController');
|
|
71
83
|
const mediaController = require('../controllers/mediaController');
|
|
72
84
|
const messageController = require('../controllers/messageController');
|
|
85
|
+
const patientController = require('../controllers/patientController');
|
|
73
86
|
const templateController = require('../controllers/templateController');
|
|
74
87
|
const templateFlowController = require('../controllers/templateFlowController');
|
|
75
88
|
const uploadController = require('../controllers/uploadController');
|
|
@@ -96,6 +109,10 @@ const builtInControllers = {
|
|
|
96
109
|
sendTemplateToNewNumberController: conversationController.sendTemplateToNewNumberController,
|
|
97
110
|
markMessagesAsReadController: conversationController.markMessagesAsReadController,
|
|
98
111
|
|
|
112
|
+
// Interaction controllers
|
|
113
|
+
addInteractionController: interactionController.addInteractionController,
|
|
114
|
+
getInteractionsByWhatsappIdController: interactionController.getInteractionsByWhatsappIdController,
|
|
115
|
+
|
|
99
116
|
// Media controllers
|
|
100
117
|
getMediaController: mediaController.getMediaController,
|
|
101
118
|
handleFileUpload: uploadController.handleFileUpload,
|
|
@@ -105,6 +122,11 @@ const builtInControllers = {
|
|
|
105
122
|
sendBulkMessageController: messageController.sendBulkMessageController,
|
|
106
123
|
sendBulkMessageAirtableController: messageController.sendBulkMessageAirtableController,
|
|
107
124
|
getLastInteractionController: messageController.getLastInteractionController,
|
|
125
|
+
getMessageQualityController: messageController.getMessageQualityController,
|
|
126
|
+
updateMessageQualityController: messageController.updateMessageQualityController,
|
|
127
|
+
|
|
128
|
+
// Patient controllers
|
|
129
|
+
getPatientInfoController: patientController.getPatientInfoController,
|
|
108
130
|
|
|
109
131
|
// Template controllers
|
|
110
132
|
createTemplate: templateController.createTemplate,
|
|
@@ -123,8 +145,10 @@ const builtInControllers = {
|
|
|
123
145
|
const setupDefaultRoutes = (app) => {
|
|
124
146
|
app.use('/api/assistant', createRouter(assistantRouteDefinitions, builtInControllers));
|
|
125
147
|
app.use('/api/conversation', createRouter(conversationRouteDefinitions, builtInControllers));
|
|
148
|
+
app.use('/api/interaction', createRouter(interactionRouteDefinitions, builtInControllers));
|
|
126
149
|
app.use('/api/media', createRouter(mediaRouteDefinitions, builtInControllers));
|
|
127
150
|
app.use('/api/message', createRouter(messageRouteDefinitions, builtInControllers));
|
|
151
|
+
app.use('/api/patient', createRouter(patientRouteDefinitions, builtInControllers));
|
|
128
152
|
app.use('/api/template', createRouter(templateRouteDefinitions, builtInControllers));
|
|
129
153
|
};
|
|
130
154
|
|
|
@@ -132,8 +156,10 @@ module.exports = {
|
|
|
132
156
|
// Route definitions for customers to use with their own controllers
|
|
133
157
|
assistantRoutes: assistantRouteDefinitions,
|
|
134
158
|
conversationRoutes: conversationRouteDefinitions,
|
|
159
|
+
interactionRoutes: interactionRouteDefinitions,
|
|
135
160
|
mediaRoutes: mediaRouteDefinitions,
|
|
136
161
|
messageRoutes: messageRouteDefinitions,
|
|
162
|
+
patientRoutes: patientRouteDefinitions,
|
|
137
163
|
templateRoutes: templateRouteDefinitions,
|
|
138
164
|
|
|
139
165
|
// Helper functions
|
|
@@ -21,7 +21,7 @@ class MongoStorage {
|
|
|
21
21
|
const interactionSchema = new mongoose.Schema({
|
|
22
22
|
messageId: String,
|
|
23
23
|
numero: String,
|
|
24
|
-
|
|
24
|
+
interactive_type: String, // 'button', 'list', 'flow'
|
|
25
25
|
payload: mongoose.Schema.Types.Mixed,
|
|
26
26
|
timestamp: String,
|
|
27
27
|
createdAt: { type: Date, default: Date.now }
|
|
@@ -202,34 +202,18 @@ class MongoStorage {
|
|
|
202
202
|
is_group: isGroup,
|
|
203
203
|
is_media: isMedia,
|
|
204
204
|
is_interactive: messageData.isInteractive || false,
|
|
205
|
-
|
|
205
|
+
interactive_type: messageData.interactionType || null,
|
|
206
|
+
interactive_data: messageData.interactiveData || null,
|
|
206
207
|
group_id: isGroup ? normalizedNumero : null,
|
|
207
208
|
reply_id: messageData.reply_id || messageData.replyId || null,
|
|
208
209
|
from_me: messageData.fromMe !== undefined ? messageData.fromMe : true,
|
|
209
210
|
media,
|
|
210
211
|
content_sid: messageData.contentSid || null,
|
|
211
|
-
template_variables: messageData.variables ? JSON.stringify(messageData.variables) : null
|
|
212
|
+
template_variables: messageData.variables ? JSON.stringify(messageData.variables) : null,
|
|
213
|
+
raw: messageData.raw || null
|
|
212
214
|
};
|
|
213
215
|
}
|
|
214
216
|
|
|
215
|
-
async saveInteractive(interactionData) {
|
|
216
|
-
try {
|
|
217
|
-
const interaction = new this.schemas.Interaction({
|
|
218
|
-
messageId: interactionData.messageId,
|
|
219
|
-
numero: interactionData.from,
|
|
220
|
-
interaction_type: interactionData.type, // 'button', 'list', 'flow'
|
|
221
|
-
payload: interactionData.payload,
|
|
222
|
-
timestamp: this.formatTimestamp(interactionData.timestamp)
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
await interaction.save();
|
|
226
|
-
return interaction;
|
|
227
|
-
} catch (error) {
|
|
228
|
-
console.error('Error saving interaction:', error);
|
|
229
|
-
throw error;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
217
|
async getMessages(numero, limit = 50) {
|
|
234
218
|
try {
|
|
235
219
|
return await this.schemas.Message
|
|
@@ -33,7 +33,8 @@ class MessageParser {
|
|
|
33
33
|
type: 'interactive',
|
|
34
34
|
interactive: interactive,
|
|
35
35
|
isInteractive: true, // Flag to indicate this is an interactive message
|
|
36
|
-
interactionType: interactive.type // Store the specific interaction type
|
|
36
|
+
interactionType: interactive.type, // Store the specific interaction type
|
|
37
|
+
interactiveData: interactive
|
|
37
38
|
};
|
|
38
39
|
}
|
|
39
40
|
|