@peopl-health/nexus 3.0.3 → 3.0.5
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/adapters/BaileysProvider.js +2 -6
- package/lib/adapters/TwilioProvider.js +2 -6
- package/lib/core/NexusMessaging.js +80 -19
- package/lib/helpers/mediaHelper.js +2 -2
- package/lib/helpers/messageHelper.js +25 -8
- package/lib/helpers/processHelper.js +3 -9
- package/lib/helpers/qrHelper.js +1 -7
- package/lib/helpers/twilioHelper.js +12 -12
- package/lib/helpers/twilioMediaProcessor.js +5 -0
- package/lib/helpers/whatsappHelper.js +1 -41
- package/lib/index.d.ts +0 -1
- package/lib/{services/DefaultConversationManager.js → memory/DefaultMemoryManager.js} +9 -10
- package/lib/{services/ConversationManager.js → memory/MemoryManager.js} +3 -3
- package/lib/providers/OpenAIResponsesProvider.js +11 -3
- package/lib/providers/createProvider.js +2 -2
- package/lib/services/assistantServiceCore.js +13 -9
- package/lib/utils/dateUtils.js +9 -1
- package/lib/utils/index.js +2 -0
- package/lib/utils/scheduleUtils.js +57 -0
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { MessageProvider } = require('../core/MessageProvider');
|
|
2
2
|
const { logger } = require('../utils/logger');
|
|
3
|
+
const { calculateDelay } = require('../utils/scheduleUtils');
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Baileys WhatsApp messaging provider
|
|
@@ -150,7 +151,7 @@ class BaileysProvider extends MessageProvider {
|
|
|
150
151
|
|
|
151
152
|
async sendScheduledMessage(scheduledMessage) {
|
|
152
153
|
const { sendTime, timeZone, __nexusSend } = scheduledMessage;
|
|
153
|
-
const delay =
|
|
154
|
+
const delay = calculateDelay(sendTime, timeZone);
|
|
154
155
|
|
|
155
156
|
const sender = typeof __nexusSend === 'function'
|
|
156
157
|
? __nexusSend
|
|
@@ -181,11 +182,6 @@ class BaileysProvider extends MessageProvider {
|
|
|
181
182
|
return { scheduled: true, delay };
|
|
182
183
|
}
|
|
183
184
|
|
|
184
|
-
calculateDelay(sendTime) {
|
|
185
|
-
const now = new Date();
|
|
186
|
-
const targetTime = new Date(sendTime);
|
|
187
|
-
return Math.max(0, targetTime.getTime() - now.getTime());
|
|
188
|
-
}
|
|
189
185
|
|
|
190
186
|
getConnectionStatus() {
|
|
191
187
|
return this.waSocket?.user ? true : false;
|
|
@@ -7,6 +7,7 @@ const { sanitizeMediaFilename } = require('../utils/sanitizer');
|
|
|
7
7
|
const { generatePresignedUrl } = require('../config/awsConfig');
|
|
8
8
|
const { validateMedia, getMediaType } = require('../utils/mediaValidator');
|
|
9
9
|
const { logger } = require('../utils/logger');
|
|
10
|
+
const { calculateDelay } = require('../utils/scheduleUtils');
|
|
10
11
|
const { v4: uuidv4 } = require('uuid');
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -220,7 +221,7 @@ class TwilioProvider extends MessageProvider {
|
|
|
220
221
|
|
|
221
222
|
async sendScheduledMessage(scheduledMessage) {
|
|
222
223
|
const { sendTime, timeZone, __nexusSend } = scheduledMessage;
|
|
223
|
-
const delay =
|
|
224
|
+
const delay = calculateDelay(sendTime, timeZone);
|
|
224
225
|
|
|
225
226
|
const sender = typeof __nexusSend === 'function'
|
|
226
227
|
? __nexusSend
|
|
@@ -423,11 +424,6 @@ class TwilioProvider extends MessageProvider {
|
|
|
423
424
|
}
|
|
424
425
|
}
|
|
425
426
|
|
|
426
|
-
calculateDelay(sendTime) {
|
|
427
|
-
const now = new Date();
|
|
428
|
-
const targetTime = new Date(sendTime);
|
|
429
|
-
return Math.max(0, targetTime.getTime() - now.getTime());
|
|
430
|
-
}
|
|
431
427
|
|
|
432
428
|
/**
|
|
433
429
|
* Split a message into chunks at sentence boundaries, respecting Twilio's character limit
|
|
@@ -43,9 +43,14 @@ class NexusMessaging {
|
|
|
43
43
|
};
|
|
44
44
|
// Message processing with check-after strategy
|
|
45
45
|
this.processingLocks = new Map(); // Per-chat locks to prevent parallel processing
|
|
46
|
+
this.activeRequests = new Map(); // Track active AI requests per chat
|
|
47
|
+
this.abandonedRuns = new Set(); // Track runs that should be ignored
|
|
46
48
|
this.batchingConfig = {
|
|
47
49
|
enabled: config.messageBatching?.enabled ?? true, // Enabled by default with check-after
|
|
48
|
-
|
|
50
|
+
abortOnNewMessage: config.messageBatching?.abortOnNewMessage ?? true, // Abort ongoing AI calls when new messages arrive
|
|
51
|
+
immediateRestart: config.messageBatching?.immediateRestart ?? true, // Start new processing immediately without waiting
|
|
52
|
+
batchWindowMs: config.messageBatching?.batchWindowMs ?? 2000, // Wait up to 2s for message bursts
|
|
53
|
+
maxBatchWait: config.messageBatching?.maxBatchWait ?? 5000 // Maximum time to wait for batching
|
|
49
54
|
};
|
|
50
55
|
}
|
|
51
56
|
|
|
@@ -656,6 +661,20 @@ class NexusMessaging {
|
|
|
656
661
|
|
|
657
662
|
if (this.processingLocks.has(chatId)) {
|
|
658
663
|
logger.info(`[CheckAfter] Already processing ${chatId}, new message will be included`);
|
|
664
|
+
|
|
665
|
+
if (this.batchingConfig.abortOnNewMessage && this.activeRequests.has(chatId)) {
|
|
666
|
+
const runId = this.activeRequests.get(chatId);
|
|
667
|
+
this.abandonedRuns.add(runId);
|
|
668
|
+
logger.info(`[CheckAfter] Marked run ${runId} as abandoned for ${chatId}`);
|
|
669
|
+
|
|
670
|
+
if (this.batchingConfig.immediateRestart) {
|
|
671
|
+
this.processingLocks.delete(chatId);
|
|
672
|
+
this.activeRequests.delete(chatId);
|
|
673
|
+
|
|
674
|
+
logger.info(`[CheckAfter] Starting immediate reprocessing for ${chatId}`);
|
|
675
|
+
await this._processWithLock(chatId, null);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
659
678
|
return;
|
|
660
679
|
}
|
|
661
680
|
|
|
@@ -668,33 +687,53 @@ class NexusMessaging {
|
|
|
668
687
|
async _processWithLock(chatId, existingTypingInterval = null) {
|
|
669
688
|
this.processingLocks.set(chatId, true);
|
|
670
689
|
let typingInterval = existingTypingInterval;
|
|
690
|
+
let runId = null;
|
|
671
691
|
|
|
672
692
|
try {
|
|
673
693
|
if (!typingInterval) {
|
|
674
694
|
typingInterval = await this._startTypingRefresh(chatId);
|
|
675
695
|
}
|
|
676
|
-
|
|
696
|
+
|
|
697
|
+
const startTime = Date.now();
|
|
698
|
+
let messageCount = await this._getUnprocessedMessageCount(chatId);
|
|
699
|
+
let lastCount = messageCount;
|
|
700
|
+
|
|
701
|
+
while (Date.now() - startTime < this.batchingConfig.batchWindowMs) {
|
|
702
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
703
|
+
const newCount = await this._getUnprocessedMessageCount(chatId);
|
|
704
|
+
|
|
705
|
+
if (newCount > lastCount) {
|
|
706
|
+
lastCount = newCount;
|
|
707
|
+
logger.info(`[Batching] New message detected for ${chatId}, extending wait`);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (Date.now() - startTime >= this.batchingConfig.maxBatchWait) {
|
|
711
|
+
logger.info(`[Batching] Max wait reached for ${chatId}`);
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
logger.info(`[CheckAfter] Processing ${lastCount} messages for ${chatId} after batching`);
|
|
717
|
+
|
|
718
|
+
runId = `run_${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
719
|
+
this.activeRequests.set(chatId, runId);
|
|
677
720
|
|
|
678
|
-
const result = await replyAssistant(chatId);
|
|
721
|
+
const result = await replyAssistant(chatId, null, null, { runId });
|
|
722
|
+
|
|
723
|
+
if (this.abandonedRuns.has(runId)) {
|
|
724
|
+
logger.info(`[CheckAfter] Discarding abandoned run ${runId} for ${chatId}`);
|
|
725
|
+
this.abandonedRuns.delete(runId);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
|
|
679
729
|
const botResponse = typeof result === 'string' ? result : result?.output;
|
|
680
730
|
const tools_executed = typeof result === 'object' ? result?.tools_executed : undefined;
|
|
681
731
|
|
|
682
|
-
|
|
683
|
-
|
|
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);
|
|
732
|
+
if (typingInterval) {
|
|
733
|
+
clearInterval(typingInterval);
|
|
734
|
+
typingInterval = null;
|
|
695
735
|
}
|
|
696
736
|
|
|
697
|
-
// No new messages - send response and mark processed
|
|
698
737
|
if (botResponse) {
|
|
699
738
|
await this.sendMessage({
|
|
700
739
|
code: chatId,
|
|
@@ -712,9 +751,25 @@ class NexusMessaging {
|
|
|
712
751
|
} finally {
|
|
713
752
|
if (typingInterval) clearInterval(typingInterval);
|
|
714
753
|
this.processingLocks.delete(chatId);
|
|
754
|
+
this.activeRequests.delete(chatId);
|
|
755
|
+
if (this.abandonedRuns.size > 100) {
|
|
756
|
+
this.abandonedRuns.clear();
|
|
757
|
+
}
|
|
715
758
|
}
|
|
716
759
|
}
|
|
717
760
|
|
|
761
|
+
/**
|
|
762
|
+
* Get count of unprocessed messages for a chat
|
|
763
|
+
*/
|
|
764
|
+
async _getUnprocessedMessageCount(chatId) {
|
|
765
|
+
const { Message } = require('../models/messageModel');
|
|
766
|
+
return await Message.countDocuments({
|
|
767
|
+
numero: chatId,
|
|
768
|
+
processed: false,
|
|
769
|
+
from_me: false
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
|
|
718
773
|
/**
|
|
719
774
|
* Start typing indicator refresh interval
|
|
720
775
|
*/
|
|
@@ -726,10 +781,16 @@ class NexusMessaging {
|
|
|
726
781
|
const lastMessage = await Message.findOne({
|
|
727
782
|
numero: chatId,
|
|
728
783
|
from_me: false,
|
|
729
|
-
|
|
784
|
+
processed: false,
|
|
785
|
+
message_id: { $exists: true, $ne: null, $not: /^pending-/ }
|
|
730
786
|
}).sort({ createdAt: -1 });
|
|
731
787
|
|
|
732
|
-
if (!lastMessage?.message_id)
|
|
788
|
+
if (!lastMessage?.message_id) {
|
|
789
|
+
logger.debug(`[_startTypingRefresh] No valid message for typing indicator: ${chatId}`);
|
|
790
|
+
return null;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
logger.debug(`[_startTypingRefresh] Starting typing indicator for message: ${lastMessage.message_id}`);
|
|
733
794
|
|
|
734
795
|
return setInterval(() =>
|
|
735
796
|
this.provider.sendTypingIndicator(lastMessage.message_id).catch(err =>
|
|
@@ -10,13 +10,13 @@ async function uploadMediaToS3(buffer, messageID, titleFile, bucketName, content
|
|
|
10
10
|
const fileName = sanitizedTitle
|
|
11
11
|
? `${messageType}/${messageID}_${sanitizedTitle}.${extension}`
|
|
12
12
|
: `${messageType}/${messageID}.${extension}`;
|
|
13
|
-
logger.info(
|
|
13
|
+
logger.info('[uploadMediaToS3] Media file prepared for upload', { fileName, messageType });
|
|
14
14
|
|
|
15
15
|
try {
|
|
16
16
|
await AWS.uploadBufferToS3(buffer, bucketName, fileName, contentType);
|
|
17
17
|
return fileName;
|
|
18
18
|
} catch (error) {
|
|
19
|
-
logger.error('Failed to upload media to S3:', error.stack);
|
|
19
|
+
logger.error('[uploadMediaToS3] Failed to upload media to S3:', error.stack);
|
|
20
20
|
throw error;
|
|
21
21
|
}
|
|
22
22
|
}
|
|
@@ -2,19 +2,31 @@ const moment = require('moment-timezone');
|
|
|
2
2
|
const { Message } = require('../models/messageModel.js');
|
|
3
3
|
const { logger } = require('../utils/logger');
|
|
4
4
|
|
|
5
|
-
const updateMessageRecord = async (reply, thread) => {
|
|
5
|
+
const updateMessageRecord = async (reply, thread, processedContent = null) => {
|
|
6
6
|
const threadId = thread.getConversationId();
|
|
7
7
|
|
|
8
|
+
const updateData = {
|
|
9
|
+
assistant_id: thread.getAssistantId(),
|
|
10
|
+
thread_id: threadId,
|
|
11
|
+
processed: true
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
if (processedContent && reply.media) {
|
|
15
|
+
updateData.media = {
|
|
16
|
+
...reply.media,
|
|
17
|
+
metadata: {
|
|
18
|
+
...(reply.media.metadata || {}),
|
|
19
|
+
processed_content: processedContent
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
8
24
|
await Message.updateOne(
|
|
9
25
|
{ message_id: reply.message_id, timestamp: reply.timestamp },
|
|
10
|
-
{ $set:
|
|
11
|
-
assistant_id: thread.getAssistantId(),
|
|
12
|
-
thread_id: threadId,
|
|
13
|
-
processed: true
|
|
14
|
-
} }
|
|
26
|
+
{ $set: updateData }
|
|
15
27
|
);
|
|
16
28
|
|
|
17
|
-
logger.info(`[updateMessageRecord] Record updated - ID: ${reply.message_id}, Thread: ${threadId}, Processed: true`);
|
|
29
|
+
logger.info(`[updateMessageRecord] Record updated - ID: ${reply.message_id}, Thread: ${threadId}, Processed: true, MediaContentUpdated: ${!!(processedContent && reply.media)}`);
|
|
18
30
|
};
|
|
19
31
|
|
|
20
32
|
|
|
@@ -81,7 +93,12 @@ function formatMessage(reply) {
|
|
|
81
93
|
.locale('es')
|
|
82
94
|
.format('dddd, D [de] MMMM [de] YYYY [a las] h:mm A');
|
|
83
95
|
|
|
84
|
-
|
|
96
|
+
let messageContent = reply.body;
|
|
97
|
+
if (reply.media?.metadata?.processed_content) {
|
|
98
|
+
messageContent = reply.media.metadata.processed_content;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return `[${mexicoCityTime}] ${messageContent}`;
|
|
85
102
|
} catch (error) {
|
|
86
103
|
logger.error(`[formatMessage] Error for message ID: ${reply.message_id}:`, error?.message || String(error));
|
|
87
104
|
return null;
|
|
@@ -35,23 +35,19 @@ const logger = {
|
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
37
|
* Dedicated message processing utilities
|
|
38
|
-
* Handles text messages, media files, audio transcription, and thread operations
|
|
39
38
|
*/
|
|
40
39
|
const processTextMessage = (reply) => {
|
|
41
|
-
const formattedMessage = formatMessage(reply);
|
|
42
40
|
logger.info('processTextMessage', {
|
|
43
41
|
message_id: reply.message_id,
|
|
44
42
|
timestamp: reply.timestamp,
|
|
45
43
|
from_me: reply.from_me,
|
|
46
44
|
body: reply.body,
|
|
47
|
-
hasContent: !!
|
|
48
|
-
formattedMessage
|
|
45
|
+
hasContent: !!reply.body
|
|
49
46
|
});
|
|
50
|
-
logger.debug('processTextMessage_content', { formattedMessage });
|
|
51
47
|
|
|
52
48
|
const messagesChat = [];
|
|
53
|
-
if (
|
|
54
|
-
messagesChat.push({ type: 'text', text:
|
|
49
|
+
if (reply.body) {
|
|
50
|
+
messagesChat.push({ type: 'text', text: reply.body });
|
|
55
51
|
}
|
|
56
52
|
|
|
57
53
|
return messagesChat;
|
|
@@ -121,8 +117,6 @@ const processImageFileCore = async (fileName, reply) => {
|
|
|
121
117
|
...timings
|
|
122
118
|
});
|
|
123
119
|
|
|
124
|
-
logger.debug('processImageFile_analysis', { imageAnalysis });
|
|
125
|
-
|
|
126
120
|
} catch (error) {
|
|
127
121
|
logger.error('processImageFile', error, {
|
|
128
122
|
message_id: reply.message_id,
|
package/lib/helpers/qrHelper.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
const { Message } = require('../models/messageModel.js');
|
|
2
1
|
const { isRecentMessage } = require('./messageHelper.js');
|
|
3
2
|
const axios = require('axios');
|
|
4
3
|
const { v4: uuidv4 } = require('uuid');
|
|
@@ -57,15 +56,27 @@ async function downloadMediaFromTwilio(mediaUrl, logger) {
|
|
|
57
56
|
|
|
58
57
|
return Buffer.from(response.data);
|
|
59
58
|
} catch (error) {
|
|
59
|
+
const is404 = error.response?.status === 404;
|
|
60
|
+
const isMediaExpired = is404 && mediaUrl.includes('/Media/');
|
|
61
|
+
|
|
60
62
|
logger.error('[TwilioMedia] Download failed', {
|
|
61
63
|
message: error.message,
|
|
62
64
|
status: error.response?.status,
|
|
63
65
|
statusText: error.response?.statusText,
|
|
66
|
+
isMediaExpired,
|
|
64
67
|
responseHeaders: error.response?.headers,
|
|
65
68
|
responseData: error.response?.data ? error.response.data.toString().substring(0, 500) : null,
|
|
66
69
|
url: mediaUrl,
|
|
67
70
|
hasCredentials: !!(process.env.TWILIO_ACCOUNT_SID && process.env.TWILIO_AUTH_TOKEN)
|
|
68
71
|
});
|
|
72
|
+
|
|
73
|
+
if (isMediaExpired) {
|
|
74
|
+
logger.warn('[TwilioMedia] Media expired (24h limit), skipping download', {
|
|
75
|
+
mediaId: mediaUrl.split('/').pop()
|
|
76
|
+
});
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
69
80
|
throw error;
|
|
70
81
|
}
|
|
71
82
|
}
|
|
@@ -89,16 +100,6 @@ function extractTitle(message, mediaType) {
|
|
|
89
100
|
}
|
|
90
101
|
|
|
91
102
|
|
|
92
|
-
async function getLastMessages(chatId, n) {
|
|
93
|
-
const messages = await Message.find({ numero: chatId })
|
|
94
|
-
.sort({ createdAt: -1 })
|
|
95
|
-
.limit(n)
|
|
96
|
-
.select('timestamp numero nombre_whatsapp body');
|
|
97
|
-
|
|
98
|
-
return messages.map(msg => `[${msg.timestamp}] ${msg.body}`);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
|
|
102
103
|
async function downloadMedia(twilioMessage, logger) {
|
|
103
104
|
try {
|
|
104
105
|
const mediaUrl = twilioMessage.MediaUrl0;
|
|
@@ -145,7 +146,6 @@ module.exports = {
|
|
|
145
146
|
getMediaTypeFromContentType,
|
|
146
147
|
extractTitle,
|
|
147
148
|
isRecentMessage,
|
|
148
|
-
getLastMessages,
|
|
149
149
|
downloadMedia,
|
|
150
150
|
ensureWhatsAppFormat
|
|
151
151
|
};
|
|
@@ -82,6 +82,11 @@ async function processTwilioMediaMessage(twilioMessage, bucketName) {
|
|
|
82
82
|
continue;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
if (!mediaBuffer) {
|
|
86
|
+
logger.info('[TwilioMedia] Skipping expired media', { index: i, mediaUrl });
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
85
90
|
const validationResult = validateMedia(mediaBuffer, contentType);
|
|
86
91
|
if (!validationResult.valid) {
|
|
87
92
|
logger.warn('[TwilioMedia] Media validation warning', { index: i, message: validationResult.message });
|
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
const moment = require('moment-timezone');
|
|
2
1
|
const { logger } = require('../utils/logger');
|
|
3
2
|
|
|
4
3
|
|
|
5
|
-
function delay(ms) {
|
|
6
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
|
|
10
4
|
function formatCode(codeBase) {
|
|
11
5
|
logger.info(`formatCode ${codeBase}`);
|
|
12
6
|
|
|
@@ -37,40 +31,6 @@ function formatCode(codeBase) {
|
|
|
37
31
|
}
|
|
38
32
|
|
|
39
33
|
|
|
40
|
-
function calculateDelay(sendTime, timeZone) {
|
|
41
|
-
if (sendTime !== undefined && timeZone !== undefined) {
|
|
42
|
-
const sendMoment = moment.tz(sendTime, timeZone);
|
|
43
|
-
|
|
44
|
-
if (!sendMoment.isValid()) {
|
|
45
|
-
return { error: 'Invalid time format' };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Get the current time and calculate the difference
|
|
49
|
-
const now = moment().tz(timeZone);
|
|
50
|
-
const randomDelay = Math.floor(Math.random() * 15001) + 15000;
|
|
51
|
-
const delay = sendMoment.diff(now) + randomDelay;
|
|
52
|
-
|
|
53
|
-
// Log the calculated details for debugging
|
|
54
|
-
logger.info(
|
|
55
|
-
'Scheduled Time:', sendMoment.format(),
|
|
56
|
-
'Current Time:', now.format(),
|
|
57
|
-
'Delay (minutes):', delay / 60000,
|
|
58
|
-
'Remaining Seconds:', delay % 60000
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
if (delay <= 0) {
|
|
62
|
-
return 2500;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return delay;
|
|
66
|
-
} else {
|
|
67
|
-
return 2500;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
34
|
module.exports = {
|
|
73
|
-
|
|
74
|
-
formatCode,
|
|
75
|
-
calculateDelay
|
|
35
|
+
formatCode
|
|
76
36
|
};
|
package/lib/index.d.ts
CHANGED
|
@@ -283,7 +283,6 @@ declare module '@peopl-health/nexus' {
|
|
|
283
283
|
|
|
284
284
|
// Utility Functions
|
|
285
285
|
export function createLogger(config?: any): any;
|
|
286
|
-
export function delay(ms: number): Promise<void>;
|
|
287
286
|
export function formatCode(codeBase: string): string;
|
|
288
287
|
export function calculateDelay(sendTime: string | Date, timeZone?: string): number;
|
|
289
288
|
export function ensureWhatsAppFormat(phoneNumber: any): string | null;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { MemoryManager } = require('./MemoryManager');
|
|
2
2
|
const { getLastNMessages, formatMessage } = require('../helpers/messageHelper');
|
|
3
3
|
const { handlePendingFunctionCalls: handlePendingFunctionCallsUtil } = require('../providers/OpenAIResponsesProviderTools');
|
|
4
|
-
const { getRecordByFilter } = require('
|
|
4
|
+
const { getRecordByFilter } = require('../services/airtableService');
|
|
5
5
|
const { Follow_Up_ID } = require('../config/airtableConfig');
|
|
6
6
|
const { logger } = require('../utils/logger');
|
|
7
7
|
|
|
8
|
-
class
|
|
8
|
+
class DefaultMemoryManager extends MemoryManager {
|
|
9
9
|
constructor(options = {}) {
|
|
10
10
|
super(options);
|
|
11
11
|
this.maxHistoricalMessages = parseInt(process.env.MAX_HISTORICAL_MESSAGES || '50', 10);
|
|
@@ -18,7 +18,6 @@ class DefaultConversationManager extends ConversationManager {
|
|
|
18
18
|
const allMessages = await getLastNMessages(thread.code, this.maxHistoricalMessages);
|
|
19
19
|
const additionalMessages = config.additionalMessages || [];
|
|
20
20
|
|
|
21
|
-
// New conversation - no history yet
|
|
22
21
|
if (!allMessages?.length) {
|
|
23
22
|
return additionalMessages;
|
|
24
23
|
}
|
|
@@ -33,7 +32,7 @@ class DefaultConversationManager extends ConversationManager {
|
|
|
33
32
|
|
|
34
33
|
return [...additionalMessages, ...messageContext];
|
|
35
34
|
} catch (error) {
|
|
36
|
-
logger.error('[
|
|
35
|
+
logger.error('[DefaultMemoryManager] Context building failed', {
|
|
37
36
|
threadCode: thread.code,
|
|
38
37
|
error: error.message
|
|
39
38
|
});
|
|
@@ -53,7 +52,7 @@ class DefaultConversationManager extends ConversationManager {
|
|
|
53
52
|
responseId: response.id
|
|
54
53
|
});
|
|
55
54
|
} catch (error) {
|
|
56
|
-
logger.error('[
|
|
55
|
+
logger.error('[DefaultMemoryManager] Response processing failed', {
|
|
57
56
|
threadCode: thread.code,
|
|
58
57
|
error: error.message
|
|
59
58
|
});
|
|
@@ -66,7 +65,7 @@ class DefaultConversationManager extends ConversationManager {
|
|
|
66
65
|
try {
|
|
67
66
|
return await handlePendingFunctionCallsUtil(assistant, conversationMessages, toolMetadata);
|
|
68
67
|
} catch (error) {
|
|
69
|
-
logger.error('[
|
|
68
|
+
logger.error('[DefaultMemoryManager] Function call handling failed', { error: error.message });
|
|
70
69
|
return { outputs: [], toolsExecuted: [] };
|
|
71
70
|
}
|
|
72
71
|
}
|
|
@@ -122,7 +121,7 @@ class DefaultConversationManager extends ConversationManager {
|
|
|
122
121
|
|
|
123
122
|
return { ...clinicalContext, symptoms };
|
|
124
123
|
} catch (error) {
|
|
125
|
-
logger.error('[
|
|
124
|
+
logger.error('[DefaultMemoryManager] Error fetching clinical context', { error: error.message });
|
|
126
125
|
return null;
|
|
127
126
|
}
|
|
128
127
|
}
|
|
@@ -201,10 +200,10 @@ class DefaultConversationManager extends ConversationManager {
|
|
|
201
200
|
lastSymptoms: symptomParts.join('; ') || 'Sin síntomas reportados recientemente'
|
|
202
201
|
};
|
|
203
202
|
} catch (error) {
|
|
204
|
-
logger.error('[
|
|
203
|
+
logger.error('[DefaultMemoryManager] Error getting clinical data', { error: error.message });
|
|
205
204
|
return null;
|
|
206
205
|
}
|
|
207
206
|
}
|
|
208
207
|
}
|
|
209
208
|
|
|
210
|
-
module.exports = {
|
|
209
|
+
module.exports = { DefaultMemoryManager };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const { logger } = require('../utils/logger');
|
|
2
2
|
|
|
3
|
-
class
|
|
3
|
+
class MemoryManager {
|
|
4
4
|
constructor(options = {}) {
|
|
5
5
|
this.memorySystem = options.memorySystem || null;
|
|
6
6
|
}
|
|
@@ -36,8 +36,8 @@ class ConversationManager {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
_logActivity(action, metadata = {}) {
|
|
39
|
-
logger.info(`[
|
|
39
|
+
logger.info(`[MemoryManager] ${action}`, metadata);
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
module.exports = {
|
|
43
|
+
module.exports = { MemoryManager };
|
|
@@ -4,8 +4,9 @@ const { retryWithBackoff } = require('../utils/retryHelper');
|
|
|
4
4
|
const {
|
|
5
5
|
handleFunctionCalls: handleFunctionCallsUtil,
|
|
6
6
|
} = require('./OpenAIResponsesProviderTools');
|
|
7
|
-
const {
|
|
7
|
+
const { DefaultMemoryManager } = require('../memory/DefaultMemoryManager');
|
|
8
8
|
const { logger } = require('../utils/logger');
|
|
9
|
+
const { getCurrentMexicoDateTime } = require('../utils/dateUtils');
|
|
9
10
|
|
|
10
11
|
const CONVERSATION_PREFIX = 'conv_';
|
|
11
12
|
const RESPONSE_PREFIX = 'resp_';
|
|
@@ -41,7 +42,7 @@ class OpenAIResponsesProvider {
|
|
|
41
42
|
};
|
|
42
43
|
|
|
43
44
|
this.variant = 'responses';
|
|
44
|
-
this.conversationManager = conversationManager || new
|
|
45
|
+
this.conversationManager = conversationManager || new DefaultMemoryManager();
|
|
45
46
|
|
|
46
47
|
this.responses = this.client.responses;
|
|
47
48
|
this.conversations = this.client.conversations;
|
|
@@ -196,13 +197,20 @@ class OpenAIResponsesProvider {
|
|
|
196
197
|
}
|
|
197
198
|
});
|
|
198
199
|
|
|
200
|
+
logger.info('[OpenAIResponsesProvider] Context built', {
|
|
201
|
+
conversationId,
|
|
202
|
+
assistantId,
|
|
203
|
+
context
|
|
204
|
+
});
|
|
205
|
+
|
|
199
206
|
const filter = thread.code ? { code: thread.code, active: true } : null;
|
|
200
207
|
|
|
201
208
|
// Get clinical context for prompt variables
|
|
202
209
|
const clinicalData = await this.conversationManager.getClinicalData(thread.code);
|
|
203
210
|
const promptVariables = clinicalData ? {
|
|
204
211
|
clinical_context: clinicalData.clinicalContext || '',
|
|
205
|
-
last_symptoms: clinicalData.lastSymptoms || ''
|
|
212
|
+
last_symptoms: clinicalData.lastSymptoms || '',
|
|
213
|
+
current_date: getCurrentMexicoDateTime(),
|
|
206
214
|
} : null;
|
|
207
215
|
|
|
208
216
|
// Execute with built context
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const { OpenAIAssistantsProvider } = require('./OpenAIAssistantsProvider');
|
|
2
2
|
const { OpenAIResponsesProvider } = require('./OpenAIResponsesProvider');
|
|
3
|
-
const {
|
|
3
|
+
const { DefaultMemoryManager } = require('../memory/DefaultMemoryManager');
|
|
4
4
|
const { logger } = require('../utils/logger');
|
|
5
5
|
|
|
6
6
|
const PROVIDER_VARIANTS = {
|
|
@@ -18,7 +18,7 @@ function createProvider(config = {}) {
|
|
|
18
18
|
.toLowerCase();
|
|
19
19
|
|
|
20
20
|
// Create conversation manager if not provided
|
|
21
|
-
const conversationManager = config.conversationManager || new
|
|
21
|
+
const conversationManager = config.conversationManager || new DefaultMemoryManager({
|
|
22
22
|
memorySystem: config.memorySystem
|
|
23
23
|
});
|
|
24
24
|
|
|
@@ -10,7 +10,7 @@ const { Historial_Clinico_ID } = require('../config/airtableConfig');
|
|
|
10
10
|
const { getCurRow, runAssistantWithRetries } = require('../helpers/assistantHelper.js');
|
|
11
11
|
const { getThread } = require('../helpers/threadHelper.js');
|
|
12
12
|
const { processThreadMessage } = require('../helpers/processHelper.js');
|
|
13
|
-
const {
|
|
13
|
+
const { getLastNMessages, updateMessageRecord } = require('../helpers/messageHelper.js');
|
|
14
14
|
const { combineImagesToPDF, cleanupFiles } = require('../helpers/filesHelper.js');
|
|
15
15
|
const { getAssistantById } = require('./assistantResolver');
|
|
16
16
|
const { logger } = require('../utils/logger');
|
|
@@ -156,18 +156,18 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
|
|
|
156
156
|
const finalThread = thread;
|
|
157
157
|
|
|
158
158
|
const messagesStart = Date.now();
|
|
159
|
-
const
|
|
159
|
+
const lastMessage = await getLastNMessages(code, 1);
|
|
160
160
|
timings.get_messages_ms = Date.now() - messagesStart;
|
|
161
161
|
|
|
162
|
-
if (!
|
|
162
|
+
if (!lastMessage || lastMessage.length === 0 || lastMessage[0].from_me) {
|
|
163
163
|
logger.info('[replyAssistantCore] No relevant data found for this assistant.');
|
|
164
164
|
return null;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
|
|
168
|
-
logger.info(`[replyAssistantCore] Processing ${
|
|
168
|
+
logger.info(`[replyAssistantCore] Processing ${lastMessage.length} messages in parallel`);
|
|
169
169
|
const processStart = Date.now();
|
|
170
|
-
const processResult = await processThreadMessage(code,
|
|
170
|
+
const processResult = await processThreadMessage(code, lastMessage, provider);
|
|
171
171
|
|
|
172
172
|
const { results: processResults, timings: processTimings } = processResult;
|
|
173
173
|
timings.process_messages_ms = Date.now() - processStart;
|
|
@@ -186,10 +186,14 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
|
|
|
186
186
|
|
|
187
187
|
const patientMsg = processResults.some(r => r.isPatient);
|
|
188
188
|
const urls = processResults.filter(r => r.url).map(r => ({ url: r.url }));
|
|
189
|
-
const allMessagesToAdd = processResults.flatMap(r => r.messages || []);
|
|
190
189
|
const allTempFiles = processResults.flatMap(r => r.tempFiles || []);
|
|
191
190
|
|
|
192
|
-
await Promise.all(processResults.map(r =>
|
|
191
|
+
await Promise.all(processResults.map(r => {
|
|
192
|
+
const processedContent = r.messages && r.messages.length > 0
|
|
193
|
+
? r.messages.map(msg => msg.content.text).join(' ')
|
|
194
|
+
: null;
|
|
195
|
+
return updateMessageRecord(r.reply, finalThread, processedContent);
|
|
196
|
+
}));
|
|
193
197
|
await cleanupFiles(allTempFiles);
|
|
194
198
|
|
|
195
199
|
if (urls.length > 0) {
|
|
@@ -217,7 +221,7 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
|
|
|
217
221
|
|
|
218
222
|
const assistant = getAssistantById(finalThread.getAssistantId(), finalThread);
|
|
219
223
|
const runStart = Date.now();
|
|
220
|
-
const runResult = await runAssistantWithRetries(finalThread, assistant, runOptions,
|
|
224
|
+
const runResult = await runAssistantWithRetries(finalThread, assistant, runOptions, lastMessage);
|
|
221
225
|
timings.run_assistant_ms = Date.now() - runStart;
|
|
222
226
|
timings.total_ms = Date.now() - startTotal;
|
|
223
227
|
|
|
@@ -225,7 +229,7 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
|
|
|
225
229
|
|
|
226
230
|
logger.info('[Assistant Reply Complete]', {
|
|
227
231
|
code: code ? `${code.substring(0, 3)}***${code.slice(-4)}` : 'unknown',
|
|
228
|
-
messageCount:
|
|
232
|
+
messageCount: lastMessage.length,
|
|
229
233
|
hasMedia: urls.length > 0,
|
|
230
234
|
retries,
|
|
231
235
|
totalMs: timings.total_ms,
|
package/lib/utils/dateUtils.js
CHANGED
|
@@ -18,9 +18,17 @@ const dateAndTimeFromStart = (startTime) => {
|
|
|
18
18
|
};
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
const getCurrentMexicoDateTime = () => {
|
|
22
|
+
return moment()
|
|
23
|
+
.tz('America/Mexico_City')
|
|
24
|
+
.locale('es')
|
|
25
|
+
.format('dddd, D [de] MMMM [de] YYYY [a las] h:mm A');
|
|
26
|
+
};
|
|
27
|
+
|
|
21
28
|
module.exports = {
|
|
22
29
|
ISO_DATE,
|
|
23
30
|
parseStartTime,
|
|
24
31
|
addDays,
|
|
25
|
-
dateAndTimeFromStart
|
|
32
|
+
dateAndTimeFromStart,
|
|
33
|
+
getCurrentMexicoDateTime
|
|
26
34
|
};
|
package/lib/utils/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
const { MessageParser } = require('./messageParser');
|
|
2
2
|
const { logger } = require('./logger');
|
|
3
3
|
const { retryWithBackoff } = require('./retryHelper');
|
|
4
|
+
const { calculateDelay } = require('./scheduleUtils');
|
|
4
5
|
|
|
5
6
|
module.exports = {
|
|
6
7
|
MessageParser,
|
|
7
8
|
logger,
|
|
8
9
|
retryWithBackoff,
|
|
10
|
+
calculateDelay,
|
|
9
11
|
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const moment = require('moment-timezone');
|
|
2
|
+
const { logger } = require('./logger');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Calculate delay in milliseconds until a scheduled time
|
|
6
|
+
*/
|
|
7
|
+
function calculateDelay(sendTime, timeZone) {
|
|
8
|
+
if (!sendTime) {
|
|
9
|
+
return 2500;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
if (timeZone && typeof sendTime === 'string') {
|
|
14
|
+
const sendMoment = moment.tz(sendTime, timeZone);
|
|
15
|
+
|
|
16
|
+
if (!sendMoment.isValid()) {
|
|
17
|
+
logger.warn('[calculateDelay] Invalid time format', { sendTime, timeZone });
|
|
18
|
+
return 2500;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const now = moment().tz(timeZone);
|
|
22
|
+
const randomDelay = Math.floor(Math.random() * 5001) + 10000;
|
|
23
|
+
const delay = sendMoment.diff(now) + randomDelay;
|
|
24
|
+
|
|
25
|
+
logger.debug('[calculateDelay] Timezone calculation', {
|
|
26
|
+
scheduledTime: sendMoment.format(),
|
|
27
|
+
currentTime: now.format(),
|
|
28
|
+
delayMinutes: delay / 60000,
|
|
29
|
+
timeZone
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return Math.max(0, delay);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const now = new Date();
|
|
36
|
+
const targetTime = new Date(sendTime);
|
|
37
|
+
|
|
38
|
+
if (isNaN(targetTime.getTime())) {
|
|
39
|
+
logger.warn('[calculateDelay] Invalid date format', { sendTime });
|
|
40
|
+
return 2500;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return Math.max(0, targetTime.getTime() - now.getTime());
|
|
44
|
+
|
|
45
|
+
} catch (error) {
|
|
46
|
+
logger.error('[calculateDelay] Error calculating delay', {
|
|
47
|
+
error: error.message,
|
|
48
|
+
sendTime,
|
|
49
|
+
timeZone
|
|
50
|
+
});
|
|
51
|
+
return 2500;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = {
|
|
56
|
+
calculateDelay
|
|
57
|
+
};
|