@peopl-health/nexus 2.4.7 → 2.4.9-fix-pdf-processing
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 +23 -14
- package/lib/helpers/llmsHelper.js +17 -10
- package/lib/helpers/mediaHelper.js +3 -2
- package/lib/helpers/messageHelper.js +12 -11
- package/lib/helpers/processHelper.js +2 -2
- package/lib/helpers/qrHelper.js +2 -1
- package/lib/helpers/twilioMediaProcessor.js +19 -29
- 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 +24 -17
- package/lib/providers/OpenAIResponsesProviderTools.js +3 -5
- package/lib/providers/createProvider.js +2 -1
- package/lib/services/airtableService.js +6 -5
- package/lib/services/assistantService.js +73 -57
- 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/mediaValidator.js +18 -14
- package/lib/utils/sanitizer.js +0 -6
- package/lib/utils/tracingDecorator.js +7 -1
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@ const {
|
|
|
5
5
|
handleRequiresAction: handleRequiresActionUtil,
|
|
6
6
|
transformToolsForResponsesAPI: transformToolsForResponsesAPIUtil
|
|
7
7
|
} = require('./OpenAIResponsesProviderTools');
|
|
8
|
+
const { logger } = require('../utils/logger');
|
|
8
9
|
|
|
9
10
|
const CONVERSATION_PREFIX = 'conv_';
|
|
10
11
|
const RESPONSE_PREFIX = 'resp_';
|
|
@@ -81,7 +82,7 @@ class OpenAIResponsesProvider {
|
|
|
81
82
|
: messages;
|
|
82
83
|
|
|
83
84
|
if (messages.length > DEFAULT_MAX_HISTORICAL_MESSAGES) {
|
|
84
|
-
|
|
85
|
+
logger.warn(`[OpenAIResponsesProvider] Capped ${messages.length} → ${DEFAULT_MAX_HISTORICAL_MESSAGES} messages`);
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
const allItems = this._conversationItems(messagesToProcess);
|
|
@@ -108,7 +109,7 @@ class OpenAIResponsesProvider {
|
|
|
108
109
|
const totalBatches = Math.ceil(remainingItems.length / MAX_ITEMS_PER_BATCH);
|
|
109
110
|
|
|
110
111
|
// Concise batch summary
|
|
111
|
-
|
|
112
|
+
logger.info(`[OpenAIResponsesProvider] Batching: ${initialItems.length} (create) + ${remainingItems.length} (${totalBatches} batches) = ${totalItems} total`);
|
|
112
113
|
|
|
113
114
|
const payload = this._cleanObject({
|
|
114
115
|
metadata,
|
|
@@ -125,7 +126,7 @@ class OpenAIResponsesProvider {
|
|
|
125
126
|
return await this._post('/conversations', payload);
|
|
126
127
|
});
|
|
127
128
|
} catch (error) {
|
|
128
|
-
|
|
129
|
+
logger.error('[OpenAIResponsesProvider] Failed to create conversation:', error?.message || error);
|
|
129
130
|
throw error;
|
|
130
131
|
}
|
|
131
132
|
|
|
@@ -134,7 +135,7 @@ class OpenAIResponsesProvider {
|
|
|
134
135
|
try {
|
|
135
136
|
await this._addItemsInBatches(conversation.id, remainingItems);
|
|
136
137
|
} catch (error) {
|
|
137
|
-
|
|
138
|
+
logger.error('[OpenAIResponsesProvider] Failed to add remaining messages. Conversation created with partial history:', error?.message || error);
|
|
138
139
|
}
|
|
139
140
|
}
|
|
140
141
|
|
|
@@ -168,12 +169,12 @@ class OpenAIResponsesProvider {
|
|
|
168
169
|
return await this._post(`/conversations/${id}/items`, { items: batchPayload });
|
|
169
170
|
});
|
|
170
171
|
} catch (error) {
|
|
171
|
-
|
|
172
|
+
logger.error(`[OpenAIResponsesProvider] Batch ${batchNumber}/${totalBatches} failed:`, error?.message || error);
|
|
172
173
|
throw error;
|
|
173
174
|
}
|
|
174
175
|
}
|
|
175
176
|
|
|
176
|
-
|
|
177
|
+
logger.info(`[OpenAIResponsesProvider] Successfully added ${items.length} messages in ${totalBatches} batches`);
|
|
177
178
|
}
|
|
178
179
|
|
|
179
180
|
/**
|
|
@@ -211,6 +212,12 @@ class OpenAIResponsesProvider {
|
|
|
211
212
|
|
|
212
213
|
if (payloads.length === 0) return null;
|
|
213
214
|
|
|
215
|
+
if (payloads.length > MAX_ITEMS_PER_BATCH) {
|
|
216
|
+
logger.info(`[OpenAIResponsesProvider] Batching ${payloads.length} messages into chunks of ${MAX_ITEMS_PER_BATCH}`);
|
|
217
|
+
await this._addItemsInBatches(id, payloads, MAX_ITEMS_PER_BATCH);
|
|
218
|
+
return { batched: true, count: payloads.length };
|
|
219
|
+
}
|
|
220
|
+
|
|
214
221
|
return this._retryWithRateLimit(async () => {
|
|
215
222
|
if (this.conversations?.items?.create) {
|
|
216
223
|
return await this.conversations.items.create(id, { items: payloads });
|
|
@@ -235,7 +242,7 @@ class OpenAIResponsesProvider {
|
|
|
235
242
|
if (items.length === 0) return;
|
|
236
243
|
|
|
237
244
|
if (deleteAll) {
|
|
238
|
-
|
|
245
|
+
logger.info(`[OpenAIResponsesProvider] Deleting all ${items.length} items from conversation`);
|
|
239
246
|
for (const item of items) {
|
|
240
247
|
await this.conversations.items.delete(item.id, {conversation_id: id});
|
|
241
248
|
}
|
|
@@ -249,12 +256,12 @@ class OpenAIResponsesProvider {
|
|
|
249
256
|
const hasOutput = functionOutputs.some(output => output.call_id === functionCall.call_id);
|
|
250
257
|
|
|
251
258
|
if (!hasOutput) {
|
|
252
|
-
|
|
259
|
+
logger.info(`[OpenAIResponsesProvider] Deleting orphaned function_call: ${functionCall.id} (${functionCall.call_id})`);
|
|
253
260
|
await this.conversations.items.delete(functionCall.id, {conversation_id: id});
|
|
254
261
|
}
|
|
255
262
|
}
|
|
256
263
|
} catch (error) {
|
|
257
|
-
|
|
264
|
+
logger.warn('[OpenAIResponsesProvider] Failed to cleanup conversation:', error?.message);
|
|
258
265
|
}
|
|
259
266
|
}
|
|
260
267
|
|
|
@@ -288,7 +295,7 @@ class OpenAIResponsesProvider {
|
|
|
288
295
|
toolOutputs = pendingOutputs;
|
|
289
296
|
}
|
|
290
297
|
} catch (error) {
|
|
291
|
-
|
|
298
|
+
logger.warn('[OpenAIResponsesProvider] Error checking for pending function calls:', error?.message);
|
|
292
299
|
}
|
|
293
300
|
}
|
|
294
301
|
|
|
@@ -324,7 +331,7 @@ class OpenAIResponsesProvider {
|
|
|
324
331
|
object: response.object || 'response',
|
|
325
332
|
};
|
|
326
333
|
} catch (error) {
|
|
327
|
-
|
|
334
|
+
logger.error('[OpenAIResponsesProvider] Error running conversation:', error);
|
|
328
335
|
throw error;
|
|
329
336
|
}
|
|
330
337
|
}
|
|
@@ -419,7 +426,7 @@ class OpenAIResponsesProvider {
|
|
|
419
426
|
async checkRunStatus(assistant, thread_id, run_id, retryCount = 0, maxRetries = DEFAULT_MAX_RETRIES, actionHandled = false) {
|
|
420
427
|
try {
|
|
421
428
|
let run = await this.getRun({ threadId: thread_id, runId: run_id });
|
|
422
|
-
|
|
429
|
+
logger.info(`Status: ${run.status} ${thread_id} ${run_id} (attempt ${retryCount + 1})`);
|
|
423
430
|
|
|
424
431
|
if (run.status === 'completed') {
|
|
425
432
|
return {run, completed: true};
|
|
@@ -432,12 +439,12 @@ class OpenAIResponsesProvider {
|
|
|
432
439
|
const needsFunctionCall = run.output?.some(item => item.type === 'function_call');
|
|
433
440
|
if (needsFunctionCall && !actionHandled) {
|
|
434
441
|
if (retryCount >= maxRetries) {
|
|
435
|
-
|
|
442
|
+
logger.warn('[OpenAIResponsesProvider] Max retries reached while handling function calls');
|
|
436
443
|
return {run, completed: false};
|
|
437
444
|
}
|
|
438
445
|
|
|
439
446
|
const outputs = await handleRequiresActionUtil(assistant, run);
|
|
440
|
-
|
|
447
|
+
logger.info('[OpenAIResponsesProvider] Function call outputs:', outputs);
|
|
441
448
|
|
|
442
449
|
if (outputs.length > 0) {
|
|
443
450
|
try {
|
|
@@ -451,7 +458,7 @@ class OpenAIResponsesProvider {
|
|
|
451
458
|
|
|
452
459
|
return this.checkRunStatus(assistant, thread_id, run_id, retryCount + 1, maxRetries, true);
|
|
453
460
|
} catch (submitError) {
|
|
454
|
-
|
|
461
|
+
logger.error('[OpenAIResponsesProvider] Error submitting tool outputs:', submitError);
|
|
455
462
|
if (retryCount < maxRetries) {
|
|
456
463
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
457
464
|
return this.checkRunStatus(assistant, thread_id, run_id, retryCount + 1, maxRetries, false);
|
|
@@ -459,7 +466,7 @@ class OpenAIResponsesProvider {
|
|
|
459
466
|
return {run, completed: false};
|
|
460
467
|
}
|
|
461
468
|
} else {
|
|
462
|
-
|
|
469
|
+
logger.warn('[OpenAIResponsesProvider] Function calls detected but no outputs generated');
|
|
463
470
|
return {run, completed: false};
|
|
464
471
|
}
|
|
465
472
|
}
|
|
@@ -471,7 +478,7 @@ class OpenAIResponsesProvider {
|
|
|
471
478
|
|
|
472
479
|
return {run, completed: false};
|
|
473
480
|
} catch (error) {
|
|
474
|
-
|
|
481
|
+
logger.error('[OpenAIResponsesProvider] Error checking run status:', error);
|
|
475
482
|
return {run: null, completed: false};
|
|
476
483
|
}
|
|
477
484
|
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Tool and function call handling utilities for OpenAIResponsesProvider
|
|
3
|
-
*/
|
|
1
|
+
const { logger } = require('../utils/logger');
|
|
4
2
|
|
|
5
3
|
/**
|
|
6
4
|
* Execute a function call and return the output format
|
|
@@ -19,7 +17,7 @@ async function executeFunctionCall(assistant, call) {
|
|
|
19
17
|
output: typeof result === 'string' ? result : JSON.stringify(result)
|
|
20
18
|
};
|
|
21
19
|
} catch (error) {
|
|
22
|
-
|
|
20
|
+
logger.error('[OpenAIResponsesProvider] Tool execution failed', error);
|
|
23
21
|
return {
|
|
24
22
|
type: 'function_call_output',
|
|
25
23
|
call_id: call.call_id,
|
|
@@ -46,7 +44,7 @@ async function handlePendingFunctionCalls(assistant, conversationItems) {
|
|
|
46
44
|
return [];
|
|
47
45
|
}
|
|
48
46
|
|
|
49
|
-
|
|
47
|
+
logger.info(`[OpenAIResponsesProvider] Found ${orphanedCalls.length} pending function calls, handling them...`);
|
|
50
48
|
const outputs = [];
|
|
51
49
|
for (const call of orphanedCalls) {
|
|
52
50
|
outputs.push(await executeFunctionCall(assistant, call));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { OpenAIAssistantsProvider } = require('./OpenAIAssistantsProvider');
|
|
2
2
|
const { OpenAIResponsesProvider } = require('./OpenAIResponsesProvider');
|
|
3
|
+
const { logger } = require('../utils/logger');
|
|
3
4
|
|
|
4
5
|
const PROVIDER_VARIANTS = {
|
|
5
6
|
assistants: OpenAIAssistantsProvider,
|
|
@@ -10,7 +11,7 @@ const PROVIDER_VARIANTS = {
|
|
|
10
11
|
* Returns the appropriate OpenAI provider implementation for the requested variant.
|
|
11
12
|
*/
|
|
12
13
|
function createProvider(config = {}) {
|
|
13
|
-
|
|
14
|
+
logger.debug('Creating OpenAI provider', { variant: config.variant || 'assistants' });
|
|
14
15
|
const variant = (config.variant || config.providerVariant || config.llmVariant || 'assistants')
|
|
15
16
|
.toString()
|
|
16
17
|
.toLowerCase();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { airtable } = require('../config/airtableConfig');
|
|
2
|
+
const { logger } = require('../utils/logger');
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
async function addRecord(baseID, tableName, fields) {
|
|
@@ -6,10 +7,10 @@ async function addRecord(baseID, tableName, fields) {
|
|
|
6
7
|
if (!airtable) throw new Error('Airtable not configured. Set AIRTABLE_API_KEY');
|
|
7
8
|
const base = airtable.base(baseID);
|
|
8
9
|
const record = await base(tableName).create(fields);
|
|
9
|
-
|
|
10
|
+
logger.info('Record added at', tableName);
|
|
10
11
|
return record;
|
|
11
12
|
} catch (error) {
|
|
12
|
-
|
|
13
|
+
logger.error('Error adding record:', error);
|
|
13
14
|
throw error;
|
|
14
15
|
}
|
|
15
16
|
}
|
|
@@ -30,7 +31,7 @@ async function getRecords(baseID, tableName) {
|
|
|
30
31
|
});
|
|
31
32
|
return records;
|
|
32
33
|
} catch (error) {
|
|
33
|
-
|
|
34
|
+
logger.error('Error fetching records:', error);
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -51,7 +52,7 @@ async function getRecordByFilter(baseID, tableName, filter, view = 'Grid view')
|
|
|
51
52
|
});
|
|
52
53
|
return records;
|
|
53
54
|
} catch (error) {
|
|
54
|
-
|
|
55
|
+
logger.error(`Error fetching records by ${filter}:`, error);
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
|
|
@@ -74,7 +75,7 @@ async function updateRecordByFilter(baseID, tableName, filter, updateFields) {
|
|
|
74
75
|
|
|
75
76
|
return updatedRecords;
|
|
76
77
|
} catch (error) {
|
|
77
|
-
|
|
78
|
+
logger.error(`Error updating records by ${filter}:`, error);
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
|
|
@@ -15,7 +15,7 @@ const { withTracing } = require('../utils/tracingDecorator.js');
|
|
|
15
15
|
const { processThreadMessage } = require('../helpers/processHelper.js');
|
|
16
16
|
const { getLastMessages, updateMessageRecord } = require('../helpers/messageHelper.js');
|
|
17
17
|
const { combineImagesToPDF, cleanupFiles } = require('../helpers/filesHelper.js');
|
|
18
|
-
const { logger } = require('../
|
|
18
|
+
const { logger } = require('../utils/logger');
|
|
19
19
|
|
|
20
20
|
const DEFAULT_MAX_RETRIES = parseInt(process.env.MAX_RETRIES || '30', 10);
|
|
21
21
|
|
|
@@ -136,9 +136,9 @@ const getAssistantById = (assistant_id, thread) => {
|
|
|
136
136
|
|
|
137
137
|
const createAssistant = async (code, assistant_id, messages=[], force=false) => {
|
|
138
138
|
const findThread = await Thread.findOne({ code: code });
|
|
139
|
-
|
|
139
|
+
logger.info('[createAssistant] findThread', findThread);
|
|
140
140
|
if (findThread && findThread.getConversationId()) {
|
|
141
|
-
|
|
141
|
+
logger.info('[createAssistant] Thread already exists');
|
|
142
142
|
const updateFields = { active: true, stopped: false };
|
|
143
143
|
Thread.setAssistantId(updateFields, assistant_id);
|
|
144
144
|
await Thread.updateOne({ code: code }, { $set: updateFields });
|
|
@@ -146,7 +146,7 @@ const createAssistant = async (code, assistant_id, messages=[], force=false) =>
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
const curRow = await getCurRow(Historial_Clinico_ID, code);
|
|
149
|
-
|
|
149
|
+
logger.info('[createAssistant] curRow', curRow[0]);
|
|
150
150
|
const nombre = curRow?.[0]?.['name'] || null;
|
|
151
151
|
const patientId = curRow?.[0]?.['record_id'] || null;
|
|
152
152
|
|
|
@@ -175,7 +175,7 @@ const createAssistant = async (code, assistant_id, messages=[], force=false) =>
|
|
|
175
175
|
const condition = { $or: [{ code: code }] };
|
|
176
176
|
const options = { new: true, upsert: true };
|
|
177
177
|
const updatedThread = await Thread.findOneAndUpdate(condition, {run_id: null, ...thread}, options);
|
|
178
|
-
|
|
178
|
+
logger.info('[createAssistant] Updated thread:', updatedThread);
|
|
179
179
|
|
|
180
180
|
// Delete previous thread
|
|
181
181
|
if (force) {
|
|
@@ -188,12 +188,12 @@ const createAssistant = async (code, assistant_id, messages=[], force=false) =>
|
|
|
188
188
|
const addMsgAssistant = async (code, inMessages, role = 'user', reply = false) => {
|
|
189
189
|
try {
|
|
190
190
|
const thread = await Thread.findOne({ code: code });
|
|
191
|
-
|
|
191
|
+
logger.info(thread);
|
|
192
192
|
if (thread === null) return null;
|
|
193
193
|
|
|
194
194
|
const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
|
|
195
195
|
for (const message of inMessages) {
|
|
196
|
-
|
|
196
|
+
logger.info(message);
|
|
197
197
|
await provider.addMessage({
|
|
198
198
|
threadId: thread.getConversationId(),
|
|
199
199
|
role: role,
|
|
@@ -209,17 +209,17 @@ const addMsgAssistant = async (code, inMessages, role = 'user', reply = false) =
|
|
|
209
209
|
const assistant = getAssistantById(thread.getAssistantId(), thread);
|
|
210
210
|
do {
|
|
211
211
|
({ output, completed } = await runAssistantAndWait({ thread, assistant }));
|
|
212
|
-
|
|
212
|
+
logger.info(`Attempt ${retries + 1}: completed=${completed}, output=${output || '(empty)'}`);
|
|
213
213
|
|
|
214
214
|
if (completed && output) break;
|
|
215
215
|
if (retries < maxRetries) await new Promise(resolve => setTimeout(resolve, 2000));
|
|
216
216
|
retries++;
|
|
217
217
|
} while (retries <= maxRetries && (!completed || !output));
|
|
218
218
|
|
|
219
|
-
|
|
219
|
+
logger.info('THE ANS IS', output);
|
|
220
220
|
return output;
|
|
221
221
|
} catch (error) {
|
|
222
|
-
|
|
222
|
+
logger.info(error);
|
|
223
223
|
return null;
|
|
224
224
|
}
|
|
225
225
|
};
|
|
@@ -254,7 +254,7 @@ const addInstructionCore = async (code, instruction, role = 'user') => {
|
|
|
254
254
|
null // no patientReply for instructions
|
|
255
255
|
);
|
|
256
256
|
|
|
257
|
-
|
|
257
|
+
logger.info('RUN RESPONSE', output);
|
|
258
258
|
return output;
|
|
259
259
|
};
|
|
260
260
|
|
|
@@ -273,33 +273,48 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
|
|
|
273
273
|
const timings = {};
|
|
274
274
|
const startTotal = Date.now();
|
|
275
275
|
|
|
276
|
-
|
|
277
|
-
|
|
276
|
+
const { result: thread, duration: getThreadMs } = await withTracing(
|
|
277
|
+
getThread,
|
|
278
|
+
'get_thread_operation',
|
|
278
279
|
(threadCode) => ({
|
|
279
280
|
'thread.code': threadCode,
|
|
280
281
|
'operation.type': 'thread_retrieval',
|
|
281
282
|
'thread.provided': !!thread_
|
|
282
|
-
})
|
|
283
|
+
}),
|
|
284
|
+
{ returnTiming: true }
|
|
283
285
|
)(code);
|
|
284
|
-
timings.
|
|
286
|
+
timings.get_thread_ms = getThreadMs;
|
|
285
287
|
|
|
286
|
-
if (!thread) return null;
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const patientReply = await
|
|
290
|
-
|
|
288
|
+
if (!thread_ && !thread) return null;
|
|
289
|
+
const finalThread = thread_ || thread;
|
|
290
|
+
|
|
291
|
+
const { result: patientReply, duration: getMessagesMs } = await withTracing(
|
|
292
|
+
getLastMessages,
|
|
293
|
+
'get_last_messages',
|
|
294
|
+
(code) => ({ 'thread.code': code }),
|
|
295
|
+
{ returnTiming: true }
|
|
296
|
+
)(code);
|
|
297
|
+
timings.get_messages_ms = getMessagesMs;
|
|
291
298
|
|
|
292
299
|
if (!patientReply) {
|
|
293
|
-
logger.
|
|
300
|
+
logger.info('[replyAssistantCore] No relevant data found for this assistant.');
|
|
294
301
|
return null;
|
|
295
302
|
}
|
|
296
303
|
|
|
297
304
|
const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
|
|
298
305
|
|
|
299
|
-
|
|
300
|
-
logger.log(`[replyAssistantCore] Processing ${patientReply.length} messages in parallel`);
|
|
306
|
+
logger.info(`[replyAssistantCore] Processing ${patientReply.length} messages in parallel`);
|
|
301
307
|
|
|
302
|
-
const processResults = await
|
|
308
|
+
const { result: processResults, duration: processMessagesMs } = await withTracing(
|
|
309
|
+
processThreadMessage,
|
|
310
|
+
'process_thread_messages',
|
|
311
|
+
(code, patientReply, provider) => ({
|
|
312
|
+
'messages.count': patientReply.length,
|
|
313
|
+
'thread.code': code
|
|
314
|
+
}),
|
|
315
|
+
{ returnTiming: true }
|
|
316
|
+
)(code, patientReply, provider);
|
|
317
|
+
timings.process_messages_ms = processMessagesMs;
|
|
303
318
|
|
|
304
319
|
const patientMsg = processResults.some(r => r.isPatient);
|
|
305
320
|
const urls = processResults.filter(r => r.url).map(r => ({ url: r.url }));
|
|
@@ -307,22 +322,28 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
|
|
|
307
322
|
const allTempFiles = processResults.flatMap(r => r.tempFiles || []);
|
|
308
323
|
|
|
309
324
|
if (allMessagesToAdd.length > 0) {
|
|
310
|
-
const threadId =
|
|
311
|
-
logger.
|
|
325
|
+
const threadId = finalThread.getConversationId();
|
|
326
|
+
logger.info(`[replyAssistantCore] Adding ${allMessagesToAdd.length} messages to thread in batch`);
|
|
312
327
|
await provider.addMessage({ threadId, messages: allMessagesToAdd });
|
|
313
328
|
}
|
|
314
329
|
|
|
315
|
-
await Promise.all(processResults.map(r => updateMessageRecord(r.reply,
|
|
330
|
+
await Promise.all(processResults.map(r => updateMessageRecord(r.reply, finalThread)));
|
|
316
331
|
await cleanupFiles(allTempFiles);
|
|
317
332
|
|
|
318
|
-
timings.processMessages = Date.now() - timings.processMessages;
|
|
319
|
-
|
|
320
333
|
if (urls.length > 0) {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
334
|
+
logger.info(`[replyAssistantCore] Processing ${urls.length} URLs for PDF combination`);
|
|
335
|
+
const { result: pdfResult, duration: pdfCombinationMs } = await withTracing(
|
|
336
|
+
combineImagesToPDF,
|
|
337
|
+
'combine_images_to_pdf',
|
|
338
|
+
({ code }) => ({
|
|
339
|
+
'pdf.thread_code': code,
|
|
340
|
+
'pdf.url_count': urls.length
|
|
341
|
+
}),
|
|
342
|
+
{ returnTiming: true }
|
|
343
|
+
)({ code });
|
|
344
|
+
timings.pdf_combination_ms = pdfCombinationMs;
|
|
345
|
+
const { pdfBuffer, processedFiles } = pdfResult;
|
|
346
|
+
logger.info(`[replyAssistantCore] PDF combination complete: ${processedFiles?.length || 0} files processed`);
|
|
326
347
|
|
|
327
348
|
if (pdfBuffer) {
|
|
328
349
|
const key = `${code}-${Date.now()}-combined.pdf`;
|
|
@@ -337,48 +358,43 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
|
|
|
337
358
|
}
|
|
338
359
|
}
|
|
339
360
|
|
|
340
|
-
if (!patientMsg ||
|
|
361
|
+
if (!patientMsg || finalThread.stopped) return null;
|
|
341
362
|
|
|
342
|
-
|
|
343
|
-
const
|
|
344
|
-
const { run, output, completed, retries, predictionTimeMs } = await withTracing(
|
|
363
|
+
const assistant = getAssistantById(finalThread.getAssistantId(), finalThread);
|
|
364
|
+
const { result: runResult, duration: runAssistantMs } = await withTracing(
|
|
345
365
|
runAssistantWithRetries,
|
|
346
366
|
'run_assistant_with_retries',
|
|
347
367
|
(thread, assistant, runConfig, patientReply) => ({
|
|
348
368
|
'assistant.id': thread.getAssistantId(),
|
|
349
369
|
'assistant.max_retries': DEFAULT_MAX_RETRIES,
|
|
350
370
|
'assistant.has_patient_reply': !!patientReply
|
|
351
|
-
})
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
timings.
|
|
371
|
+
}),
|
|
372
|
+
{ returnTiming: true }
|
|
373
|
+
)(finalThread, assistant, runOptions, patientReply);
|
|
374
|
+
timings.run_assistant_ms = runAssistantMs;
|
|
375
|
+
timings.total_ms = Date.now() - startTotal;
|
|
376
|
+
|
|
377
|
+
const { run, output, completed, retries, predictionTimeMs } = runResult;
|
|
355
378
|
|
|
356
|
-
logger.
|
|
379
|
+
logger.info('[Assistant Reply Complete]', {
|
|
357
380
|
code: code ? `${code.substring(0, 3)}***${code.slice(-4)}` : 'unknown',
|
|
358
381
|
messageCount: patientReply.length,
|
|
359
382
|
hasMedia: urls.length > 0,
|
|
360
383
|
retries,
|
|
361
|
-
|
|
384
|
+
totalMs: timings.total_ms
|
|
362
385
|
});
|
|
363
386
|
|
|
364
387
|
if (output && predictionTimeMs) {
|
|
365
388
|
await PredictionMetrics.create({
|
|
366
389
|
message_id: `${code}-${Date.now()}`,
|
|
367
390
|
numero: code,
|
|
368
|
-
assistant_id:
|
|
369
|
-
thread_id:
|
|
391
|
+
assistant_id: finalThread.getAssistantId(),
|
|
392
|
+
thread_id: finalThread.getConversationId(),
|
|
370
393
|
prediction_time_ms: predictionTimeMs,
|
|
371
394
|
retry_count: retries,
|
|
372
395
|
completed: completed,
|
|
373
|
-
timing_breakdown:
|
|
374
|
-
|
|
375
|
-
get_messages_ms: timings.getMessages,
|
|
376
|
-
process_messages_ms: timings.processMessages,
|
|
377
|
-
pdf_combination_ms: timings.pdfCombination || 0,
|
|
378
|
-
run_assistant_ms: timings.runAssistant,
|
|
379
|
-
total_ms: timings.total
|
|
380
|
-
}
|
|
381
|
-
}).catch(err => console.error('[replyAssistantCore] Failed to store metrics:', err));
|
|
396
|
+
timing_breakdown: timings
|
|
397
|
+
}).catch(err => logger.error('[replyAssistantCore] Failed to store metrics:', err));
|
|
382
398
|
}
|
|
383
399
|
|
|
384
400
|
return output;
|
|
@@ -398,7 +414,7 @@ const replyAssistant = withTracing(
|
|
|
398
414
|
const switchAssistant = async (code, assistant_id) => {
|
|
399
415
|
try {
|
|
400
416
|
const thread = await Thread.findOne({ code: code });
|
|
401
|
-
|
|
417
|
+
logger.info('Inside thread', thread);
|
|
402
418
|
if (thread === null) return;
|
|
403
419
|
|
|
404
420
|
const variant = process.env.VARIANT || 'assistants';
|
|
@@ -412,7 +428,7 @@ const switchAssistant = async (code, assistant_id) => {
|
|
|
412
428
|
|
|
413
429
|
await Thread.updateOne({ code }, { $set: updateFields });
|
|
414
430
|
} catch (error) {
|
|
415
|
-
|
|
431
|
+
logger.info(error);
|
|
416
432
|
return null;
|
|
417
433
|
}
|
|
418
434
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { Message } = require('../models/messageModel');
|
|
2
2
|
const { Historial_Clinico_ID } = require('../config/airtableConfig');
|
|
3
3
|
const { getRecordByFilter } = require('./airtableService');
|
|
4
|
+
const { logger } = require('../utils/logger');
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
const fetchConversationData = async (filter, skip, limit) => {
|
|
@@ -16,11 +17,11 @@ const fetchConversationData = async (filter, skip, limit) => {
|
|
|
16
17
|
{ read: { $exists: false } }
|
|
17
18
|
]
|
|
18
19
|
};
|
|
19
|
-
|
|
20
|
+
logger.info('Applying unread filter');
|
|
20
21
|
break;
|
|
21
22
|
|
|
22
23
|
case 'no-response':
|
|
23
|
-
|
|
24
|
+
logger.info('Applying no-response filter');
|
|
24
25
|
break;
|
|
25
26
|
|
|
26
27
|
case 'recent': {
|
|
@@ -30,18 +31,18 @@ const fetchConversationData = async (filter, skip, limit) => {
|
|
|
30
31
|
is_group: false,
|
|
31
32
|
createdAt: { $gt: yesterday }
|
|
32
33
|
};
|
|
33
|
-
|
|
34
|
+
logger.info('Applying recent filter (last 24 hours)');
|
|
34
35
|
break;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
case 'all':
|
|
38
39
|
default:
|
|
39
40
|
filterConditions = { is_group: false };
|
|
40
|
-
|
|
41
|
+
logger.info('Applying all conversations filter');
|
|
41
42
|
break;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
logger.info('Executing aggregation pipeline...');
|
|
45
46
|
const aggregationStartTime = Date.now();
|
|
46
47
|
|
|
47
48
|
let aggregationPipeline = [
|
|
@@ -77,7 +78,7 @@ const fetchConversationData = async (filter, skip, limit) => {
|
|
|
77
78
|
const conversations = await Message.aggregate(aggregationPipeline);
|
|
78
79
|
|
|
79
80
|
const aggregationTime = Date.now() - aggregationStartTime;
|
|
80
|
-
|
|
81
|
+
logger.info(`Aggregation completed in ${aggregationTime}ms, found ${conversations.length} conversations`);
|
|
81
82
|
|
|
82
83
|
// Fetch names from Airtable and WhatsApp
|
|
83
84
|
const phoneNumbers = conversations.map(conv => conv._id).filter(Boolean);
|
|
@@ -89,7 +90,7 @@ const fetchConversationData = async (filter, skip, limit) => {
|
|
|
89
90
|
map[patient.whatsapp_id] = patient.name;
|
|
90
91
|
return map;
|
|
91
92
|
}, {});
|
|
92
|
-
|
|
93
|
+
logger.info(`Found ${Object.keys(airtableNameMap).length} names in Airtable`);
|
|
93
94
|
|
|
94
95
|
const contactNames = await Message.aggregate([
|
|
95
96
|
{ $match: { is_group: false, from_me: false } },
|
|
@@ -110,7 +111,7 @@ const fetchConversationData = async (filter, skip, limit) => {
|
|
|
110
111
|
}, {...airtableNameMap}) || airtableNameMap || {};
|
|
111
112
|
|
|
112
113
|
// Fetch unread counts
|
|
113
|
-
|
|
114
|
+
logger.info('Fetching unread counts using Message.aggregate');
|
|
114
115
|
const unreadCounts = await Message.aggregate([
|
|
115
116
|
{
|
|
116
117
|
$match: {
|
|
@@ -134,8 +135,8 @@ const fetchConversationData = async (filter, skip, limit) => {
|
|
|
134
135
|
}
|
|
135
136
|
return map;
|
|
136
137
|
}, {}) || {};
|
|
137
|
-
|
|
138
|
-
|
|
138
|
+
logger.info('unreadMap', JSON.stringify(unreadMap));
|
|
139
|
+
logger.info('Number of conversations found:', conversations?.length || 0);
|
|
139
140
|
|
|
140
141
|
// Calculate total count for pagination
|
|
141
142
|
let totalFilterConditions = { is_group: false };
|
|
@@ -193,14 +194,14 @@ const fetchConversationData = async (filter, skip, limit) => {
|
|
|
193
194
|
* Processes conversations to prepare them for the response
|
|
194
195
|
*/
|
|
195
196
|
const processConversations = async (conversations, nameMap, unreadMap) => {
|
|
196
|
-
|
|
197
|
+
logger.info('Processing conversations for response...');
|
|
197
198
|
|
|
198
199
|
let processedConversations = [];
|
|
199
200
|
try {
|
|
200
201
|
processedConversations = (conversations || []).map((conv, index) => {
|
|
201
202
|
try {
|
|
202
203
|
if (!conv || !conv.latestMessage) {
|
|
203
|
-
|
|
204
|
+
logger.warn(`Conversation ${index} missing latestMessage:`, conv?._id || 'unknown');
|
|
204
205
|
return {
|
|
205
206
|
phoneNumber: conv?._id || 'unknown',
|
|
206
207
|
name: 'Unknown',
|
|
@@ -245,7 +246,7 @@ const processConversations = async (conversations, nameMap, unreadMap) => {
|
|
|
245
246
|
lastMessageFromMe: conv?.latestMessage?.from_me || false
|
|
246
247
|
};
|
|
247
248
|
} catch (convError) {
|
|
248
|
-
|
|
249
|
+
logger.error(`Error processing conversation ${index}:`, convError);
|
|
249
250
|
return {
|
|
250
251
|
phoneNumber: conv?._id || `error_${index}`,
|
|
251
252
|
name: 'Error Processing',
|
|
@@ -260,11 +261,10 @@ const processConversations = async (conversations, nameMap, unreadMap) => {
|
|
|
260
261
|
}
|
|
261
262
|
});
|
|
262
263
|
|
|
263
|
-
|
|
264
|
+
logger.info(`Successfully processed ${processedConversations.length} conversations`);
|
|
264
265
|
|
|
265
266
|
} catch (mappingError) {
|
|
266
|
-
|
|
267
|
-
// Return empty conversations if mapping fails
|
|
267
|
+
logger.error('Error in conversation mapping:', mappingError);
|
|
268
268
|
processedConversations = [];
|
|
269
269
|
}
|
|
270
270
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { logger } = require('../utils/logger');
|
|
2
|
+
|
|
1
3
|
let preprocessingHandler = null;
|
|
2
4
|
|
|
3
5
|
const setPreprocessingHandler = (handler) => {
|
|
@@ -16,7 +18,7 @@ const invokePreprocessingHandler = async (payload) => {
|
|
|
16
18
|
const result = await preprocessingHandler(payload);
|
|
17
19
|
return Boolean(result);
|
|
18
20
|
} catch (error) {
|
|
19
|
-
|
|
21
|
+
logger.warn('[PreprocessingHooks] Handler threw an error:', error?.message || error);
|
|
20
22
|
return false;
|
|
21
23
|
}
|
|
22
24
|
};
|