@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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|