@peopl-health/nexus 2.4.6 → 2.4.8
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/assistants/DoctorScheduleAssistant.js +3 -2
- package/lib/adapters/BaileysProvider.js +5 -4
- package/lib/adapters/TwilioProvider.js +20 -19
- package/lib/assistants/BaseAssistant.js +7 -6
- package/lib/config/awsConfig.js +14 -12
- package/lib/config/llmConfig.js +3 -2
- package/lib/config/mongoAuthConfig.js +5 -5
- package/lib/controllers/assistantController.js +12 -11
- package/lib/controllers/bugReportController.js +6 -5
- package/lib/controllers/conversationController.js +72 -71
- package/lib/controllers/interactionController.js +7 -6
- package/lib/controllers/mediaController.js +15 -13
- package/lib/controllers/messageController.js +7 -6
- package/lib/controllers/patientController.js +2 -1
- package/lib/controllers/qualityMessageController.js +5 -4
- package/lib/controllers/templateController.js +11 -9
- package/lib/controllers/uploadController.js +3 -1
- package/lib/core/NexusMessaging.js +18 -18
- package/lib/helpers/assistantHelper.js +8 -9
- package/lib/helpers/baileysHelper.js +4 -3
- package/lib/helpers/filesHelper.js +9 -8
- package/lib/helpers/llmsHelper.js +24 -9
- package/lib/helpers/mediaHelper.js +3 -2
- package/lib/helpers/messageHelper.js +12 -11
- package/lib/helpers/processHelper.js +24 -10
- package/lib/helpers/qrHelper.js +2 -1
- package/lib/helpers/twilioMediaProcessor.js +45 -35
- package/lib/helpers/whatsappHelper.js +3 -2
- package/lib/index.js +11 -14
- package/lib/interactive/index.js +11 -11
- package/lib/middleware/requestId.js +9 -14
- package/lib/models/messageModel.js +5 -4
- package/lib/providers/OpenAIAssistantsProvider.js +10 -9
- package/lib/providers/OpenAIResponsesProvider.js +18 -17
- package/lib/providers/OpenAIResponsesProviderTools.js +3 -5
- package/lib/providers/createProvider.js +2 -1
- package/lib/services/airtableService.js +39 -6
- package/lib/services/assistantService.js +20 -20
- package/lib/services/conversationService.js +16 -16
- package/lib/services/preprocessingHooks.js +3 -1
- package/lib/storage/MongoStorage.js +14 -14
- package/lib/utils/errorHandler.js +3 -1
- package/lib/utils/logger.js +35 -3
- package/lib/utils/sanitizer.js +0 -6
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ const fs = require('fs');
|
|
|
2
2
|
const { generatePresignedUrl } = require('../config/awsConfig.js');
|
|
3
3
|
const { analyzeImage } = require('./llmsHelper.js');
|
|
4
4
|
const { cleanupFiles, downloadMediaAndCreateFile } = require('./filesHelper.js');
|
|
5
|
-
const { formatMessage
|
|
5
|
+
const { formatMessage } = require('./messageHelper.js');
|
|
6
6
|
const { sanitizeLogMetadata } = require('../utils/sanitizer.js');
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -61,13 +61,18 @@ const processImageFile = async (fileName, reply) => {
|
|
|
61
61
|
let url = null;
|
|
62
62
|
const messagesChat = [];
|
|
63
63
|
|
|
64
|
+
const isSticker = reply.media?.mediaType === 'sticker' ||
|
|
65
|
+
fileName.toLowerCase().includes('sticker/') ||
|
|
66
|
+
fileName.toLowerCase().includes('/sticker/');
|
|
67
|
+
|
|
64
68
|
try {
|
|
65
|
-
imageAnalysis = await analyzeImage(fileName);
|
|
69
|
+
imageAnalysis = await analyzeImage(fileName, isSticker);
|
|
66
70
|
|
|
67
71
|
logger.info('processImageFile', {
|
|
68
72
|
message_id: reply.message_id,
|
|
69
73
|
bucketName: reply.media?.bucketName,
|
|
70
74
|
key: reply.media?.key,
|
|
75
|
+
is_sticker: isSticker,
|
|
71
76
|
medical_relevance: imageAnalysis?.medical_relevance,
|
|
72
77
|
has_table: imageAnalysis?.has_table,
|
|
73
78
|
analysis_type: imageAnalysis?.medical_analysis ? 'medical' : 'general'
|
|
@@ -77,18 +82,18 @@ const processImageFile = async (fileName, reply) => {
|
|
|
77
82
|
|
|
78
83
|
const invalidAnalysis = ['NOT_MEDICAL', 'QUALITY_INSUFFICIENT'];
|
|
79
84
|
|
|
80
|
-
// Generate presigned URL if medically relevant
|
|
81
|
-
if (imageAnalysis?.medical_relevance) {
|
|
85
|
+
// Generate presigned URL only if medically relevant AND not a sticker
|
|
86
|
+
if (imageAnalysis?.medical_relevance && !isSticker) {
|
|
82
87
|
url = await generatePresignedUrl(reply.media.bucketName, reply.media.key);
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
// Add appropriate text based on analysis
|
|
86
|
-
if (imageAnalysis?.has_table && imageAnalysis.table_data) {
|
|
91
|
+
if (!isSticker && imageAnalysis?.has_table && imageAnalysis.table_data) {
|
|
87
92
|
messagesChat.push({
|
|
88
93
|
type: 'text',
|
|
89
94
|
text: imageAnalysis.table_data,
|
|
90
95
|
});
|
|
91
|
-
} else if (imageAnalysis?.medical_analysis && !invalidAnalysis.some(tag => imageAnalysis.medical_analysis.includes(tag))) {
|
|
96
|
+
} else if (!isSticker && imageAnalysis?.medical_analysis && !invalidAnalysis.some(tag => imageAnalysis.medical_analysis.includes(tag))) {
|
|
92
97
|
messagesChat.push({
|
|
93
98
|
type: 'text',
|
|
94
99
|
text: imageAnalysis.medical_analysis,
|
|
@@ -102,7 +107,8 @@ const processImageFile = async (fileName, reply) => {
|
|
|
102
107
|
} catch (error) {
|
|
103
108
|
logger.error('processImageFile', error, {
|
|
104
109
|
message_id: reply.message_id,
|
|
105
|
-
fileName: fileName ? fileName.split('/').pop() : 'unknown'
|
|
110
|
+
fileName: fileName ? fileName.split('/').pop() : 'unknown',
|
|
111
|
+
is_sticker: isSticker
|
|
106
112
|
});
|
|
107
113
|
messagesChat.push({
|
|
108
114
|
type: 'text',
|
|
@@ -175,8 +181,8 @@ const processMediaFiles = async (code, reply, provider) => {
|
|
|
175
181
|
fileName: safeFileName
|
|
176
182
|
});
|
|
177
183
|
|
|
178
|
-
// Skip unsupported
|
|
179
|
-
if (fileName.toLowerCase().includes('.wbmp')
|
|
184
|
+
// Skip only WBMP files (unsupported format)
|
|
185
|
+
if (fileName.toLowerCase().includes('.wbmp')) {
|
|
180
186
|
logger.info('processMediaFiles_skip', {
|
|
181
187
|
message_id: reply.message_id,
|
|
182
188
|
fileName: safeFileName,
|
|
@@ -185,7 +191,15 @@ const processMediaFiles = async (code, reply, provider) => {
|
|
|
185
191
|
continue;
|
|
186
192
|
}
|
|
187
193
|
|
|
188
|
-
|
|
194
|
+
// Process images, documents, applications, and stickers as images
|
|
195
|
+
const isImageLike = fileName.includes('image') ||
|
|
196
|
+
fileName.includes('document') ||
|
|
197
|
+
fileName.includes('application') ||
|
|
198
|
+
reply.media?.mediaType === 'sticker' ||
|
|
199
|
+
fileName.toLowerCase().includes('sticker/') ||
|
|
200
|
+
fileName.toLowerCase().includes('/sticker/');
|
|
201
|
+
|
|
202
|
+
if (isImageLike) {
|
|
189
203
|
const { messagesChat: imageMessages, url: imageUrl } = await processImageFile(fileName, reply);
|
|
190
204
|
messagesChat.push(...imageMessages);
|
|
191
205
|
if (imageUrl) url = imageUrl;
|
package/lib/helpers/qrHelper.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const qrcode = require('qrcode');
|
|
2
|
+
const { logger } = require('../utils/logger');
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
async function generateQRBuffer(text) {
|
|
@@ -6,7 +7,7 @@ async function generateQRBuffer(text) {
|
|
|
6
7
|
const qrBuffer = await qrcode.toBuffer(text, { type: 'image/png' });
|
|
7
8
|
return qrBuffer;
|
|
8
9
|
} catch (err) {
|
|
9
|
-
|
|
10
|
+
logger.error('Error generating QR code', { error: err.message, stack: err.stack });
|
|
10
11
|
throw err;
|
|
11
12
|
}
|
|
12
13
|
}
|
|
@@ -5,37 +5,55 @@ const {
|
|
|
5
5
|
getMediaTypeFromContentType,
|
|
6
6
|
extractTitle
|
|
7
7
|
} = require('./twilioHelper');
|
|
8
|
-
const { validateMedia } = require('../utils/mediaValidator');
|
|
9
8
|
const { generatePresignedUrl } = require('../config/awsConfig');
|
|
10
|
-
const {
|
|
9
|
+
const { addLinkedRecord } = require('../services/airtableService');
|
|
11
10
|
const { Monitoreo_ID } = require('../config/airtableConfig');
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
if (!logger) return console;
|
|
15
|
-
const fallback = {};
|
|
16
|
-
for (const level of ['info', 'warn', 'error', 'debug']) {
|
|
17
|
-
fallback[level] = typeof logger[level] === 'function' ? logger[level].bind(logger) : console[level].bind(console);
|
|
18
|
-
}
|
|
19
|
-
return fallback;
|
|
20
|
-
};
|
|
11
|
+
const { validateMedia } = require('../utils/mediaValidator');
|
|
12
|
+
const { logger } = require('../utils/logger');
|
|
21
13
|
|
|
22
14
|
const normalizeMediaType = (type) => {
|
|
23
15
|
if (!type || typeof type !== 'string') return 'document';
|
|
24
16
|
return type.replace(/Message$/i, '').replace(/WithCaption$/i, '').toLowerCase();
|
|
25
17
|
};
|
|
26
18
|
|
|
27
|
-
async function
|
|
28
|
-
if (!
|
|
19
|
+
async function uploadMediaToAirtable(whatsappId, mediaUrl, baseID, tableName) {
|
|
20
|
+
if (!baseID) {
|
|
21
|
+
logger.debug('[uploadMediaToAirtable] Base ID not configured; skipping Airtable upload');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
await addLinkedRecord(
|
|
27
|
+
baseID,
|
|
28
|
+
tableName,
|
|
29
|
+
{
|
|
30
|
+
estudios: [{ url: mediaUrl }],
|
|
31
|
+
combined_estudios: [{ url: mediaUrl }]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
referenceTable: 'estado_general',
|
|
35
|
+
referenceFilter: `whatsapp_id = "${whatsappId}"`,
|
|
36
|
+
linkFieldName: 'patient_id'
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
logger.debug('[uploadMediaToAirtable] Successfully uploaded media to estudios table', { whatsappId });
|
|
41
|
+
} catch (error) {
|
|
42
|
+
logger.warn('[uploadMediaToAirtable] Failed to upload media to estudios table', { whatsappId, error: error?.message || error });
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
29
46
|
|
|
30
|
-
|
|
47
|
+
async function processTwilioMediaMessage(twilioMessage, bucketName) {
|
|
48
|
+
if (!twilioMessage) return [];
|
|
31
49
|
const numMedia = parseInt(twilioMessage.NumMedia || '0', 10);
|
|
32
50
|
if (!numMedia || numMedia <= 0) {
|
|
33
|
-
|
|
51
|
+
logger.debug('[TwilioMedia] No media detected in message');
|
|
34
52
|
return [];
|
|
35
53
|
}
|
|
36
54
|
|
|
37
55
|
if (!bucketName) {
|
|
38
|
-
|
|
56
|
+
logger.warn('[TwilioMedia] AWS bucket not configured; skipping media processing');
|
|
39
57
|
return [];
|
|
40
58
|
}
|
|
41
59
|
|
|
@@ -50,25 +68,25 @@ async function processTwilioMediaMessage(twilioMessage, logger, bucketName) {
|
|
|
50
68
|
const contentType = twilioMessage[`MediaContentType${i}`];
|
|
51
69
|
|
|
52
70
|
if (!mediaUrl || !contentType) {
|
|
53
|
-
|
|
71
|
+
logger.warn('[TwilioMedia] Missing media URL or content type', { index: i });
|
|
54
72
|
continue;
|
|
55
73
|
}
|
|
56
74
|
|
|
57
|
-
|
|
75
|
+
logger.info('[TwilioMedia] Processing media item', { index: i, contentType, mediaUrl });
|
|
58
76
|
|
|
59
77
|
let mediaBuffer;
|
|
60
78
|
try {
|
|
61
|
-
mediaBuffer = await downloadMediaFromTwilio(mediaUrl,
|
|
79
|
+
mediaBuffer = await downloadMediaFromTwilio(mediaUrl, logger);
|
|
62
80
|
} catch (error) {
|
|
63
|
-
|
|
81
|
+
logger.error('[TwilioMedia] Failed to download media', { index: i, error: error?.message || error });
|
|
64
82
|
continue;
|
|
65
83
|
}
|
|
66
84
|
|
|
67
85
|
const validationResult = validateMedia(mediaBuffer, contentType);
|
|
68
86
|
if (!validationResult.valid) {
|
|
69
|
-
|
|
87
|
+
logger.warn('[TwilioMedia] Media validation warning', { index: i, message: validationResult.message });
|
|
70
88
|
} else {
|
|
71
|
-
|
|
89
|
+
logger.debug('[TwilioMedia] Media validation passed', { index: i, message: validationResult.message });
|
|
72
90
|
}
|
|
73
91
|
|
|
74
92
|
const helperMediaType = getMediaTypeFromContentType(contentType);
|
|
@@ -91,7 +109,7 @@ async function processTwilioMediaMessage(twilioMessage, logger, bucketName) {
|
|
|
91
109
|
derivedMediaType
|
|
92
110
|
);
|
|
93
111
|
} catch (error) {
|
|
94
|
-
|
|
112
|
+
logger.error('[TwilioMedia] Failed to upload media to S3', { index: i, error: error?.message || error });
|
|
95
113
|
continue;
|
|
96
114
|
}
|
|
97
115
|
|
|
@@ -117,25 +135,17 @@ async function processTwilioMediaMessage(twilioMessage, logger, bucketName) {
|
|
|
117
135
|
const url = await generatePresignedUrl(bucketName, key);
|
|
118
136
|
item.metadata.presignedUrl = url;
|
|
119
137
|
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
const patientId = Array.isArray(patient) && patient.length > 0 ? [patient[0].recordID || patient[0].recordId || patient[0].record_id] : [];
|
|
123
|
-
await addRecord(Monitoreo_ID, 'estudios', [{
|
|
124
|
-
fields: {
|
|
125
|
-
estudios: [{ url }],
|
|
126
|
-
combined_estudios: [{ url }],
|
|
127
|
-
patient_id: patientId
|
|
128
|
-
}
|
|
129
|
-
}]);
|
|
138
|
+
if (derivedMediaType !== 'sticker') {
|
|
139
|
+
await uploadMediaToAirtable(code, url, Monitoreo_ID, 'estudios');
|
|
130
140
|
}
|
|
131
141
|
} catch (error) {
|
|
132
|
-
|
|
142
|
+
logger.warn('[TwilioMedia] Failed to update Airtable with media reference', { index: i, error: error?.message || error });
|
|
133
143
|
}
|
|
134
144
|
|
|
135
145
|
mediaItems.push(item);
|
|
136
146
|
}
|
|
137
147
|
|
|
138
|
-
|
|
148
|
+
logger.info('[TwilioMedia] Completed processing', {
|
|
139
149
|
from: code,
|
|
140
150
|
itemsStored: mediaItems.length,
|
|
141
151
|
bucket: bucketName
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const moment = require('moment-timezone');
|
|
2
|
+
const { logger } = require('../utils/logger');
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
function delay(ms) {
|
|
@@ -7,7 +8,7 @@ function delay(ms) {
|
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
function formatCode(codeBase) {
|
|
10
|
-
|
|
11
|
+
logger.info(`formatCode ${codeBase}`);
|
|
11
12
|
|
|
12
13
|
const [number, domain] = codeBase.split('@');
|
|
13
14
|
|
|
@@ -50,7 +51,7 @@ function calculateDelay(sendTime, timeZone) {
|
|
|
50
51
|
const delay = sendMoment.diff(now) + randomDelay;
|
|
51
52
|
|
|
52
53
|
// Log the calculated details for debugging
|
|
53
|
-
|
|
54
|
+
logger.info(
|
|
54
55
|
'Scheduled Time:', sendMoment.format(),
|
|
55
56
|
'Current Time:', now.format(),
|
|
56
57
|
'Delay (minutes):', delay / 60000,
|
package/lib/index.js
CHANGED
|
@@ -25,9 +25,9 @@ const {
|
|
|
25
25
|
} = require('./services/preprocessingHooks');
|
|
26
26
|
const {
|
|
27
27
|
requestIdMiddleware,
|
|
28
|
-
getRequestId
|
|
29
|
-
logger
|
|
28
|
+
getRequestId
|
|
30
29
|
} = require('./middleware/requestId');
|
|
30
|
+
const { logger } = require('./utils/logger');
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Main Nexus class that orchestrates all components
|
|
@@ -39,7 +39,7 @@ class Nexus {
|
|
|
39
39
|
try {
|
|
40
40
|
setDefaultInstance(this.messaging);
|
|
41
41
|
} catch (err) {
|
|
42
|
-
|
|
42
|
+
logger.warn('[Nexus] Failed to set default messaging instance:', err?.message || err);
|
|
43
43
|
}
|
|
44
44
|
this.storage = null;
|
|
45
45
|
this.messageParser = null;
|
|
@@ -92,11 +92,10 @@ class Nexus {
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
} catch (e) {
|
|
95
|
-
|
|
95
|
+
logger.warn('Warning: failed to auto-configure template providers:', e?.message || e);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
// Convenience: handle mongoUri early (before storage connect)
|
|
98
|
+
// Handle mongoUri early (before storage connect)
|
|
100
99
|
try {
|
|
101
100
|
if (options.mongoUri) {
|
|
102
101
|
if (storage === 'mongo') {
|
|
@@ -104,12 +103,11 @@ class Nexus {
|
|
|
104
103
|
storageConfig.mongoUri = options.mongoUri;
|
|
105
104
|
}
|
|
106
105
|
} else {
|
|
107
|
-
// If not using MongoStorage but a URI is provided, initialize default mongoose connection
|
|
108
106
|
await this.messaging.initializeMongoDB(options.mongoUri);
|
|
109
107
|
}
|
|
110
108
|
}
|
|
111
109
|
} catch (dbErr) {
|
|
112
|
-
|
|
110
|
+
logger.warn('[Nexus] mongo convenience warning:', dbErr?.message || dbErr);
|
|
113
111
|
}
|
|
114
112
|
|
|
115
113
|
// Initialize storage if provided
|
|
@@ -119,9 +117,8 @@ class Nexus {
|
|
|
119
117
|
if (typeof storage === 'string') {
|
|
120
118
|
this.storage = createStorage(storage, storageConfig || {});
|
|
121
119
|
} else if (typeof storage === 'object' && (storage.saveMessage || storage.saveInteractive)) {
|
|
122
|
-
this.storage = storage;
|
|
120
|
+
this.storage = storage;
|
|
123
121
|
} else {
|
|
124
|
-
// default to mongo if truthy but not recognized
|
|
125
122
|
this.storage = createStorage('mongo', storageConfig || {});
|
|
126
123
|
}
|
|
127
124
|
if (this.storage && typeof this.storage.connect === 'function') {
|
|
@@ -129,7 +126,7 @@ class Nexus {
|
|
|
129
126
|
}
|
|
130
127
|
this.messaging.setMessageStorage(this.storage);
|
|
131
128
|
} catch (e) {
|
|
132
|
-
|
|
129
|
+
logger.warn('Warning: storage initialization failed:', e?.message || e);
|
|
133
130
|
}
|
|
134
131
|
}
|
|
135
132
|
|
|
@@ -150,7 +147,7 @@ class Nexus {
|
|
|
150
147
|
llmConfigModule.setOpenAIClient(providerInstance.getClient());
|
|
151
148
|
}
|
|
152
149
|
} catch (err) {
|
|
153
|
-
|
|
150
|
+
logger.warn('[Nexus] Failed to expose OpenAI provider:', err?.message || err);
|
|
154
151
|
}
|
|
155
152
|
}
|
|
156
153
|
|
|
@@ -171,7 +168,7 @@ class Nexus {
|
|
|
171
168
|
runtimeConfig.set('AIRTABLE_API_KEY', options.airtable.apiKey);
|
|
172
169
|
}
|
|
173
170
|
} catch (cfgErr) {
|
|
174
|
-
|
|
171
|
+
logger.warn('[Nexus] convenience config warning:', cfgErr?.message || cfgErr);
|
|
175
172
|
}
|
|
176
173
|
|
|
177
174
|
|
|
@@ -195,7 +192,7 @@ class Nexus {
|
|
|
195
192
|
}
|
|
196
193
|
}
|
|
197
194
|
} catch (e) {
|
|
198
|
-
|
|
195
|
+
logger.warn('Warning: failed to configure assistants:', e?.message || e);
|
|
199
196
|
}
|
|
200
197
|
|
|
201
198
|
this.isInitialized = true;
|
package/lib/interactive/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
const { toTwilioContent } = require('./twilioMapper');
|
|
3
3
|
const { registerFlow, getFlow, listFlows, registerInteractiveHandler, listInteractiveHandlers } = require('./registry');
|
|
4
|
+
const { logger } = require('../utils/logger');
|
|
4
5
|
|
|
5
6
|
async function sendInteractive(nexusOrMessaging, params) {
|
|
6
7
|
const { code, spec, id, variables } = params || {};
|
|
@@ -28,16 +29,6 @@ async function sendInteractive(nexusOrMessaging, params) {
|
|
|
28
29
|
throw new Error('Interactive/flows not supported for this provider');
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
module.exports = {
|
|
32
|
-
registerFlow,
|
|
33
|
-
getFlow,
|
|
34
|
-
listFlows,
|
|
35
|
-
sendInteractive,
|
|
36
|
-
registerInteractiveHandler,
|
|
37
|
-
attachInteractiveRouter
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
|
|
41
32
|
function _matchInteractive(match, interactive) {
|
|
42
33
|
if (!match) return true;
|
|
43
34
|
if (!interactive) return false;
|
|
@@ -76,7 +67,7 @@ function attachInteractiveRouter(nexusOrMessaging) {
|
|
|
76
67
|
await handler(messageData, messaging);
|
|
77
68
|
}
|
|
78
69
|
} catch (e) {
|
|
79
|
-
|
|
70
|
+
logger.warn('Interactive handler error:', e && e.message || e);
|
|
80
71
|
}
|
|
81
72
|
}
|
|
82
73
|
};
|
|
@@ -84,3 +75,12 @@ function attachInteractiveRouter(nexusOrMessaging) {
|
|
|
84
75
|
bus.on('interactive:received', handler);
|
|
85
76
|
return () => bus.off && bus.off('interactive:received', handler);
|
|
86
77
|
}
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
registerFlow,
|
|
81
|
+
getFlow,
|
|
82
|
+
listFlows,
|
|
83
|
+
sendInteractive,
|
|
84
|
+
registerInteractiveHandler,
|
|
85
|
+
attachInteractiveRouter
|
|
86
|
+
};
|
|
@@ -1,41 +1,36 @@
|
|
|
1
1
|
const { AsyncLocalStorage } = require('async_hooks');
|
|
2
2
|
const { v4: uuidv4 } = require('uuid');
|
|
3
|
+
const { setRequestIdGetter } = require('../utils/logger');
|
|
3
4
|
|
|
4
5
|
const requestContext = new AsyncLocalStorage();
|
|
5
6
|
|
|
6
7
|
function getRequestId() {
|
|
7
8
|
const store = requestContext.getStore();
|
|
8
|
-
return store?.requestId ||
|
|
9
|
+
return store?.requestId || null;
|
|
9
10
|
}
|
|
10
11
|
|
|
12
|
+
setRequestIdGetter(getRequestId);
|
|
13
|
+
|
|
11
14
|
function requestIdMiddleware(req, res, next) {
|
|
12
15
|
const requestId = uuidv4().substring(0, 8);
|
|
13
16
|
req.requestId = requestId;
|
|
14
17
|
res.setHeader('X-Request-Id', requestId);
|
|
15
18
|
|
|
16
19
|
requestContext.run({ requestId }, () => {
|
|
17
|
-
console.log(`[${requestId}] → ${req.method} ${req.path}`);
|
|
18
|
-
|
|
19
20
|
const startTime = Date.now();
|
|
21
|
+
|
|
20
22
|
res.on('finish', () => {
|
|
21
23
|
const duration = Date.now() - startTime;
|
|
22
|
-
|
|
24
|
+
if (process.env.LOG_HTTP_REQUESTS === 'true') {
|
|
25
|
+
console.log(`[${requestId}] ${req.method} ${req.path} - ${res.statusCode} (${duration}ms)`);
|
|
26
|
+
}
|
|
23
27
|
});
|
|
24
28
|
|
|
25
29
|
next();
|
|
26
30
|
});
|
|
27
31
|
}
|
|
28
32
|
|
|
29
|
-
const logger = {
|
|
30
|
-
log: (...args) => console.log(`[${getRequestId()}]`, ...args),
|
|
31
|
-
error: (...args) => console.error(`[${getRequestId()}]`, ...args),
|
|
32
|
-
warn: (...args) => console.warn(`[${getRequestId()}]`, ...args),
|
|
33
|
-
info: (...args) => console.info(`[${getRequestId()}]`, ...args),
|
|
34
|
-
debug: (...args) => console.debug(`[${getRequestId()}]`, ...args)
|
|
35
|
-
};
|
|
36
|
-
|
|
37
33
|
module.exports = {
|
|
38
34
|
requestIdMiddleware,
|
|
39
|
-
getRequestId
|
|
40
|
-
logger
|
|
35
|
+
getRequestId
|
|
41
36
|
};
|
|
@@ -2,6 +2,7 @@ const mongoose = require('mongoose');
|
|
|
2
2
|
const moment = require('moment-timezone');
|
|
3
3
|
const { getRecordByFilter } = require('../services/airtableService');
|
|
4
4
|
const { Monitoreo_ID } = require('../config/airtableConfig');
|
|
5
|
+
const { logger } = require('../utils/logger');
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
const messageSchema = new mongoose.Schema({
|
|
@@ -79,7 +80,7 @@ async function getClinicalContext(whatsappId) {
|
|
|
79
80
|
}
|
|
80
81
|
return null;
|
|
81
82
|
} catch (error) {
|
|
82
|
-
|
|
83
|
+
logger.error('Error fetching clinical context from Airtable:', error);
|
|
83
84
|
return null;
|
|
84
85
|
}
|
|
85
86
|
}
|
|
@@ -116,9 +117,9 @@ async function insertMessage(values) {
|
|
|
116
117
|
{ upsert: true, new: true }
|
|
117
118
|
);
|
|
118
119
|
|
|
119
|
-
|
|
120
|
+
logger.info('[MongoStorage] Message inserted or updated successfully');
|
|
120
121
|
} catch (err) {
|
|
121
|
-
|
|
122
|
+
logger.error('[MongoStorage] Error inserting message:', err);
|
|
122
123
|
throw err;
|
|
123
124
|
}
|
|
124
125
|
}
|
|
@@ -173,7 +174,7 @@ async function getContactDisplayName(contactNumber) {
|
|
|
173
174
|
return contactNumber;
|
|
174
175
|
}
|
|
175
176
|
} catch (error) {
|
|
176
|
-
|
|
177
|
+
logger.error('[MongoStorage] Error fetching display name for ${contactNumber}:', error);
|
|
177
178
|
return contactNumber;
|
|
178
179
|
}
|
|
179
180
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { OpenAI } = require('openai');
|
|
2
2
|
const { retryWithBackoff } = require('../utils/retryHelper');
|
|
3
|
+
const { logger } = require('../utils/logger');
|
|
3
4
|
|
|
4
5
|
const DEFAULT_MAX_RETRIES = parseInt(process.env.MAX_RETRIES || '10', 10);
|
|
5
6
|
const PROVIDER_NAME = 'OpenAIAssistantsProvider';
|
|
@@ -274,7 +275,7 @@ class OpenAIAssistantsProvider {
|
|
|
274
275
|
const result = await assistant.executeTool(name, args);
|
|
275
276
|
outputs.push({ tool_call_id: call.id, output: typeof result === 'string' ? result : JSON.stringify(result) });
|
|
276
277
|
} catch (error) {
|
|
277
|
-
|
|
278
|
+
logger.error('[OpenAIAssistantsProvider] Tool execution failed', error);
|
|
278
279
|
outputs.push({
|
|
279
280
|
tool_call_id: call.id,
|
|
280
281
|
output: JSON.stringify({ success: false, error: error?.message || 'Tool execution failed' })
|
|
@@ -294,19 +295,19 @@ class OpenAIAssistantsProvider {
|
|
|
294
295
|
async checkRunStatus(assistant, thread_id, run_id, retryCount = 0, maxRetries = DEFAULT_MAX_RETRIES, actionHandled = false) {
|
|
295
296
|
try {
|
|
296
297
|
const run = await this.getRun({ threadId: thread_id, runId: run_id });
|
|
297
|
-
|
|
298
|
+
logger.info(`Status: ${run.status} ${thread_id} ${run_id} (attempt ${retryCount + 1})`);
|
|
298
299
|
|
|
299
300
|
const failedStatuses = ['failed', 'expired', 'incomplete', 'errored'];
|
|
300
301
|
if (failedStatuses.includes(run.status)) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
302
|
+
logger.info(`Run failed. ${run.status} `);
|
|
303
|
+
logger.info('Error:');
|
|
304
|
+
logger.info(run);
|
|
304
305
|
return {run, completed: false};
|
|
305
306
|
} else if (run.status === 'cancelled') {
|
|
306
|
-
|
|
307
|
+
logger.info('cancelled');
|
|
307
308
|
return {run, completed: true};
|
|
308
309
|
} else if (run.status === 'requires_action') {
|
|
309
|
-
|
|
310
|
+
logger.info('requires_action');
|
|
310
311
|
if (retryCount >= maxRetries) {
|
|
311
312
|
return {run, completed: false};
|
|
312
313
|
}
|
|
@@ -325,11 +326,11 @@ class OpenAIAssistantsProvider {
|
|
|
325
326
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
326
327
|
return this.checkRunStatus(assistant, thread_id, run_id, retryCount + 1, maxRetries, actionHandled);
|
|
327
328
|
} else {
|
|
328
|
-
|
|
329
|
+
logger.info('Run completed.');
|
|
329
330
|
return {run, completed: true};
|
|
330
331
|
}
|
|
331
332
|
} catch (error) {
|
|
332
|
-
|
|
333
|
+
logger.error('Error checking run status:', error);
|
|
333
334
|
return {run: null, completed: false};
|
|
334
335
|
}
|
|
335
336
|
}
|