@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.
@@ -0,0 +1,3 @@
1
+ const INTERACTION_QUALITY_VALUES = ['correct', 'correct but not helpful', 'incorrect', 'harmful'];
2
+
3
+ module.exports = { INTERACTION_QUALITY_VALUES };
@@ -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
 
@@ -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
- interaction_type: {
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
- interaction_type: values.interaction_type || null,
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
 
@@ -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
- interaction_type: String, // 'button', 'list', 'flow'
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
- interaction_type: messageData.interactionType || null,
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "2.0.5",
3
+ "version": "2.0.7",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",