@peopl-health/nexus 2.4.4 → 2.4.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.
@@ -5,6 +5,7 @@ const { createProvider } = require('../providers/createProvider.js');
5
5
  const { withTracing } = require('../utils/tracingDecorator');
6
6
 
7
7
  const { getRecordByFilter } = require('../services/airtableService.js');
8
+ const { logger } = require('../middleware/requestId');
8
9
 
9
10
  const DEFAULT_MAX_RETRIES = parseInt(process.env.MAX_RETRIES || '30', 10);
10
11
 
@@ -77,7 +78,7 @@ const runAssistantAndWait = async ({
77
78
  let completed = false;
78
79
 
79
80
  try {
80
- console.log('[runAssistantAndWait] RUN ID', run.id, 'THREAD ID', thread.getConversationId(), 'ASSISTANT ID', thread.getAssistantId());
81
+ logger.log('[runAssistantAndWait] RUN ID', run.id, 'THREAD ID', thread.getConversationId(), 'ASSISTANT ID', thread.getAssistantId());
81
82
  ({run, completed} = await provider.checkRunStatus(assistant, thread.getConversationId(), run.id, 0, maxRetries));
82
83
  } finally {
83
84
  if (filter) {
@@ -101,7 +102,7 @@ const executeAssistantAttempt = async (thread, assistant, runConfig, attemptNumb
101
102
  runConfig
102
103
  });
103
104
 
104
- console.log(`[executeAssistantAttempt] Attempt ${attemptNumber}: completed=${result.completed}, output=${result.output || '(empty)'}`);
105
+ logger.log(`[executeAssistantAttempt] Attempt ${attemptNumber}: completed=${result.completed}, output=${result.output || '(empty)'}`);
105
106
 
106
107
  return result;
107
108
  };
@@ -129,15 +130,22 @@ const runAssistantWithRetries = async (thread, assistant, runConfig, patientRepl
129
130
  )(thread, assistant, runConfig, retries));
130
131
 
131
132
  if (completed && output) break;
132
- if (retries < maxRetries) await new Promise(resolve => setTimeout(resolve, 2000));
133
+
134
+ if (retries < maxRetries) {
135
+ const delay = retries === 1
136
+ ? 500
137
+ : Math.min(1000 * Math.pow(1.5, retries - 1), 5000);
138
+ logger.log(`[runAssistantWithRetries] Retry ${retries}, waiting ${delay}ms`);
139
+ await new Promise(resolve => setTimeout(resolve, delay));
140
+ }
133
141
  } while (retries < maxRetries && (!completed || !output));
134
142
 
135
143
  const predictionTimeMs = Date.now() - startTime;
136
144
 
137
- if (run?.last_error) console.log('[runAssistantWithRetries] RUN LAST ERROR:', run.last_error);
138
- console.log('[runAssistantWithRetries] RUN STATUS', completed);
139
- console.log('[runAssistantWithRetries] OUTPUT', output);
140
- console.log('[runAssistantWithRetries] TIMING', { predictionTimeMs, retries });
145
+ if (run?.last_error) logger.log('[runAssistantWithRetries] RUN LAST ERROR:', run.last_error);
146
+ logger.log('[runAssistantWithRetries] RUN STATUS', completed);
147
+ logger.log('[runAssistantWithRetries] OUTPUT', output);
148
+ logger.log('[runAssistantWithRetries] TIMING', { predictionTimeMs, retries });
141
149
 
142
150
  return { run, output, completed, retries, predictionTimeMs };
143
151
  };
package/lib/index.js CHANGED
@@ -23,6 +23,11 @@ const {
23
23
  hasPreprocessingHandler,
24
24
  invokePreprocessingHandler
25
25
  } = require('./services/preprocessingHooks');
26
+ const {
27
+ requestIdMiddleware,
28
+ getRequestId,
29
+ logger
30
+ } = require('./middleware/requestId');
26
31
 
27
32
  /**
28
33
  * Main Nexus class that orchestrates all components
@@ -341,5 +346,9 @@ module.exports = {
341
346
  listFlows: interactive.listFlows,
342
347
  sendInteractive: interactive.sendInteractive,
343
348
  registerInteractiveHandler: interactive.registerInteractiveHandler,
344
- attachInteractiveRouter: interactive.attachInteractiveRouter
349
+ attachInteractiveRouter: interactive.attachInteractiveRouter,
350
+ // Request tracing
351
+ requestIdMiddleware,
352
+ getRequestId,
353
+ logger
345
354
  };
@@ -0,0 +1,41 @@
1
+ const { AsyncLocalStorage } = require('async_hooks');
2
+ const { v4: uuidv4 } = require('uuid');
3
+
4
+ const requestContext = new AsyncLocalStorage();
5
+
6
+ function getRequestId() {
7
+ const store = requestContext.getStore();
8
+ return store?.requestId || 'no-context';
9
+ }
10
+
11
+ function requestIdMiddleware(req, res, next) {
12
+ const requestId = uuidv4().substring(0, 8);
13
+ req.requestId = requestId;
14
+ res.setHeader('X-Request-Id', requestId);
15
+
16
+ requestContext.run({ requestId }, () => {
17
+ console.log(`[${requestId}] → ${req.method} ${req.path}`);
18
+
19
+ const startTime = Date.now();
20
+ res.on('finish', () => {
21
+ const duration = Date.now() - startTime;
22
+ console.log(`[${requestId}] ← ${res.statusCode} ${req.method} ${req.path} (${duration}ms)`);
23
+ });
24
+
25
+ next();
26
+ });
27
+ }
28
+
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
+ module.exports = {
38
+ requestIdMiddleware,
39
+ getRequestId,
40
+ logger
41
+ };
@@ -59,6 +59,9 @@ const messageSchema = new mongoose.Schema({
59
59
  messageSchema.index({ message_id: 1, timestamp: 1 }, { unique: true });
60
60
  messageSchema.index({ numero: 1, createdAt: -1 });
61
61
 
62
+ messageSchema.index({ numero: 1, processed: 1, origin: 1 }, { name: 'numero_processed_origin_idx' });
63
+ messageSchema.index({ numero: 1, createdAt: -1, processed: 1 }, { name: 'numero_created_processed_idx' });
64
+
62
65
  messageSchema.pre('save', function (next) {
63
66
  if (this.timestamp) {
64
67
  this.timestamp = moment.tz(this.timestamp, 'America/Mexico_City').toDate();
@@ -8,7 +8,8 @@ const predictionMetricsSchema = new mongoose.Schema({
8
8
  prediction_time_ms: { type: Number, required: true },
9
9
  retry_count: { type: Number, required: true, default: 1 },
10
10
  completed: { type: Boolean, default: true },
11
- error: { type: String, default: null }
11
+ error: { type: String, default: null },
12
+ timing_breakdown: { type: Object, default: {} }
12
13
  }, { timestamps: true });
13
14
 
14
15
  predictionMetricsSchema.index({ createdAt: -1 });
@@ -15,6 +15,7 @@ const { withTracing } = require('../utils/tracingDecorator.js');
15
15
  const { processIndividualMessage } = require('../helpers/processHelper.js');
16
16
  const { getLastMessages } = require('../helpers/messageHelper.js');
17
17
  const { combineImagesToPDF, cleanupFiles } = require('../helpers/filesHelper.js');
18
+ const { logger } = require('../middleware/requestId');
18
19
 
19
20
  const DEFAULT_MAX_RETRIES = parseInt(process.env.MAX_RETRIES || '30', 10);
20
21
 
@@ -269,6 +270,10 @@ const addInsAssistant = withTracing(
269
270
  );
270
271
 
271
272
  const replyAssistantCore = async (code, message_ = null, thread_ = null, runOptions = {}) => {
273
+ const timings = {};
274
+ const startTotal = Date.now();
275
+
276
+ timings.getThread = Date.now();
272
277
  const thread = thread_ || await withTracing(getThread, 'get_thread_operation',
273
278
  (threadCode) => ({
274
279
  'thread.code': threadCode,
@@ -276,30 +281,43 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
276
281
  'thread.provided': !!thread_
277
282
  })
278
283
  )(code);
284
+ timings.getThread = Date.now() - timings.getThread;
285
+
279
286
  if (!thread) return null;
280
287
 
288
+ timings.getMessages = Date.now();
281
289
  const patientReply = await getLastMessages(code);
290
+ timings.getMessages = Date.now() - timings.getMessages;
291
+
282
292
  if (!patientReply) {
283
- console.log('[replyAssistantCore] No relevant data found for this assistant.');
293
+ logger.log('[replyAssistantCore] No relevant data found for this assistant.');
284
294
  return null;
285
295
  }
286
296
 
287
297
  const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
288
298
 
289
- let patientMsg = false;
290
- let urls = [];
291
- for (let i = 0; i < patientReply.length; i++) {
292
- const reply = patientReply[i];
293
- const { isPatient, url } = await processIndividualMessage(code, reply, provider, thread);
294
- console.log(`[replyAssistantCore] Processing message ${i + 1}/${patientReply.length}: isPatient=${isPatient}, hasUrl=${!!url}`);
295
- patientMsg = patientMsg || isPatient;
296
- if (url) urls.push({ 'url': url });
297
- }
299
+ timings.processMessages = Date.now();
300
+ logger.log(`[replyAssistantCore] Processing ${patientReply.length} messages in parallel`);
301
+ const results = await Promise.all(
302
+ patientReply.map((reply, i) =>
303
+ processIndividualMessage(code, reply, provider, thread)
304
+ .then(result => {
305
+ logger.log(`[replyAssistantCore] Message ${i + 1}/${patientReply.length}: isPatient=${result.isPatient}, hasUrl=${!!result.url}`);
306
+ return result;
307
+ })
308
+ )
309
+ );
310
+ timings.processMessages = Date.now() - timings.processMessages;
311
+
312
+ const patientMsg = results.some(r => r.isPatient);
313
+ const urls = results.filter(r => r.url).map(r => ({ url: r.url }));
298
314
 
299
315
  if (urls.length > 0) {
300
- console.log(`[replyAssistantCore] Processing ${urls.length} URLs for PDF combination`);
316
+ timings.pdfCombination = Date.now();
317
+ logger.log(`[replyAssistantCore] Processing ${urls.length} URLs for PDF combination`);
301
318
  const { pdfBuffer, processedFiles } = await combineImagesToPDF({ code });
302
- console.log(`[replyAssistantCore] PDF combination complete: ${processedFiles?.length || 0} files processed`);
319
+ timings.pdfCombination = Date.now() - timings.pdfCombination;
320
+ logger.log(`[replyAssistantCore] PDF combination complete: ${processedFiles?.length || 0} files processed`);
303
321
 
304
322
  if (pdfBuffer) {
305
323
  const key = `${code}-${Date.now()}-combined.pdf`;
@@ -316,6 +334,7 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
316
334
 
317
335
  if (!patientMsg || thread.stopped) return null;
318
336
 
337
+ timings.runAssistant = Date.now();
319
338
  const assistant = getAssistantById(thread.getAssistantId(), thread);
320
339
  const { run, output, completed, retries, predictionTimeMs } = await withTracing(
321
340
  runAssistantWithRetries,
@@ -326,8 +345,17 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
326
345
  'assistant.has_patient_reply': !!patientReply
327
346
  })
328
347
  )(thread, assistant, runOptions, patientReply);
348
+ timings.runAssistant = Date.now() - timings.runAssistant;
349
+ timings.total = Date.now() - startTotal;
350
+
351
+ logger.log('[Performance Breakdown]', {
352
+ code: code ? `${code.substring(0, 3)}***${code.slice(-4)}` : 'unknown',
353
+ messageCount: patientReply.length,
354
+ hasMedia: urls.length > 0,
355
+ retries,
356
+ time: `${timings.total}ms`
357
+ });
329
358
 
330
- // Store prediction metrics
331
359
  if (output && predictionTimeMs) {
332
360
  await PredictionMetrics.create({
333
361
  message_id: `${code}-${Date.now()}`,
@@ -336,7 +364,15 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
336
364
  thread_id: thread.getConversationId(),
337
365
  prediction_time_ms: predictionTimeMs,
338
366
  retry_count: retries,
339
- completed: completed
367
+ completed: completed,
368
+ timing_breakdown: {
369
+ get_thread_ms: timings.getThread,
370
+ get_messages_ms: timings.getMessages,
371
+ process_messages_ms: timings.processMessages,
372
+ pdf_combination_ms: timings.pdfCombination || 0,
373
+ run_assistant_ms: timings.runAssistant,
374
+ total_ms: timings.total
375
+ }
340
376
  }).catch(err => console.error('[replyAssistantCore] Failed to store metrics:', err));
341
377
  }
342
378
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "2.4.4",
3
+ "version": "2.4.5",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",