@peopl-health/nexus 2.5.11 → 2.7.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 +3 -4
- package/lib/assistants/BaseAssistant.js +1 -16
- package/lib/config/airtableConfig.js +3 -0
- package/lib/controllers/conversationController.js +52 -42
- package/lib/core/NexusMessaging.js +66 -87
- package/lib/helpers/messageHelper.js +0 -26
- package/lib/models/messageModel.js +6 -0
- package/lib/providers/OpenAIResponsesProvider.js +85 -80
- package/lib/providers/OpenAIResponsesProviderTools.js +4 -1
- package/lib/providers/createProvider.js +11 -1
- package/lib/routes/index.js +1 -1
- package/lib/services/ConversationManager.js +43 -0
- package/lib/services/DefaultConversationManager.js +207 -0
- package/lib/services/airtableService.js +1 -1
- package/lib/services/assistantServiceCore.js +0 -23
- package/lib/services/conversationService.js +42 -10
- package/package.json +2 -2
package/examples/basic-usage.js
CHANGED
|
@@ -66,13 +66,12 @@ class GeneralAssistant extends BaseAssistant {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
async function startServer() {
|
|
69
|
-
// Initialize Nexus with
|
|
69
|
+
// Initialize Nexus with check-after processing (immediate response, checks for new messages after)
|
|
70
70
|
const nexus = new Nexus({
|
|
71
71
|
messaging: {
|
|
72
72
|
messageBatching: {
|
|
73
|
-
enabled: true,
|
|
74
|
-
|
|
75
|
-
randomVariation: 5000 // 0-5 seconds random variation
|
|
73
|
+
enabled: true, // Enable check-after processing
|
|
74
|
+
checkDelayMs: 100 // Delay before checking for new messages (ms)
|
|
76
75
|
}
|
|
77
76
|
}
|
|
78
77
|
});
|
|
@@ -170,22 +170,7 @@ class BaseAssistant {
|
|
|
170
170
|
async create(code, context = {}) {
|
|
171
171
|
this._ensureClient();
|
|
172
172
|
this.status = 'active';
|
|
173
|
-
|
|
174
|
-
const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
|
|
175
|
-
if (!provider || typeof provider.createConversation !== 'function') {
|
|
176
|
-
throw new Error('Provider not configured. Cannot create conversation.');
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const initialMessages = await this.buildInitialMessages({ code, context });
|
|
180
|
-
const conversation = await provider.createConversation({
|
|
181
|
-
messages: initialMessages,
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
this.thread = {
|
|
185
|
-
...conversation,
|
|
186
|
-
thread_id: conversation?.id,
|
|
187
|
-
code,
|
|
188
|
-
};
|
|
173
|
+
this.thread = { code };
|
|
189
174
|
|
|
190
175
|
return this.thread;
|
|
191
176
|
}
|
|
@@ -13,6 +13,7 @@ const Logging_ID = require('./runtimeConfig').get('AIRTABLE_LOGGING_ID') || 'app
|
|
|
13
13
|
const Monitoreo_ID = require('./runtimeConfig').get('AIRTABLE_MONITOREO_ID') || 'appdvraKSdp0XVn5n';
|
|
14
14
|
const Programa_Juntas_ID = require('./runtimeConfig').get('AIRTABLE_PROGRAMA_JUNTAS_ID') || 'appKFWzkcDEWlrXBE';
|
|
15
15
|
const Symptoms_ID = require('./runtimeConfig').get('AIRTABLE_SYMPTOMS_ID') || 'appQRhZlQ9tMfYZWJ';
|
|
16
|
+
const Follow_Up_ID = require('./runtimeConfig').get('AIRTABLE_FOLLOW_UP_ID') || 'appBjKw1Ub0KkbZf0';
|
|
16
17
|
const Webinars_Leads_ID = require('./runtimeConfig').get('AIRTABLE_WEBINARS_LEADS_ID') || 'appzjpVXTI0TgqGPq';
|
|
17
18
|
const Product_ID = require('./runtimeConfig').get('AIRTABLE_PRODUCT_ID') || 'appu2YDW2pKDYLL5H';
|
|
18
19
|
|
|
@@ -30,6 +31,7 @@ const BASE_MAP = {
|
|
|
30
31
|
monitoreo: Monitoreo_ID,
|
|
31
32
|
programa: Programa_Juntas_ID,
|
|
32
33
|
symptoms: Symptoms_ID,
|
|
34
|
+
followup: Follow_Up_ID,
|
|
33
35
|
webinars: Webinars_Leads_ID,
|
|
34
36
|
product: Product_ID
|
|
35
37
|
};
|
|
@@ -44,6 +46,7 @@ module.exports = {
|
|
|
44
46
|
Monitoreo_ID,
|
|
45
47
|
Programa_Juntas_ID,
|
|
46
48
|
Symptoms_ID,
|
|
49
|
+
Follow_Up_ID,
|
|
47
50
|
Webinars_Leads_ID,
|
|
48
51
|
Product_ID,
|
|
49
52
|
// Helper function to get base by ID
|
|
@@ -13,7 +13,7 @@ const Message = mongoose.models.Message;
|
|
|
13
13
|
|
|
14
14
|
const getConversationController = async (req, res) => {
|
|
15
15
|
const startTime = Date.now();
|
|
16
|
-
logger.info(
|
|
16
|
+
logger.info(`Starting getConversationController at ${new Date().toISOString()}`);
|
|
17
17
|
|
|
18
18
|
try {
|
|
19
19
|
// Parse pagination parameters
|
|
@@ -34,7 +34,7 @@ const getConversationController = async (req, res) => {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
const messageCount = await Message.countDocuments({});
|
|
37
|
-
logger.info(
|
|
37
|
+
logger.info(`Total message count: ${messageCount}`);
|
|
38
38
|
|
|
39
39
|
if (messageCount === 0) {
|
|
40
40
|
logger.info('No messages found in database, returning empty conversations list');
|
|
@@ -63,7 +63,7 @@ const getConversationController = async (req, res) => {
|
|
|
63
63
|
const totalPages = Math.ceil(total / limit);
|
|
64
64
|
const totalTime = Date.now() - startTime;
|
|
65
65
|
|
|
66
|
-
logger.info(
|
|
66
|
+
logger.info(`Number of conversations found: ${conversations?.length || 0}`);
|
|
67
67
|
logger.info(`Total controller execution time: ${totalTime}ms`);
|
|
68
68
|
logger.info(`Filter: ${filter}, Pagination: ${conversations.length} of ${total} conversations (page ${page}/${totalPages})`);
|
|
69
69
|
|
|
@@ -83,8 +83,8 @@ const getConversationController = async (req, res) => {
|
|
|
83
83
|
logger.info('Response sent successfully!');
|
|
84
84
|
|
|
85
85
|
} catch (error) {
|
|
86
|
-
logger.error('Error fetching conversations:', error);
|
|
87
|
-
logger.error('Error stack:', error.stack);
|
|
86
|
+
logger.error('Error fetching conversations:', { error });
|
|
87
|
+
logger.error('Error stack:', { stack: error.stack });
|
|
88
88
|
res.status(500).json({
|
|
89
89
|
success: false,
|
|
90
90
|
error: error.message || 'Failed to fetch conversations'
|
|
@@ -93,10 +93,10 @@ const getConversationController = async (req, res) => {
|
|
|
93
93
|
};
|
|
94
94
|
|
|
95
95
|
const getConversationMessagesController = async (req, res) => {
|
|
96
|
-
logger.info(
|
|
96
|
+
logger.info(`Starting getConversationMessagesController at ${new Date().toISOString()}`);
|
|
97
97
|
try {
|
|
98
98
|
const { phoneNumber } = req.params;
|
|
99
|
-
logger.info(
|
|
99
|
+
logger.info(`Requested conversation for phone number: ${phoneNumber}`);
|
|
100
100
|
|
|
101
101
|
if (!phoneNumber) {
|
|
102
102
|
return res.status(400).json({
|
|
@@ -113,13 +113,12 @@ const getConversationMessagesController = async (req, res) => {
|
|
|
113
113
|
try {
|
|
114
114
|
query.createdAt = { $lt: new Date(before) };
|
|
115
115
|
} catch (parseError) {
|
|
116
|
-
logger.warn(
|
|
116
|
+
logger.warn(`Invalid date format for before parameter: ${before}`, { parseError });
|
|
117
117
|
query.createdAt = { $lt: before };
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
logger.info(
|
|
122
|
-
logger.info('Executing Message.find', { query });
|
|
121
|
+
logger.info(`Fetching conversation messages ${query}, limit ${limit}`);
|
|
123
122
|
let messages = [];
|
|
124
123
|
|
|
125
124
|
try {
|
|
@@ -136,18 +135,18 @@ const getConversationMessagesController = async (req, res) => {
|
|
|
136
135
|
|
|
137
136
|
const problematicMessages = messages.filter(msg => {
|
|
138
137
|
if (!msg || !msg.numero || !msg.createdAt) {
|
|
139
|
-
logger.warn(
|
|
138
|
+
logger.warn(`Found message missing required fields: ${msg?._id || 'unknown'}`);
|
|
140
139
|
return true;
|
|
141
140
|
}
|
|
142
141
|
|
|
143
142
|
if (msg.media) {
|
|
144
143
|
if (!msg.media || typeof msg.media !== 'object') {
|
|
145
|
-
logger.warn(
|
|
144
|
+
logger.warn(`Found media message with invalid media data: ${msg._id}`);
|
|
146
145
|
return true;
|
|
147
146
|
}
|
|
148
147
|
|
|
149
148
|
if (msg.media && (typeof msg.media.data === 'function' || msg.media.data instanceof Buffer)) {
|
|
150
|
-
logger.warn(
|
|
149
|
+
logger.warn(`Found media message with Buffer data that might cause serialization issues: ${msg._id}`);
|
|
151
150
|
return true;
|
|
152
151
|
}
|
|
153
152
|
}
|
|
@@ -157,13 +156,14 @@ const getConversationMessagesController = async (req, res) => {
|
|
|
157
156
|
|
|
158
157
|
if (problematicMessages.length > 0) {
|
|
159
158
|
logger.warn(`Found ${problematicMessages.length} potentially problematic messages`);
|
|
160
|
-
logger.info(
|
|
159
|
+
logger.info(`First problematic message IDs: ${problematicMessages.slice(0, 3).map(m => m?._id || 'unknown')}`);
|
|
161
160
|
}
|
|
162
161
|
} else {
|
|
163
162
|
logger.info('No messages found for this query');
|
|
164
163
|
}
|
|
165
164
|
} catch (err) {
|
|
166
|
-
logger.error('Database query error in message retrieval:', err);
|
|
165
|
+
logger.error('Database query error in message retrieval:', { err });
|
|
166
|
+
logger.error('Database query error in message retrieval:', { err });
|
|
167
167
|
messages = [];
|
|
168
168
|
}
|
|
169
169
|
|
|
@@ -174,7 +174,7 @@ const getConversationMessagesController = async (req, res) => {
|
|
|
174
174
|
JSON.stringify(msg);
|
|
175
175
|
return msg;
|
|
176
176
|
} catch (serializationError) {
|
|
177
|
-
logger.error(`Found non-serializable message with ID ${msg._id}:`, serializationError);
|
|
177
|
+
logger.error(`Found non-serializable message with ID ${msg._id}:`, { serializationError}) ;
|
|
178
178
|
return {
|
|
179
179
|
_id: msg._id?.toString() || 'unknown',
|
|
180
180
|
numero: msg.numero || phoneNumber,
|
|
@@ -197,7 +197,8 @@ const getConversationMessagesController = async (req, res) => {
|
|
|
197
197
|
});
|
|
198
198
|
logger.info('Successfully sent conversation messages response');
|
|
199
199
|
} catch (error) {
|
|
200
|
-
logger.error(`Error fetching conversation for ${req.params?.phoneNumber || 'unknown'}:`, error);
|
|
200
|
+
logger.error(`Error fetching conversation for ${req.params?.phoneNumber || 'unknown'}:`, { error });
|
|
201
|
+
logger.error(`Error fetching conversation for ${req.params?.phoneNumber || 'unknown'}:`, { error });
|
|
201
202
|
res.status(500).json({
|
|
202
203
|
success: false,
|
|
203
204
|
error: error.message || 'Failed to fetch conversation'
|
|
@@ -206,7 +207,7 @@ const getConversationMessagesController = async (req, res) => {
|
|
|
206
207
|
};
|
|
207
208
|
|
|
208
209
|
const getConversationReplyController = async (req, res) => {
|
|
209
|
-
logger.info(
|
|
210
|
+
logger.info(`Starting getConversationReplyController at ${new Date().toISOString()}`);
|
|
210
211
|
try {
|
|
211
212
|
const { phoneNumber, message, mediaData, contentSid, variables } = req.body;
|
|
212
213
|
logger.info('Reply request params:', {
|
|
@@ -225,7 +226,7 @@ const getConversationReplyController = async (req, res) => {
|
|
|
225
226
|
}
|
|
226
227
|
|
|
227
228
|
const formattedPhoneNumber = ensureWhatsAppFormat(phoneNumber);
|
|
228
|
-
logger.info(
|
|
229
|
+
logger.info(`Formatted phone number: ${formattedPhoneNumber}`);
|
|
229
230
|
|
|
230
231
|
const messageData = {
|
|
231
232
|
code: formattedPhoneNumber,
|
|
@@ -235,7 +236,7 @@ const getConversationReplyController = async (req, res) => {
|
|
|
235
236
|
|
|
236
237
|
// Handle template message (contentSid provided)
|
|
237
238
|
if (contentSid) {
|
|
238
|
-
logger.info(
|
|
239
|
+
logger.info(`Processing template message with contentSid: ${contentSid}`);
|
|
239
240
|
messageData.contentSid = contentSid;
|
|
240
241
|
|
|
241
242
|
if (variables && Object.keys(variables).length > 0) {
|
|
@@ -281,10 +282,12 @@ const getConversationReplyController = async (req, res) => {
|
|
|
281
282
|
message: 'Reply sent successfully'
|
|
282
283
|
});
|
|
283
284
|
} catch (error) {
|
|
284
|
-
logger.error('Error sending reply:', error);
|
|
285
|
+
logger.error('Error sending reply:', { error });
|
|
286
|
+
logger.error('Error sending reply:', { error });
|
|
285
287
|
logger.info('Request body', { body: req.body || {} });
|
|
286
288
|
const errorMsg = error.message || 'Failed to send reply';
|
|
287
|
-
logger.error('Responding with error:', errorMsg);
|
|
289
|
+
logger.error('Responding with error:', { errorMsg });
|
|
290
|
+
logger.error('Responding with error:', { errorMsg });
|
|
288
291
|
res.status(500).json({
|
|
289
292
|
success: false,
|
|
290
293
|
error: errorMsg
|
|
@@ -361,7 +364,8 @@ const searchConversationsController = async (req, res) => {
|
|
|
361
364
|
batch.map(p => `{whatsapp_id} = "${p}"`).join(', ') +
|
|
362
365
|
')';
|
|
363
366
|
return getRecordByFilter(Historial_Clinico_ID, 'estado_general', formula).catch(error => {
|
|
364
|
-
logger.error('Error fetching Airtable batch for search:', error);
|
|
367
|
+
logger.error('Error fetching Airtable batch for search:', { error });
|
|
368
|
+
logger.error('Error fetching Airtable batch for search:', { error });
|
|
365
369
|
return [];
|
|
366
370
|
});
|
|
367
371
|
});
|
|
@@ -380,7 +384,8 @@ const searchConversationsController = async (req, res) => {
|
|
|
380
384
|
});
|
|
381
385
|
logger.info(`Found ${Object.keys(airtableNameMap).length} names in Airtable for search results (${batches.length} batches)`);
|
|
382
386
|
} catch (error) {
|
|
383
|
-
logger.error('Error fetching names from Airtable for search, falling back to nombre_whatsapp:', error);
|
|
387
|
+
logger.error('Error fetching names from Airtable for search, falling back to nombre_whatsapp:', { error });
|
|
388
|
+
logger.error('Error fetching names from Airtable for search, falling back to nombre_whatsapp:', { error });
|
|
384
389
|
}
|
|
385
390
|
}
|
|
386
391
|
|
|
@@ -445,7 +450,8 @@ const searchConversationsController = async (req, res) => {
|
|
|
445
450
|
});
|
|
446
451
|
|
|
447
452
|
} catch (error) {
|
|
448
|
-
logger.error('Error searching conversations:', error);
|
|
453
|
+
logger.error('Error searching conversations:', { error });
|
|
454
|
+
logger.error('Error searching conversations:', { error });
|
|
449
455
|
res.status(500).json({
|
|
450
456
|
success: false,
|
|
451
457
|
error: error.message || 'Failed to search conversations'
|
|
@@ -478,7 +484,8 @@ const getConversationsByNameController = async (req, res) => {
|
|
|
478
484
|
}))
|
|
479
485
|
});
|
|
480
486
|
} catch (error) {
|
|
481
|
-
logger.error('Error fetching conversations by name:', error);
|
|
487
|
+
logger.error('Error fetching conversations by name:', { error });
|
|
488
|
+
logger.error('Error fetching conversations by name:', { error });
|
|
482
489
|
res.status(500).json({
|
|
483
490
|
success: false,
|
|
484
491
|
error: error.message || 'Failed to fetch conversations by name'
|
|
@@ -524,7 +531,8 @@ const getNewMessagesController = async (req, res) => {
|
|
|
524
531
|
messages: messages
|
|
525
532
|
});
|
|
526
533
|
} catch (error) {
|
|
527
|
-
logger.error(`Error fetching new messages for ${req.params.phoneNumber}:`, error);
|
|
534
|
+
logger.error(`Error fetching new messages for ${req.params.phoneNumber}:`, { error });
|
|
535
|
+
logger.error(`Error fetching new messages for ${req.params.phoneNumber}:`, { error });
|
|
528
536
|
res.status(500).json({
|
|
529
537
|
success: false,
|
|
530
538
|
error: error.message || 'Failed to fetch new messages'
|
|
@@ -533,7 +541,7 @@ const getNewMessagesController = async (req, res) => {
|
|
|
533
541
|
};
|
|
534
542
|
|
|
535
543
|
const markMessagesAsReadController = async (req, res) => {
|
|
536
|
-
logger.info(
|
|
544
|
+
logger.info(`Starting markMessagesAsReadController at ${new Date().toISOString()}`);
|
|
537
545
|
try {
|
|
538
546
|
const { phoneNumber } = req.params;
|
|
539
547
|
|
|
@@ -544,7 +552,7 @@ const markMessagesAsReadController = async (req, res) => {
|
|
|
544
552
|
});
|
|
545
553
|
}
|
|
546
554
|
|
|
547
|
-
logger.info(
|
|
555
|
+
logger.info(`Marking messages as read for phone number: ${phoneNumber}`);
|
|
548
556
|
const result = await Message.updateMany(
|
|
549
557
|
{
|
|
550
558
|
numero: phoneNumber,
|
|
@@ -563,8 +571,8 @@ const markMessagesAsReadController = async (req, res) => {
|
|
|
563
571
|
modifiedCount: result.nModified || result.modifiedCount
|
|
564
572
|
});
|
|
565
573
|
} catch (error) {
|
|
566
|
-
logger.error(`Error marking messages as read for ${req.params?.phoneNumber || 'unknown'}:`, error);
|
|
567
|
-
logger.error('Error stack:', error.stack);
|
|
574
|
+
logger.error(`Error marking messages as read for ${req.params?.phoneNumber || 'unknown'}:`, { error });
|
|
575
|
+
logger.error('Error stack:', { errorStack: error.stack });
|
|
568
576
|
res.status(500).json({
|
|
569
577
|
success: false,
|
|
570
578
|
error: error.message || 'Failed to mark messages as read'
|
|
@@ -573,7 +581,7 @@ const markMessagesAsReadController = async (req, res) => {
|
|
|
573
581
|
};
|
|
574
582
|
|
|
575
583
|
const sendTemplateToNewNumberController = async (req, res) => {
|
|
576
|
-
logger.info(
|
|
584
|
+
logger.info(`Starting sendTemplateToNewNumberController at ${new Date().toISOString()}`);
|
|
577
585
|
try {
|
|
578
586
|
const { phoneNumber, templateId, variables } = req.body;
|
|
579
587
|
|
|
@@ -618,7 +626,8 @@ const sendTemplateToNewNumberController = async (req, res) => {
|
|
|
618
626
|
messageId: message.sid
|
|
619
627
|
});
|
|
620
628
|
} catch (error) {
|
|
621
|
-
logger.error('Error sending template to new number:', error);
|
|
629
|
+
logger.error('Error sending template to new number:', { error });
|
|
630
|
+
logger.error('Error sending template to new number:', { error });
|
|
622
631
|
res.status(500).json({
|
|
623
632
|
success: false,
|
|
624
633
|
error: error.message || 'Failed to send template to new number'
|
|
@@ -627,14 +636,14 @@ const sendTemplateToNewNumberController = async (req, res) => {
|
|
|
627
636
|
};
|
|
628
637
|
|
|
629
638
|
const getOpenAIThreadMessagesController = async (req, res) => {
|
|
630
|
-
logger.info(
|
|
639
|
+
logger.info(`Starting getOpenAIThreadMessagesController at ${new Date().toISOString()}`);
|
|
631
640
|
try {
|
|
632
641
|
const { phoneNumber } = req.params;
|
|
633
642
|
const { limit = 50, order = 'desc', runId } = req.query;
|
|
634
643
|
const variant = process.env.VARIANT || 'assistants';
|
|
635
644
|
|
|
636
|
-
logger.info(
|
|
637
|
-
logger.info(
|
|
645
|
+
logger.info(`Fetching OpenAI thread messages for: ${phoneNumber}`);
|
|
646
|
+
logger.info(`Variant: ${variant}, Limit: ${limit}, Order: ${order}`);
|
|
638
647
|
|
|
639
648
|
if (!phoneNumber) {
|
|
640
649
|
return res.status(400).json({
|
|
@@ -649,7 +658,7 @@ const getOpenAIThreadMessagesController = async (req, res) => {
|
|
|
649
658
|
}).sort({ createdAt: -1 });
|
|
650
659
|
|
|
651
660
|
if (!thread) {
|
|
652
|
-
logger.info(
|
|
661
|
+
logger.info(`No active OpenAI thread found for: ${phoneNumber}`);
|
|
653
662
|
return res.status(404).json({
|
|
654
663
|
success: false,
|
|
655
664
|
error: 'No active OpenAI thread found for this phone number'
|
|
@@ -657,14 +666,14 @@ const getOpenAIThreadMessagesController = async (req, res) => {
|
|
|
657
666
|
}
|
|
658
667
|
|
|
659
668
|
let conversationId = thread.conversation_id;
|
|
660
|
-
logger.info(
|
|
669
|
+
logger.info(`Thread found - Conversation ID: ${conversationId}`);
|
|
661
670
|
|
|
662
671
|
const provider = llmConfig.getOpenAIProvider({ instantiate: true, variant });
|
|
663
672
|
if (!provider) {
|
|
664
673
|
throw new Error('OpenAI provider not initialized');
|
|
665
674
|
}
|
|
666
675
|
|
|
667
|
-
logger.info(
|
|
676
|
+
logger.info(`Using provider variant: ${provider.getVariant()}`);
|
|
668
677
|
|
|
669
678
|
const queryParams = {
|
|
670
679
|
threadId: conversationId,
|
|
@@ -679,7 +688,8 @@ const getOpenAIThreadMessagesController = async (req, res) => {
|
|
|
679
688
|
let messages;
|
|
680
689
|
let threadRecreated = false;
|
|
681
690
|
|
|
682
|
-
logger.info('Calling listMessages with params:', queryParams);
|
|
691
|
+
logger.info('Calling listMessages with params:', { queryParams });
|
|
692
|
+
logger.info('Calling listMessages with params:', { queryParams });
|
|
683
693
|
messages = await withThreadRecovery(
|
|
684
694
|
async (currentThread = thread) => {
|
|
685
695
|
if (currentThread !== thread) {
|
|
@@ -712,8 +722,8 @@ const getOpenAIThreadMessagesController = async (req, res) => {
|
|
|
712
722
|
});
|
|
713
723
|
|
|
714
724
|
} catch (error) {
|
|
715
|
-
logger.error('Error fetching OpenAI thread messages:', error);
|
|
716
|
-
logger.error('Error stack:', error.stack);
|
|
725
|
+
logger.error('Error fetching OpenAI thread messages:', { error });
|
|
726
|
+
logger.error('Error stack:', { errorStack: error.stack });
|
|
717
727
|
res.status(500).json({
|
|
718
728
|
success: false,
|
|
719
729
|
error: error.message || 'Failed to fetch OpenAI thread messages'
|
|
@@ -41,11 +41,11 @@ class NexusMessaging {
|
|
|
41
41
|
keyword: [],
|
|
42
42
|
flow: []
|
|
43
43
|
};
|
|
44
|
-
// Message
|
|
45
|
-
this.
|
|
44
|
+
// Message processing with check-after strategy
|
|
45
|
+
this.processingLocks = new Map(); // Per-chat locks to prevent parallel processing
|
|
46
46
|
this.batchingConfig = {
|
|
47
|
-
enabled: config.messageBatching?.enabled ??
|
|
48
|
-
|
|
47
|
+
enabled: config.messageBatching?.enabled ?? true, // Enabled by default with check-after
|
|
48
|
+
checkDelayMs: config.messageBatching?.checkDelayMs ?? 100 // Delay before checking for new messages
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
|
|
@@ -393,10 +393,9 @@ class NexusMessaging {
|
|
|
393
393
|
} else if (messageData.flow) {
|
|
394
394
|
return await this.handleFlow(messageData);
|
|
395
395
|
} else {
|
|
396
|
-
// For regular messages and media, use
|
|
397
|
-
logger.info('Batching config:', this.batchingConfig);
|
|
396
|
+
// For regular messages and media, use check-after processing
|
|
398
397
|
if (this.batchingConfig.enabled && chatId) {
|
|
399
|
-
return await this.
|
|
398
|
+
return await this._handleWithCheckAfter(chatId);
|
|
400
399
|
} else {
|
|
401
400
|
if (messageData.media) {
|
|
402
401
|
return await this.handleMedia(messageData);
|
|
@@ -649,77 +648,52 @@ class NexusMessaging {
|
|
|
649
648
|
}
|
|
650
649
|
|
|
651
650
|
/**
|
|
652
|
-
* Handle message with
|
|
651
|
+
* Handle message with check-after strategy - process immediately, check for new messages after
|
|
653
652
|
*/
|
|
654
|
-
async
|
|
655
|
-
|
|
656
|
-
if (
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
clearInterval(existing.typingInterval);
|
|
660
|
-
}
|
|
661
|
-
logger.info(`Received additional message from ${chatId}, resetting wait timer`);
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// Start typing indicator refresh for batching period
|
|
665
|
-
const typingInterval = await this._startTypingRefresh(chatId);
|
|
666
|
-
|
|
667
|
-
const waitTime = this.batchingConfig.baseWaitTime;
|
|
668
|
-
const timeoutId = setTimeout(async () => {
|
|
669
|
-
try {
|
|
670
|
-
if (typingInterval) {
|
|
671
|
-
clearInterval(typingInterval);
|
|
672
|
-
}
|
|
673
|
-
this.pendingResponses.delete(chatId);
|
|
674
|
-
await this._handleBatchedMessages(chatId);
|
|
675
|
-
} catch (error) {
|
|
676
|
-
logger.error(`Error handling batched messages for ${chatId}:`, error);
|
|
677
|
-
}
|
|
678
|
-
}, waitTime);
|
|
679
|
-
|
|
680
|
-
this.pendingResponses.set(chatId, { timeoutId, typingInterval });
|
|
681
|
-
logger.info(`Waiting ${Math.round(waitTime/1000)} seconds for more messages from ${chatId}`);
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
/**
|
|
685
|
-
* Start typing indicator refresh interval
|
|
686
|
-
*/
|
|
687
|
-
async _startTypingRefresh(chatId) {
|
|
688
|
-
if (!this.provider || typeof this.provider.sendTypingIndicator !== 'function') {
|
|
689
|
-
return null;
|
|
653
|
+
async _handleWithCheckAfter(chatId) {
|
|
654
|
+
// If already processing this chat, just return (message is saved, will be picked up)
|
|
655
|
+
if (this.processingLocks.has(chatId)) {
|
|
656
|
+
logger.info(`[CheckAfter] Already processing ${chatId}, new message will be included`);
|
|
657
|
+
return;
|
|
690
658
|
}
|
|
691
659
|
|
|
692
|
-
|
|
693
|
-
numero: chatId,
|
|
694
|
-
from_me: false,
|
|
695
|
-
message_id: { $exists: true, $ne: null }
|
|
696
|
-
}).sort({ createdAt: -1 });
|
|
697
|
-
|
|
698
|
-
if (!lastMessage?.message_id) return null;
|
|
699
|
-
|
|
700
|
-
return setInterval(() =>
|
|
701
|
-
this.provider.sendTypingIndicator(lastMessage.message_id).catch(err =>
|
|
702
|
-
logger.debug('[_startTypingRefresh] Failed', { error: err.message })
|
|
703
|
-
), 5000
|
|
704
|
-
);
|
|
660
|
+
await this._processWithLock(chatId);
|
|
705
661
|
}
|
|
706
662
|
|
|
707
663
|
/**
|
|
708
|
-
* Process
|
|
664
|
+
* Process messages with per-chat lock and check-after logic
|
|
709
665
|
*/
|
|
710
|
-
async
|
|
666
|
+
async _processWithLock(chatId) {
|
|
667
|
+
this.processingLocks.set(chatId, true);
|
|
711
668
|
let typingInterval = null;
|
|
712
|
-
|
|
669
|
+
|
|
713
670
|
try {
|
|
714
|
-
logger.info(`Processing batched messages from ${chatId} (including media if any)`);
|
|
715
|
-
|
|
716
671
|
typingInterval = await this._startTypingRefresh(chatId);
|
|
717
|
-
|
|
718
|
-
|
|
672
|
+
logger.info(`[CheckAfter] Processing messages for ${chatId}`);
|
|
673
|
+
|
|
674
|
+
// Process with assistant
|
|
719
675
|
const result = await replyAssistant(chatId);
|
|
720
676
|
const botResponse = typeof result === 'string' ? result : result?.output;
|
|
721
677
|
const tools_executed = typeof result === 'object' ? result?.tools_executed : undefined;
|
|
722
|
-
|
|
678
|
+
|
|
679
|
+
// Small delay to catch very recent DB writes
|
|
680
|
+
await new Promise(resolve => setTimeout(resolve, this.batchingConfig.checkDelayMs));
|
|
681
|
+
|
|
682
|
+
// Check for new unprocessed messages
|
|
683
|
+
const hasNewMessages = await Message.exists({
|
|
684
|
+
numero: chatId,
|
|
685
|
+
processed: false,
|
|
686
|
+
from_me: false
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
if (hasNewMessages) {
|
|
690
|
+
logger.info(`[CheckAfter] New messages detected for ${chatId}, discarding response and reprocessing`);
|
|
691
|
+
if (typingInterval) clearInterval(typingInterval);
|
|
692
|
+
// Recursively process with new messages
|
|
693
|
+
return await this._processWithLock(chatId);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// No new messages - send response and mark processed
|
|
723
697
|
if (botResponse) {
|
|
724
698
|
await this.sendMessage({
|
|
725
699
|
code: chatId,
|
|
@@ -729,40 +703,45 @@ class NexusMessaging {
|
|
|
729
703
|
tools_executed
|
|
730
704
|
});
|
|
731
705
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
706
|
+
|
|
707
|
+
this.events.emit('messages:processed', { chatId, response: botResponse });
|
|
708
|
+
|
|
736
709
|
} catch (error) {
|
|
737
|
-
logger.error('Error
|
|
710
|
+
logger.error('[CheckAfter] Error processing messages:', { chatId, error: error.message });
|
|
738
711
|
} finally {
|
|
739
|
-
if (typingInterval)
|
|
740
|
-
|
|
741
|
-
}
|
|
712
|
+
if (typingInterval) clearInterval(typingInterval);
|
|
713
|
+
this.processingLocks.delete(chatId);
|
|
742
714
|
}
|
|
743
715
|
}
|
|
744
716
|
|
|
745
717
|
/**
|
|
746
|
-
*
|
|
718
|
+
* Start typing indicator refresh interval
|
|
747
719
|
*/
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
if (pending.timeoutId) {
|
|
752
|
-
clearTimeout(pending.timeoutId);
|
|
753
|
-
}
|
|
754
|
-
if (pending.typingInterval) {
|
|
755
|
-
clearInterval(pending.typingInterval);
|
|
756
|
-
}
|
|
757
|
-
this.pendingResponses.delete(chatId);
|
|
720
|
+
async _startTypingRefresh(chatId) {
|
|
721
|
+
if (!this.provider || typeof this.provider.sendTypingIndicator !== 'function') {
|
|
722
|
+
return null;
|
|
758
723
|
}
|
|
724
|
+
|
|
725
|
+
const lastMessage = await Message.findOne({
|
|
726
|
+
numero: chatId,
|
|
727
|
+
from_me: false,
|
|
728
|
+
message_id: { $exists: true, $ne: null }
|
|
729
|
+
}).sort({ createdAt: -1 });
|
|
730
|
+
|
|
731
|
+
if (!lastMessage?.message_id) return null;
|
|
732
|
+
|
|
733
|
+
return setInterval(() =>
|
|
734
|
+
this.provider.sendTypingIndicator(lastMessage.message_id).catch(err =>
|
|
735
|
+
logger.debug('[_startTypingRefresh] Failed', { error: err.message })
|
|
736
|
+
), 5000
|
|
737
|
+
);
|
|
759
738
|
}
|
|
760
739
|
|
|
761
740
|
/**
|
|
762
|
-
*
|
|
741
|
+
* Check if chat is currently being processed
|
|
763
742
|
*/
|
|
764
|
-
|
|
765
|
-
return this.
|
|
743
|
+
isProcessing(chatId) {
|
|
744
|
+
return this.processingLocks.has(chatId);
|
|
766
745
|
}
|
|
767
746
|
}
|
|
768
747
|
|
|
@@ -2,31 +2,6 @@ const moment = require('moment-timezone');
|
|
|
2
2
|
const { Message } = require('../models/messageModel.js');
|
|
3
3
|
const { logger } = require('../utils/logger');
|
|
4
4
|
|
|
5
|
-
const addMessageToThread = async (reply, messagesChat, provider, thread) => {
|
|
6
|
-
const threadId = thread.getConversationId();
|
|
7
|
-
|
|
8
|
-
if (reply.interactive_type === 'flow') {
|
|
9
|
-
logger.info(`[addMessageToThread] Skipping flow message (UI only) - ID: ${reply.message_id}`);
|
|
10
|
-
return;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
if (reply.origin === 'whatsapp_platform') {
|
|
14
|
-
await provider.addMessage({
|
|
15
|
-
threadId,
|
|
16
|
-
role: 'assistant',
|
|
17
|
-
content: messagesChat
|
|
18
|
-
});
|
|
19
|
-
} else if (reply.origin === 'patient') {
|
|
20
|
-
await provider.addMessage({
|
|
21
|
-
threadId,
|
|
22
|
-
role: 'user',
|
|
23
|
-
content: messagesChat
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
logger.info(`[addMessageToThread] Message added - ID: ${reply.message_id}, Thread: ${threadId}, Origin: ${reply.origin}`);
|
|
28
|
-
};
|
|
29
|
-
|
|
30
5
|
const updateMessageRecord = async (reply, thread) => {
|
|
31
6
|
const threadId = thread.getConversationId();
|
|
32
7
|
|
|
@@ -125,7 +100,6 @@ async function isRecentMessage(chatId) {
|
|
|
125
100
|
}
|
|
126
101
|
|
|
127
102
|
module.exports = {
|
|
128
|
-
addMessageToThread,
|
|
129
103
|
updateMessageRecord,
|
|
130
104
|
getLastMessages,
|
|
131
105
|
getLastNMessages,
|
|
@@ -80,6 +80,12 @@ messageSchema.index({ numero: 1, createdAt: -1 });
|
|
|
80
80
|
messageSchema.index({ numero: 1, processed: 1, origin: 1 }, { name: 'numero_processed_origin_idx' });
|
|
81
81
|
messageSchema.index({ numero: 1, createdAt: -1, processed: 1 }, { name: 'numero_created_processed_idx' });
|
|
82
82
|
|
|
83
|
+
// Indexes for conversation aggregation queries
|
|
84
|
+
messageSchema.index({ group_id: 1, createdAt: 1 }, { name: 'conversation_sort_idx' });
|
|
85
|
+
messageSchema.index({ group_id: 1, from_me: 1, read: 1 }, { name: 'unread_filter_idx' });
|
|
86
|
+
messageSchema.index({ group_id: 1, numero: 1, createdAt: -1 }, { name: 'conversation_lookup_idx' });
|
|
87
|
+
messageSchema.index({ createdAt: -1 }, { name: 'global_sort_idx' });
|
|
88
|
+
|
|
83
89
|
messageSchema.pre('save', function (next) {
|
|
84
90
|
if (this.timestamp) {
|
|
85
91
|
this.timestamp = moment.tz(this.timestamp, 'America/Mexico_City').toDate();
|