@peopl-health/nexus 3.0.1 → 3.0.3
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/core/NexusMessaging.js +67 -87
- package/lib/models/messageModel.js +6 -0
- package/lib/providers/OpenAIResponsesProvider.js +0 -3
- package/lib/services/DefaultConversationManager.js +8 -5
- package/lib/services/conversationService.js +7 -6
- 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
|
});
|
|
@@ -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
|
|
|
@@ -348,6 +348,7 @@ class NexusMessaging {
|
|
|
348
348
|
if (this.messageStorage) {
|
|
349
349
|
await this.messageStorage.saveMessage({
|
|
350
350
|
...messageData,
|
|
351
|
+
messageId: messageData.id,
|
|
351
352
|
timestamp: new Date(),
|
|
352
353
|
fromMe: false,
|
|
353
354
|
origin: 'patient'
|
|
@@ -393,10 +394,9 @@ class NexusMessaging {
|
|
|
393
394
|
} else if (messageData.flow) {
|
|
394
395
|
return await this.handleFlow(messageData);
|
|
395
396
|
} else {
|
|
396
|
-
// For regular messages and media, use
|
|
397
|
-
logger.info('Batching config:', this.batchingConfig);
|
|
397
|
+
// For regular messages and media, use check-after processing
|
|
398
398
|
if (this.batchingConfig.enabled && chatId) {
|
|
399
|
-
return await this.
|
|
399
|
+
return await this._handleWithCheckAfter(chatId);
|
|
400
400
|
} else {
|
|
401
401
|
if (messageData.media) {
|
|
402
402
|
return await this.handleMedia(messageData);
|
|
@@ -649,77 +649,52 @@ class NexusMessaging {
|
|
|
649
649
|
}
|
|
650
650
|
|
|
651
651
|
/**
|
|
652
|
-
* Handle message with
|
|
652
|
+
* Handle message with check-after strategy - process immediately, check for new messages after
|
|
653
653
|
*/
|
|
654
|
-
async
|
|
655
|
-
const existing = this.pendingResponses.get(chatId);
|
|
656
|
-
if (existing) {
|
|
657
|
-
clearTimeout(existing.timeoutId);
|
|
658
|
-
if (existing.typingInterval) {
|
|
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
|
|
654
|
+
async _handleWithCheckAfter(chatId) {
|
|
665
655
|
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
656
|
|
|
680
|
-
this.
|
|
681
|
-
|
|
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;
|
|
657
|
+
if (this.processingLocks.has(chatId)) {
|
|
658
|
+
logger.info(`[CheckAfter] Already processing ${chatId}, new message will be included`);
|
|
659
|
+
return;
|
|
690
660
|
}
|
|
691
661
|
|
|
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
|
-
);
|
|
662
|
+
await this._processWithLock(chatId, typingInterval);
|
|
705
663
|
}
|
|
706
664
|
|
|
707
665
|
/**
|
|
708
|
-
* Process
|
|
666
|
+
* Process messages with per-chat lock and check-after logic
|
|
709
667
|
*/
|
|
710
|
-
async
|
|
711
|
-
|
|
712
|
-
|
|
668
|
+
async _processWithLock(chatId, existingTypingInterval = null) {
|
|
669
|
+
this.processingLocks.set(chatId, true);
|
|
670
|
+
let typingInterval = existingTypingInterval;
|
|
671
|
+
|
|
713
672
|
try {
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
673
|
+
if (!typingInterval) {
|
|
674
|
+
typingInterval = await this._startTypingRefresh(chatId);
|
|
675
|
+
}
|
|
676
|
+
logger.info(`[CheckAfter] Processing messages for ${chatId}`);
|
|
677
|
+
|
|
719
678
|
const result = await replyAssistant(chatId);
|
|
720
679
|
const botResponse = typeof result === 'string' ? result : result?.output;
|
|
721
680
|
const tools_executed = typeof result === 'object' ? result?.tools_executed : undefined;
|
|
722
|
-
|
|
681
|
+
|
|
682
|
+
// Small delay to catch very recent DB writes
|
|
683
|
+
await new Promise(resolve => setTimeout(resolve, this.batchingConfig.checkDelayMs));
|
|
684
|
+
|
|
685
|
+
// Check for new unprocessed messages
|
|
686
|
+
const hasNewMessages = await Message.exists({
|
|
687
|
+
numero: chatId,
|
|
688
|
+
processed: false,
|
|
689
|
+
from_me: false
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
if (hasNewMessages) {
|
|
693
|
+
logger.info(`[CheckAfter] New messages detected for ${chatId}, discarding response and reprocessing`);
|
|
694
|
+
return await this._processWithLock(chatId, typingInterval);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// No new messages - send response and mark processed
|
|
723
698
|
if (botResponse) {
|
|
724
699
|
await this.sendMessage({
|
|
725
700
|
code: chatId,
|
|
@@ -729,40 +704,45 @@ class NexusMessaging {
|
|
|
729
704
|
tools_executed
|
|
730
705
|
});
|
|
731
706
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
707
|
+
|
|
708
|
+
this.events.emit('messages:processed', { chatId, response: botResponse });
|
|
709
|
+
|
|
736
710
|
} catch (error) {
|
|
737
|
-
logger.error('Error
|
|
711
|
+
logger.error('[CheckAfter] Error processing messages:', { chatId, error: error.message });
|
|
738
712
|
} finally {
|
|
739
|
-
if (typingInterval)
|
|
740
|
-
|
|
741
|
-
}
|
|
713
|
+
if (typingInterval) clearInterval(typingInterval);
|
|
714
|
+
this.processingLocks.delete(chatId);
|
|
742
715
|
}
|
|
743
716
|
}
|
|
744
717
|
|
|
745
718
|
/**
|
|
746
|
-
*
|
|
719
|
+
* Start typing indicator refresh interval
|
|
747
720
|
*/
|
|
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);
|
|
721
|
+
async _startTypingRefresh(chatId) {
|
|
722
|
+
if (!this.provider || typeof this.provider.sendTypingIndicator !== 'function') {
|
|
723
|
+
return null;
|
|
758
724
|
}
|
|
725
|
+
|
|
726
|
+
const lastMessage = await Message.findOne({
|
|
727
|
+
numero: chatId,
|
|
728
|
+
from_me: false,
|
|
729
|
+
message_id: { $exists: true, $ne: null }
|
|
730
|
+
}).sort({ createdAt: -1 });
|
|
731
|
+
|
|
732
|
+
if (!lastMessage?.message_id) return null;
|
|
733
|
+
|
|
734
|
+
return setInterval(() =>
|
|
735
|
+
this.provider.sendTypingIndicator(lastMessage.message_id).catch(err =>
|
|
736
|
+
logger.debug('[_startTypingRefresh] Failed', { error: err.message })
|
|
737
|
+
), 5000
|
|
738
|
+
);
|
|
759
739
|
}
|
|
760
740
|
|
|
761
741
|
/**
|
|
762
|
-
*
|
|
742
|
+
* Check if chat is currently being processed
|
|
763
743
|
*/
|
|
764
|
-
|
|
765
|
-
return this.
|
|
744
|
+
isProcessing(chatId) {
|
|
745
|
+
return this.processingLocks.has(chatId);
|
|
766
746
|
}
|
|
767
747
|
}
|
|
768
748
|
|
|
@@ -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();
|
|
@@ -287,10 +287,7 @@ class OpenAIResponsesProvider {
|
|
|
287
287
|
const makeAPICall = (inputData) => retryWithBackoff(() =>
|
|
288
288
|
this.client.responses.create({
|
|
289
289
|
prompt: promptConfig,
|
|
290
|
-
model: model || this.defaults.responseModel,
|
|
291
|
-
instructions: additionalInstructions || instructions,
|
|
292
290
|
input: inputData,
|
|
293
|
-
metadata, top_p: topP, temperature, max_output_tokens: maxOutputTokens,
|
|
294
291
|
truncation: truncationStrategy,
|
|
295
292
|
}), { providerName: PROVIDER_NAME });
|
|
296
293
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const { ConversationManager } = require('./ConversationManager');
|
|
2
|
-
const { getLastNMessages } = require('../helpers/messageHelper');
|
|
2
|
+
const { getLastNMessages, formatMessage } = require('../helpers/messageHelper');
|
|
3
3
|
const { handlePendingFunctionCalls: handlePendingFunctionCallsUtil } = require('../providers/OpenAIResponsesProviderTools');
|
|
4
4
|
const { getRecordByFilter } = require('./airtableService');
|
|
5
5
|
const { Follow_Up_ID } = require('../config/airtableConfig');
|
|
@@ -23,10 +23,13 @@ class DefaultConversationManager extends ConversationManager {
|
|
|
23
23
|
return additionalMessages;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
const messageContext = allMessages.reverse().map(msg =>
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
const messageContext = allMessages.reverse().map(msg => {
|
|
27
|
+
const formattedContent = formatMessage(msg);
|
|
28
|
+
return {
|
|
29
|
+
role: msg.origin === 'patient' ? 'user' : 'assistant',
|
|
30
|
+
content: formattedContent || msg.body || msg.content || ''
|
|
31
|
+
};
|
|
32
|
+
});
|
|
30
33
|
|
|
31
34
|
return [...additionalMessages, ...messageContext];
|
|
32
35
|
} catch (error) {
|
|
@@ -41,22 +41,23 @@ const fetchConversationData = async (filter, skip, limit) => {
|
|
|
41
41
|
},
|
|
42
42
|
{ $project: { threadInfo: 0 } },
|
|
43
43
|
...(filter === 'pending-review' ? [{ $match: { $or: [{ review: false }, { review: null }] } }] : []),
|
|
44
|
-
{ $sort: { 'latestMessage.createdAt': -1
|
|
44
|
+
{ $sort: { 'latestMessage.createdAt': -1 } },
|
|
45
45
|
{ $skip: skip },
|
|
46
46
|
{ $limit: limit }
|
|
47
47
|
];
|
|
48
48
|
|
|
49
49
|
const startTime = Date.now();
|
|
50
50
|
const [conversations, contactNames, unreadCounts, totalResult] = await Promise.all([
|
|
51
|
-
Message.aggregate(pipeline),
|
|
52
|
-
Message.aggregate([{ $match: { ...baseMatch, from_me: false } }, { $sort: { createdAt: -1 } }, { $group: { _id: '$numero', name: { $first: '$nombre_whatsapp' } } }]),
|
|
53
|
-
Message.aggregate([{ $match: { ...baseMatch, ...unreadMatch } }, { $group: { _id: '$numero', unreadCount: { $sum: 1 } } }]),
|
|
51
|
+
Message.aggregate(pipeline, { allowDiskUse: true }),
|
|
52
|
+
Message.aggregate([{ $match: { ...baseMatch, from_me: false } }, { $sort: { createdAt: -1 } }, { $group: { _id: '$numero', name: { $first: '$nombre_whatsapp' } } }], { allowDiskUse: true }),
|
|
53
|
+
Message.aggregate([{ $match: { ...baseMatch, ...unreadMatch } }, { $group: { _id: '$numero', unreadCount: { $sum: 1 } } }], { allowDiskUse: true }),
|
|
54
54
|
Message.aggregate(
|
|
55
55
|
filter === 'no-response'
|
|
56
|
-
? [{ $match: baseMatch }, { $project: { numero: 1, from_me: 1, createdAt: 1
|
|
56
|
+
? [{ $match: baseMatch }, { $project: { numero: 1, from_me: 1, createdAt: 1 } }, { $sort: { createdAt: -1 } }, { $group: { _id: '$numero', latestMessage: { $first: '$$ROOT' } } }, { $match: { 'latestMessage.from_me': false } }, { $count: 'total' }]
|
|
57
57
|
: filter === 'pending-review'
|
|
58
58
|
? [{ $match: baseMatch }, { $group: { _id: '$numero' } }, { $lookup: { from: 'threads', localField: '_id', foreignField: 'code', as: 'threadInfo' } }, { $addFields: { review: { $arrayElemAt: ['$threadInfo.review', 0] } } }, { $match: { $or: [{ review: false }, { review: null }] } }, { $count: 'total' }]
|
|
59
|
-
: [{ $match: filterConditions }, { $group: { _id: '$numero' } }, { $count: 'total' }]
|
|
59
|
+
: [{ $match: filterConditions }, { $group: { _id: '$numero' } }, { $count: 'total' }],
|
|
60
|
+
{ allowDiskUse: true }
|
|
60
61
|
)
|
|
61
62
|
]);
|
|
62
63
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peopl-health/nexus",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.3",
|
|
4
4
|
"description": "Core messaging and assistant library for WhatsApp communication platforms",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"whatsapp",
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
"peerDependencies": {
|
|
106
106
|
"@anthropic-ai/sdk": "^0.32.0",
|
|
107
107
|
"baileys": "^6.4.0",
|
|
108
|
-
"express": "4.22.1",
|
|
108
|
+
"express": "^4.22.1",
|
|
109
109
|
"openai": "6.7.0",
|
|
110
110
|
"twilio": "5.6.0"
|
|
111
111
|
},
|