@peopl-health/nexus 2.4.3 → 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
  };
@@ -111,6 +112,7 @@ const runAssistantWithRetries = async (thread, assistant, runConfig, patientRepl
111
112
  assistant.setReplies(patientReply);
112
113
  }
113
114
 
115
+ const startTime = Date.now();
114
116
  let run, output, completed;
115
117
  let retries = 0;
116
118
  const maxRetries = DEFAULT_MAX_RETRIES;
@@ -128,14 +130,24 @@ const runAssistantWithRetries = async (thread, assistant, runConfig, patientRepl
128
130
  )(thread, assistant, runConfig, retries));
129
131
 
130
132
  if (completed && output) break;
131
- 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
+ }
132
141
  } while (retries < maxRetries && (!completed || !output));
133
142
 
134
- if (run?.last_error) console.log('[runAssistantWithRetries] RUN LAST ERROR:', run.last_error);
135
- console.log('[runAssistantWithRetries] RUN STATUS', completed);
136
- console.log('[runAssistantWithRetries] OUTPUT', output);
143
+ const predictionTimeMs = Date.now() - startTime;
144
+
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 });
137
149
 
138
- return { run, output, completed, retries };
150
+ return { run, output, completed, retries, predictionTimeMs };
139
151
  };
140
152
 
141
153
  module.exports = {
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();
@@ -0,0 +1,21 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const predictionMetricsSchema = new mongoose.Schema({
4
+ message_id: { type: String, required: true, index: true },
5
+ numero: { type: String, required: true, index: true },
6
+ assistant_id: { type: String, required: true, index: true },
7
+ thread_id: { type: String, required: true },
8
+ prediction_time_ms: { type: Number, required: true },
9
+ retry_count: { type: Number, required: true, default: 1 },
10
+ completed: { type: Boolean, default: true },
11
+ error: { type: String, default: null },
12
+ timing_breakdown: { type: Object, default: {} }
13
+ }, { timestamps: true });
14
+
15
+ predictionMetricsSchema.index({ createdAt: -1 });
16
+ predictionMetricsSchema.index({ assistant_id: 1, createdAt: -1 });
17
+ predictionMetricsSchema.index({ numero: 1, createdAt: -1 });
18
+
19
+ const PredictionMetrics = mongoose.model('PredictionMetrics', predictionMetricsSchema);
20
+
21
+ module.exports = { PredictionMetrics, predictionMetricsSchema };
@@ -6,6 +6,7 @@ const { BaseAssistant } = require('../assistants/BaseAssistant');
6
6
  const { createProvider } = require('../providers/createProvider');
7
7
 
8
8
  const { Thread } = require('../models/threadModel.js');
9
+ const { PredictionMetrics } = require('../models/predictionMetricsModel');
9
10
 
10
11
  const { getCurRow } = require('../helpers/assistantHelper.js');
11
12
  const { runAssistantAndWait, runAssistantWithRetries } = require('../helpers/assistantHelper.js');
@@ -14,6 +15,7 @@ const { withTracing } = require('../utils/tracingDecorator.js');
14
15
  const { processIndividualMessage } = require('../helpers/processHelper.js');
15
16
  const { getLastMessages } = require('../helpers/messageHelper.js');
16
17
  const { combineImagesToPDF, cleanupFiles } = require('../helpers/filesHelper.js');
18
+ const { logger } = require('../middleware/requestId');
17
19
 
18
20
  const DEFAULT_MAX_RETRIES = parseInt(process.env.MAX_RETRIES || '30', 10);
19
21
 
@@ -268,6 +270,10 @@ const addInsAssistant = withTracing(
268
270
  );
269
271
 
270
272
  const replyAssistantCore = async (code, message_ = null, thread_ = null, runOptions = {}) => {
273
+ const timings = {};
274
+ const startTotal = Date.now();
275
+
276
+ timings.getThread = Date.now();
271
277
  const thread = thread_ || await withTracing(getThread, 'get_thread_operation',
272
278
  (threadCode) => ({
273
279
  'thread.code': threadCode,
@@ -275,30 +281,43 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
275
281
  'thread.provided': !!thread_
276
282
  })
277
283
  )(code);
284
+ timings.getThread = Date.now() - timings.getThread;
285
+
278
286
  if (!thread) return null;
279
287
 
288
+ timings.getMessages = Date.now();
280
289
  const patientReply = await getLastMessages(code);
290
+ timings.getMessages = Date.now() - timings.getMessages;
291
+
281
292
  if (!patientReply) {
282
- console.log('[replyAssistantCore] No relevant data found for this assistant.');
293
+ logger.log('[replyAssistantCore] No relevant data found for this assistant.');
283
294
  return null;
284
295
  }
285
296
 
286
297
  const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
287
298
 
288
- let patientMsg = false;
289
- let urls = [];
290
- for (let i = 0; i < patientReply.length; i++) {
291
- const reply = patientReply[i];
292
- const { isPatient, url } = await processIndividualMessage(code, reply, provider, thread);
293
- console.log(`[replyAssistantCore] Processing message ${i + 1}/${patientReply.length}: isPatient=${isPatient}, hasUrl=${!!url}`);
294
- patientMsg = patientMsg || isPatient;
295
- if (url) urls.push({ 'url': url });
296
- }
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 }));
297
314
 
298
315
  if (urls.length > 0) {
299
- 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`);
300
318
  const { pdfBuffer, processedFiles } = await combineImagesToPDF({ code });
301
- 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`);
302
321
 
303
322
  if (pdfBuffer) {
304
323
  const key = `${code}-${Date.now()}-combined.pdf`;
@@ -315,8 +334,9 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
315
334
 
316
335
  if (!patientMsg || thread.stopped) return null;
317
336
 
337
+ timings.runAssistant = Date.now();
318
338
  const assistant = getAssistantById(thread.getAssistantId(), thread);
319
- const { run, output, completed, retries } = await withTracing(
339
+ const { run, output, completed, retries, predictionTimeMs } = await withTracing(
320
340
  runAssistantWithRetries,
321
341
  'run_assistant_with_retries',
322
342
  (thread, assistant, runConfig, patientReply) => ({
@@ -325,6 +345,36 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
325
345
  'assistant.has_patient_reply': !!patientReply
326
346
  })
327
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
+ });
358
+
359
+ if (output && predictionTimeMs) {
360
+ await PredictionMetrics.create({
361
+ message_id: `${code}-${Date.now()}`,
362
+ numero: code,
363
+ assistant_id: thread.getAssistantId(),
364
+ thread_id: thread.getConversationId(),
365
+ prediction_time_ms: predictionTimeMs,
366
+ retry_count: retries,
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
+ }
376
+ }).catch(err => console.error('[replyAssistantCore] Failed to store metrics:', err));
377
+ }
328
378
 
329
379
  return output;
330
380
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "2.4.3",
3
+ "version": "2.4.5",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",