@peopl-health/nexus 2.5.1 → 2.5.2-fix

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.
@@ -1,4 +1,5 @@
1
1
  const { airtable, getBase } = require('../config/airtableConfig');
2
+ const { Message } = require('../models/messageModel');
2
3
  const { addMsgAssistant, replyAssistant } = require('../services/assistantService');
3
4
  const { createProvider } = require('../adapters/registry');
4
5
  const runtimeConfig = require('../config/runtimeConfig');
@@ -42,7 +43,7 @@ class NexusMessaging {
42
43
  this.pendingResponses = new Map();
43
44
  this.batchingConfig = {
44
45
  enabled: config.messageBatching?.enabled ?? false,
45
- baseWaitTime: config.messageBatching?.baseWaitTime ?? 15000
46
+ baseWaitTime: config.messageBatching?.baseWaitTime ?? 10000
46
47
  };
47
48
  }
48
49
 
@@ -372,28 +373,11 @@ class NexusMessaging {
372
373
  } else if (messageData.flow) {
373
374
  return await this.handleFlow(messageData);
374
375
  } else {
375
- if (chatId && this.provider && typeof this.provider.sendTypingIndicator === 'function') {
376
- const messageId = messageData.id || messageData.MessageSid || messageData.message_id;
377
- if (messageId) {
378
- this.provider.sendTypingIndicator(messageId)
379
- .then(() => logger.debug('[processIncomingMessage] Typing indicator sent successfully', { messageId }))
380
- .catch(err => logger.warn('[processIncomingMessage] Typing indicator failed', {
381
- error: err.message,
382
- messageId
383
- }));
384
- } else {
385
- logger.warn('[processIncomingMessage] No message ID found for typing indicator', {
386
- messageData: JSON.stringify(messageData).substring(0, 200)
387
- });
388
- }
389
- }
390
-
391
376
  // For regular messages and media, use batching if enabled
392
377
  logger.info('Batching config:', this.batchingConfig);
393
378
  if (this.batchingConfig.enabled && chatId) {
394
379
  return await this._handleWithBatching(messageData, chatId);
395
380
  } else {
396
- // Handle media and regular messages without batching
397
381
  if (messageData.media) {
398
382
  return await this.handleMedia(messageData);
399
383
  } else {
@@ -650,14 +634,24 @@ class NexusMessaging {
650
634
  * Handle message with batching - waits for additional messages before processing
651
635
  */
652
636
  async _handleWithBatching(messageData, chatId) {
653
- if (this.pendingResponses.has(chatId)) {
654
- clearTimeout(this.pendingResponses.get(chatId));
637
+ const existing = this.pendingResponses.get(chatId);
638
+ if (existing) {
639
+ clearTimeout(existing.timeoutId);
640
+ if (existing.typingInterval) {
641
+ clearInterval(existing.typingInterval);
642
+ }
655
643
  logger.info(`Received additional message from ${chatId}, resetting wait timer`);
656
644
  }
657
645
 
646
+ // Start typing indicator refresh for batching period
647
+ const typingInterval = await this._startTypingRefresh(chatId);
648
+
658
649
  const waitTime = this.batchingConfig.baseWaitTime;
659
650
  const timeoutId = setTimeout(async () => {
660
651
  try {
652
+ if (typingInterval) {
653
+ clearInterval(typingInterval);
654
+ }
661
655
  this.pendingResponses.delete(chatId);
662
656
  await this._handleBatchedMessages(chatId);
663
657
  } catch (error) {
@@ -665,17 +659,44 @@ class NexusMessaging {
665
659
  }
666
660
  }, waitTime);
667
661
 
668
- this.pendingResponses.set(chatId, timeoutId);
662
+ this.pendingResponses.set(chatId, { timeoutId, typingInterval });
669
663
  logger.info(`Waiting ${Math.round(waitTime/1000)} seconds for more messages from ${chatId}`);
670
664
  }
671
665
 
666
+ /**
667
+ * Start typing indicator refresh interval
668
+ */
669
+ async _startTypingRefresh(chatId) {
670
+ if (!this.provider || typeof this.provider.sendTypingIndicator !== 'function') {
671
+ return null;
672
+ }
673
+
674
+ const lastMessage = await Message.findOne({
675
+ numero: chatId,
676
+ from_me: false,
677
+ message_id: { $exists: true, $ne: null }
678
+ }).sort({ createdAt: -1 });
679
+
680
+ if (!lastMessage?.message_id) return null;
681
+
682
+ return setInterval(() =>
683
+ this.provider.sendTypingIndicator(lastMessage.message_id).catch(err =>
684
+ logger.debug('[_startTypingRefresh] Failed', { error: err.message })
685
+ ), 5000
686
+ );
687
+ }
688
+
672
689
  /**
673
690
  * Process all batched messages for a chat
674
691
  */
675
692
  async _handleBatchedMessages(chatId) {
693
+ let typingInterval = null;
694
+
676
695
  try {
677
696
  logger.info(`Processing batched messages from ${chatId} (including media if any)`);
678
697
 
698
+ typingInterval = await this._startTypingRefresh(chatId);
699
+
679
700
  // Get assistant response
680
701
  const result = await replyAssistant(chatId);
681
702
  const botResponse = typeof result === 'string' ? result : result?.output;
@@ -696,6 +717,10 @@ class NexusMessaging {
696
717
 
697
718
  } catch (error) {
698
719
  logger.error('Error in batched message handling:', { error: error.message });
720
+ } finally {
721
+ if (typingInterval) {
722
+ clearInterval(typingInterval);
723
+ }
699
724
  }
700
725
  }
701
726
 
@@ -703,8 +728,14 @@ class NexusMessaging {
703
728
  * Clear pending response for a chat (useful for cleanup)
704
729
  */
705
730
  clearPendingResponse(chatId) {
706
- if (this.pendingResponses.has(chatId)) {
707
- clearTimeout(this.pendingResponses.get(chatId));
731
+ const pending = this.pendingResponses.get(chatId);
732
+ if (pending) {
733
+ if (pending.timeoutId) {
734
+ clearTimeout(pending.timeoutId);
735
+ }
736
+ if (pending.typingInterval) {
737
+ clearInterval(pending.typingInterval);
738
+ }
708
739
  this.pendingResponses.delete(chatId);
709
740
  }
710
741
  }
@@ -1,32 +1,18 @@
1
- const { Historial_Clinico_ID } = require('../config/airtableConfig.js');
2
- const AWS = require('../config/awsConfig.js');
1
+ const { withTracing } = require('../utils/tracingDecorator.js');
3
2
  const llmConfig = require('../config/llmConfig');
4
- const runtimeConfig = require('../config/runtimeConfig');
5
3
  const { BaseAssistant } = require('../assistants/BaseAssistant');
6
- const { createProvider } = require('../providers/createProvider');
7
-
8
- const { Message } = require('../models/messageModel.js');
9
- const { Thread } = require('../models/threadModel.js');
10
- const { PredictionMetrics } = require('../models/predictionMetricsModel');
11
- const { insertMessage } = require('../models/messageModel');
12
-
13
- const { getCurRow } = require('../helpers/assistantHelper.js');
14
- const { runAssistantAndWait, runAssistantWithRetries } = require('../helpers/assistantHelper.js');
15
- const { getThread, getThreadInfo } = require('../helpers/threadHelper.js');
16
- const { withTracing } = require('../utils/tracingDecorator.js');
17
- const { processThreadMessage } = require('../helpers/processHelper.js');
18
- const { getLastMessages, updateMessageRecord } = require('../helpers/messageHelper.js');
19
- const { withThreadRecovery } = require('../helpers/threadRecoveryHelper.js');
20
- const { combineImagesToPDF, cleanupFiles } = require('../helpers/filesHelper.js');
21
- const { logger } = require('../utils/logger');
22
-
23
- const DEFAULT_MAX_RETRIES = parseInt(process.env.MAX_RETRIES || '30', 10);
4
+ const {
5
+ createAssistantCore,
6
+ addMsgAssistantCore,
7
+ addInstructionCore,
8
+ replyAssistantCore,
9
+ switchAssistantCore
10
+ } = require('./assistantServiceCore');
24
11
 
25
12
  let assistantConfig = null;
26
13
  let assistantRegistry = {};
27
14
  let customGetAssistantById = null;
28
15
 
29
-
30
16
  const configureAssistants = (config) => {
31
17
  if (!config) {
32
18
  throw new Error('Assistant configuration is required');
@@ -99,224 +85,46 @@ const overrideGetAssistantById = (resolverFn) => {
99
85
 
100
86
  const getAssistantById = (assistant_id, thread) => {
101
87
  if (customGetAssistantById) {
102
- const inst = customGetAssistantById(assistant_id, thread);
103
- if (inst) return inst;
88
+ return customGetAssistantById(assistant_id, thread);
104
89
  }
105
90
 
106
- if (!assistantConfig) {
107
- assistantConfig = {};
91
+ if (assistantRegistry[assistant_id]) {
92
+ const AssistantClass = assistantRegistry[assistant_id];
93
+ return new AssistantClass({ thread });
108
94
  }
109
-
110
- const AssistantClass = assistantRegistry[assistant_id];
111
- if (!AssistantClass) {
112
- throw new Error(`Assistant '${assistant_id}' not found. Available assistants: ${Object.keys(assistantRegistry).join(', ')}`);
113
- }
114
-
115
- const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
116
- const sharedClient = provider?.getClient?.() || llmConfig.openaiClient || null;
117
95
 
118
- if (AssistantClass.prototype instanceof BaseAssistant) {
119
- return new AssistantClass({
96
+ if (assistantConfig && assistantConfig[assistant_id]) {
97
+ const config = assistantConfig[assistant_id];
98
+ return new BaseAssistant({
99
+ ...config,
120
100
  assistantId: assistant_id,
121
- thread,
122
- client: sharedClient,
123
- provider
124
- });
125
- }
126
-
127
- try {
128
- return new AssistantClass(thread);
129
- } catch (error) {
130
- return new AssistantClass({
131
- thread,
132
- assistantId: assistant_id,
133
- client: sharedClient,
134
- provider
135
- });
136
- }
137
- };
138
-
139
-
140
- const createAssistant = async (code, assistant_id, messages=[], force=false) => {
141
- const findThread = await Thread.findOne({ code: code });
142
- logger.info('[createAssistant] findThread', findThread);
143
- if (findThread && findThread.getConversationId() && !force) {
144
- logger.info('[createAssistant] Thread already exists');
145
- const updateFields = { active: true, stopped: false };
146
- Thread.setAssistantId(updateFields, assistant_id);
147
- await Thread.updateOne({ code: code }, { $set: updateFields });
148
- return findThread;
149
- }
150
-
151
- if (force && findThread?.getConversationId()) {
152
- const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
153
- await provider.deleteConversation(findThread.getConversationId());
154
- logger.info('[createAssistant] Deleted old conversation, will create new one');
155
- }
156
-
157
- const curRow = await getCurRow(Historial_Clinico_ID, code);
158
- logger.info('[createAssistant] curRow', curRow[0]);
159
- const nombre = curRow?.[0]?.['name'] || null;
160
- const patientId = curRow?.[0]?.['record_id'] || null;
161
-
162
- const assistant = getAssistantById(assistant_id, null);
163
- const initialThread = await assistant.create(code, curRow[0]);
164
-
165
- const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
166
- for (const message of messages) {
167
- await provider.addMessage({
168
- threadId: initialThread.id,
169
- role: 'assistant',
170
- content: message
101
+ thread
171
102
  });
172
103
  }
173
-
174
- const thread = {
175
- code: code,
176
- patient_id: patientId,
177
- nombre: nombre,
178
- active: true
179
- };
180
- Thread.setAssistantId(thread, assistant_id);
181
- Thread.setConversationId(thread, initialThread.id);
182
104
 
183
- const condition = { $or: [{ code: code }] };
184
- const options = { new: true, upsert: true };
185
- const updatedThread = await Thread.findOneAndUpdate(condition, {run_id: null, ...thread}, options);
186
- logger.info('[createAssistant] Updated thread:', updatedThread);
187
-
188
- return thread;
105
+ throw new Error(`Assistant with ID "${assistant_id}" not found`);
189
106
  };
190
107
 
191
- const addMsgAssistant = async (code, inMessages, role = 'user', reply = false, skipSystemMessage = false) => {
192
- try {
193
- let thread = await Thread.findOne({ code: code });
194
- logger.info(thread);
195
- if (thread === null) return null;
196
-
197
- const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
198
-
199
- await withThreadRecovery(
200
- async (recoveredThread = thread) => {
201
- thread = recoveredThread;
202
- for (const message of inMessages) {
203
- logger.info(message);
204
- await provider.addMessage({
205
- threadId: thread.getConversationId(),
206
- role: role,
207
- content: message
208
- });
209
-
210
- // Save system message to database for frontend visibility
211
- // Skip if message is already saved (e.g., from getConversationReplyController)
212
- if (!skipSystemMessage) {
213
- try {
214
- const message_id = `system_${Date.now()}_${Math.random().toString(36).substring(7)}`;
215
- await insertMessage({
216
- nombre_whatsapp: 'System',
217
- numero: code,
218
- body: message,
219
- timestamp: new Date(),
220
- message_id: message_id,
221
- is_group: false,
222
- is_media: false,
223
- from_me: true,
224
- processed: true,
225
- origin: 'system',
226
- thread_id: thread.getConversationId(),
227
- assistant_id: thread.getAssistantId(),
228
- raw: { role: role }
229
- });
230
- } catch (err) {
231
- // Don't throw - we don't want to break the flow if logging fails
232
- logger.error('[addMsgAssistant] Error saving system message:', err);
233
- }
234
- }
235
- }
236
- },
237
- thread,
238
- process.env.VARIANT || 'assistants'
239
- );
240
-
241
- if (!reply) return null;
242
-
243
- let output, completed;
244
- let retries = 0;
245
- const maxRetries = DEFAULT_MAX_RETRIES;
246
- const assistant = getAssistantById(thread.getAssistantId(), thread);
247
- do {
248
- ({ output, completed } = await runAssistantAndWait({ thread, assistant }));
249
- logger.info(`Attempt ${retries + 1}: completed=${completed}, output=${output || '(empty)'}`);
250
-
251
- if (completed && output) break;
252
- if (retries < maxRetries) await new Promise(resolve => setTimeout(resolve, 2000));
253
- retries++;
254
- } while (retries <= maxRetries && (!completed || !output));
255
-
256
- logger.info('THE ANS IS', output);
257
- return output;
258
- } catch (error) {
259
- logger.info(error);
260
- return null;
261
- }
262
- };
263
-
264
- const addInstructionCore = async (code, instruction, role = 'user') => {
265
- const thread = await withTracing(getThread, 'get_thread_operation',
266
- (threadCode) => ({
267
- 'thread.code': threadCode,
268
- 'operation.type': 'thread_retrieval'
269
- })
270
- )(code);
271
- if (thread === null) return null;
272
-
273
- const assistant = getAssistantById(thread.getAssistantId(), thread);
274
- const { output, completed, retries } = await withTracing(
275
- runAssistantWithRetries,
276
- 'run_assistant_with_retries',
277
- (thread, assistant, runConfig, patientReply) => ({
278
- 'assistant.id': thread.getAssistantId(),
279
- 'assistant.max_retries': DEFAULT_MAX_RETRIES,
280
- 'assistant.has_patient_reply': !!patientReply
281
- })
282
- )(
283
- thread,
284
- assistant,
285
- {
286
- additionalInstructions: instruction,
287
- additionalMessages: [
288
- { role: role, content: instruction }
289
- ]
290
- },
291
- null // no patientReply for instructions
292
- );
293
-
294
- // Save instruction to database for frontend visibility
295
- try {
296
- const message_id = `instruction_${Date.now()}_${Math.random().toString(36).substring(7)}`;
297
- await insertMessage({
298
- nombre_whatsapp: 'Instruction',
299
- numero: code,
300
- body: instruction,
301
- timestamp: new Date(),
302
- message_id: message_id,
303
- is_group: false,
304
- is_media: false,
305
- from_me: true,
306
- processed: true,
307
- origin: 'instruction',
308
- thread_id: thread.getConversationId(),
309
- assistant_id: thread.getAssistantId(),
310
- raw: { role: role }
311
- });
312
- } catch (err) {
313
- // Don't throw - we don't want to break the flow if logging fails
314
- logger.error('[addInstructionCore] Error saving instruction message:', err);
315
- }
108
+ const createAssistant = withTracing(
109
+ (code, assistant_id) => createAssistantCore(code, assistant_id, getAssistantById),
110
+ 'create_assistant',
111
+ (code, assistant_id) => ({
112
+ 'assistant.thread_code': code,
113
+ 'assistant.id': assistant_id,
114
+ 'operation.type': 'create_assistant'
115
+ })
116
+ );
316
117
 
317
- logger.info('RUN RESPONSE', output);
318
- return output;
319
- };
118
+ const addMsgAssistant = withTracing(
119
+ addMsgAssistantCore,
120
+ 'add_message_assistant',
121
+ (code, message, role) => ({
122
+ 'message.thread_code': code,
123
+ 'message.content_length': message?.length || 0,
124
+ 'message.role': role,
125
+ 'operation.type': 'add_message'
126
+ })
127
+ );
320
128
 
321
129
  const addInsAssistant = withTracing(
322
130
  addInstructionCore,
@@ -329,226 +137,28 @@ const addInsAssistant = withTracing(
329
137
  })
330
138
  );
331
139
 
332
- const startTypingIndicator = async (provider, code) => {
333
- try {
334
- const lastMessage = await Message.findOne({
335
- numero: code,
336
- from_me: false,
337
- message_id: { $exists: true, $ne: null }
338
- }).sort({ createdAt: -1 });
339
-
340
- if (!lastMessage?.message_id) return null;
341
-
342
- return setInterval(() =>
343
- provider.sendTypingIndicator(lastMessage.message_id).catch(err =>
344
- logger.debug('[startTypingIndicator] Interval failed', { error: err.message })
345
- ), 25000
346
- );
347
- } catch (err) {
348
- logger.debug('[startTypingIndicator] Failed to start', { error: err.message });
349
- return null;
350
- }
351
- };
352
-
353
- const replyAssistantCore = async (code, message_ = null, thread_ = null, runOptions = {}) => {
354
- const timings = {};
355
- const startTotal = Date.now();
356
-
357
- const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
358
- const typingInterval = await startTypingIndicator(provider, code);
359
-
360
- try {
361
- const { result: thread, duration: getThreadMs } = await withTracing(
362
- getThread,
363
- 'get_thread_operation',
364
- (threadCode) => ({
365
- 'thread.code': threadCode,
366
- 'operation.type': 'thread_retrieval',
367
- 'thread.provided': !!thread_
368
- }),
369
- { returnTiming: true }
370
- )(code);
371
- timings.get_thread_ms = getThreadMs;
372
-
373
- if (!thread_ && !thread) return null;
374
- const finalThread = thread_ || thread;
375
-
376
- const { result: patientReply, duration: getMessagesMs } = await withTracing(
377
- getLastMessages,
378
- 'get_last_messages',
379
- (code) => ({ 'thread.code': code }),
380
- { returnTiming: true }
381
- )(code);
382
- timings.get_messages_ms = getMessagesMs;
383
-
384
- if (!patientReply) {
385
- logger.info('[replyAssistantCore] No relevant data found for this assistant.');
386
- return null;
387
- }
388
-
389
- logger.info(`[replyAssistantCore] Processing ${patientReply.length} messages in parallel`);
390
- const { result: processResult, duration: processMessagesMs } = await withTracing(
391
- processThreadMessage,
392
- 'process_thread_messages',
393
- (code, patientReply, provider) => ({
394
- 'messages.count': patientReply.length,
395
- 'thread.code': code
396
- }),
397
- { returnTiming: true }
398
- )(code, patientReply, provider);
399
-
400
- const { results: processResults, timings: processTimings } = processResult;
401
- timings.process_messages_ms = processMessagesMs;
402
-
403
- logger.debug('[replyAssistantCore] Process timings breakdown', { processTimings });
404
-
405
- if (processTimings) {
406
- timings.process_messages_breakdown = {
407
- download_ms: processTimings.download_ms || 0,
408
- image_analysis_ms: processTimings.image_analysis_ms || 0,
409
- audio_transcription_ms: processTimings.audio_transcription_ms || 0,
410
- url_generation_ms: processTimings.url_generation_ms || 0,
411
- total_media_ms: processTimings.total_media_ms || 0
412
- };
413
- }
414
-
415
- const patientMsg = processResults.some(r => r.isPatient);
416
- const urls = processResults.filter(r => r.url).map(r => ({ url: r.url }));
417
- const allMessagesToAdd = processResults.flatMap(r => r.messages || []);
418
- const allTempFiles = processResults.flatMap(r => r.tempFiles || []);
419
-
420
- if (allMessagesToAdd.length > 0) {
421
- logger.info(`[replyAssistantCore] Adding ${allMessagesToAdd.length} messages to thread in batch`);
422
- await withThreadRecovery(
423
- async (thread = finalThread) => {
424
- const threadId = thread.getConversationId();
425
- await provider.addMessage({ threadId, messages: allMessagesToAdd });
426
- },
427
- finalThread,
428
- process.env.VARIANT || 'assistants'
429
- );
430
- }
431
-
432
- await Promise.all(processResults.map(r => updateMessageRecord(r.reply, finalThread)));
433
- await cleanupFiles(allTempFiles);
434
-
435
- if (urls.length > 0) {
436
- logger.info(`[replyAssistantCore] Processing ${urls.length} URLs for PDF combination`);
437
- const { result: pdfResult, duration: pdfCombinationMs } = await withTracing(
438
- combineImagesToPDF,
439
- 'combine_images_to_pdf',
440
- ({ code }) => ({
441
- 'pdf.thread_code': code,
442
- 'pdf.url_count': urls.length
443
- }),
444
- { returnTiming: true }
445
- )({ code });
446
- timings.pdf_combination_ms = pdfCombinationMs;
447
- const { pdfBuffer, processedFiles } = pdfResult;
448
- logger.info(`[replyAssistantCore] PDF combination complete: ${processedFiles?.length || 0} files processed`);
449
-
450
- if (pdfBuffer) {
451
- const key = `${code}-${Date.now()}-combined.pdf`;
452
- const bucket = runtimeConfig.get('AWS_S3_BUCKET_NAME');
453
- if (bucket) {
454
- await AWS.uploadBufferToS3(pdfBuffer, bucket, key, 'application/pdf');
455
- }
456
- }
457
-
458
- if (processedFiles && processedFiles.length) {
459
- cleanupFiles(processedFiles);
460
- }
461
- }
462
-
463
- if (!patientMsg || finalThread.stopped) return null;
464
-
465
- const assistant = getAssistantById(finalThread.getAssistantId(), finalThread);
466
- const { result: runResult, duration: runAssistantMs } = await withTracing(
467
- runAssistantWithRetries,
468
- 'run_assistant_with_retries',
469
- (thread, assistant, runConfig, patientReply) => ({
470
- 'assistant.id': thread.getAssistantId(),
471
- 'assistant.max_retries': DEFAULT_MAX_RETRIES,
472
- 'assistant.has_patient_reply': !!patientReply
473
- }),
474
- { returnTiming: true }
475
- )(finalThread, assistant, runOptions, patientReply);
476
- timings.run_assistant_ms = runAssistantMs;
477
- timings.total_ms = Date.now() - startTotal;
478
-
479
- const { run, output, completed, retries, predictionTimeMs, tools_executed } = runResult;
480
-
481
- logger.info('[Assistant Reply Complete]', {
482
- code: code ? `${code.substring(0, 3)}***${code.slice(-4)}` : 'unknown',
483
- messageCount: patientReply.length,
484
- hasMedia: urls.length > 0,
485
- retries,
486
- totalMs: timings.total_ms,
487
- toolsExecuted: tools_executed?.length || 0
488
- });
489
-
490
- if (output && predictionTimeMs) {
491
- logger.debug('[replyAssistantCore] Storing metrics with timing_breakdown', {
492
- timing_breakdown: timings,
493
- has_breakdown: !!timings.process_messages_breakdown
494
- });
495
-
496
- await PredictionMetrics.create({
497
- message_id: `${code}-${Date.now()}`,
498
- numero: code,
499
- assistant_id: finalThread.getAssistantId(),
500
- thread_id: finalThread.getConversationId(),
501
- prediction_time_ms: predictionTimeMs,
502
- retry_count: retries,
503
- completed: completed,
504
- timing_breakdown: timings
505
- }).catch(err => logger.error('[replyAssistantCore] Failed to store metrics:', err));
506
- }
507
-
508
- return { output, tools_executed };
509
- } finally {
510
- if (typingInterval) {
511
- clearInterval(typingInterval);
512
- }
513
- }
514
- };
515
-
516
140
  const replyAssistant = withTracing(
517
- replyAssistantCore,
141
+ (code, message_, thread_, runOptions) => replyAssistantCore(code, message_, thread_, runOptions, getAssistantById),
518
142
  'assistant_reply',
519
143
  (code, message_, thread_, runOptions) => ({
520
144
  'assistant.thread_code': code,
521
145
  'assistant.has_message': !!message_,
522
146
  'assistant.has_custom_thread': !!thread_,
523
- 'assistant.run_options': JSON.stringify(runOptions)
147
+ 'assistant.has_run_options': !!runOptions && Object.keys(runOptions).length > 0
524
148
  })
525
149
  );
526
150
 
527
- const switchAssistant = async (code, assistant_id) => {
528
- try {
529
- const thread = await Thread.findOne({ code: code });
530
- logger.info('Inside thread', thread);
531
- if (thread === null) return;
532
-
533
- const variant = process.env.VARIANT || 'assistants';
534
- const updateFields = { active: true, stopped: false };
535
-
536
- if (variant === 'responses') {
537
- updateFields.prompt_id = assistant_id;
538
- } else {
539
- updateFields.assistant_id = assistant_id;
540
- }
541
-
542
- await Thread.updateOne({ code }, { $set: updateFields });
543
- } catch (error) {
544
- logger.info(error);
545
- return null;
546
- }
547
- };
151
+ const switchAssistant = withTracing(
152
+ switchAssistantCore,
153
+ 'switch_assistant',
154
+ (code, assistant_id) => ({
155
+ 'assistant.thread_code': code,
156
+ 'assistant.new_id': assistant_id,
157
+ 'operation.type': 'switch_assistant'
158
+ })
159
+ );
548
160
 
549
161
  module.exports = {
550
- getThread,
551
- getThreadInfo,
552
162
  getAssistantById,
553
163
  createAssistant,
554
164
  replyAssistant,
@@ -557,6 +167,5 @@ module.exports = {
557
167
  switchAssistant,
558
168
  configureAssistants,
559
169
  registerAssistant,
560
- overrideGetAssistantById,
561
- runAssistantAndWait
170
+ overrideGetAssistantById
562
171
  };
@@ -0,0 +1,215 @@
1
+ const AWS = require('../config/awsConfig.js');
2
+ const runtimeConfig = require('../config/runtimeConfig');
3
+ const { createProvider } = require('../providers/createProvider');
4
+
5
+ const { Thread } = require('../models/threadModel.js');
6
+ const { PredictionMetrics } = require('../models/predictionMetricsModel');
7
+ const { insertMessage } = require('../models/messageModel');
8
+
9
+ const { getCurRow, runAssistantWithRetries } = require('../helpers/assistantHelper.js');
10
+ const { getThread } = require('../helpers/threadHelper.js');
11
+ const { processThreadMessage } = require('../helpers/processHelper.js');
12
+ const { getLastMessages, updateMessageRecord } = require('../helpers/messageHelper.js');
13
+ const { withThreadRecovery } = require('../helpers/threadRecoveryHelper.js');
14
+ const { combineImagesToPDF, cleanupFiles } = require('../helpers/filesHelper.js');
15
+ const { logger } = require('../utils/logger');
16
+
17
+ const createAssistantCore = async (code, assistant_id, getAssistantById) => {
18
+ const thread = await getThread(code);
19
+ if (!thread) return null;
20
+
21
+ const assistant = getAssistantById(assistant_id, thread);
22
+ const curRow = await getCurRow(code);
23
+ const context = { curRow };
24
+
25
+ try {
26
+ await assistant.create(code, context);
27
+ return { success: true, assistant_id };
28
+ } catch (error) {
29
+ logger.error('[createAssistantCore] Error:', error);
30
+ return { success: false, error: error.message };
31
+ }
32
+ };
33
+
34
+ const addMsgAssistantCore = async (code, message, role = 'user') => {
35
+ const thread = await getThread(code);
36
+ if (!thread) return null;
37
+
38
+ const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
39
+ const threadId = thread.getConversationId();
40
+
41
+ try {
42
+ await provider.addMessage({ threadId, messages: [{ role, content: message }] });
43
+ await insertMessage({ code, message, role });
44
+ return { success: true };
45
+ } catch (error) {
46
+ logger.error('[addMsgAssistantCore] Error:', error);
47
+ return { success: false, error: error.message };
48
+ }
49
+ };
50
+
51
+ const addInstructionCore = async (code, instruction, role = 'user') => {
52
+ const thread = await getThread(code);
53
+ if (!thread) return null;
54
+
55
+ const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
56
+ const threadId = thread.getConversationId();
57
+
58
+ try {
59
+ await provider.addMessage({ threadId, messages: [{ role, content: instruction }] });
60
+ return { success: true };
61
+ } catch (error) {
62
+ logger.error('[addInstructionCore] Error:', error);
63
+ return { success: false, error: error.message };
64
+ }
65
+ };
66
+
67
+ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOptions = {}, getAssistantById) => {
68
+ const timings = {};
69
+ const startTotal = Date.now();
70
+
71
+ try {
72
+ const thread = thread_ || await getThread(code);
73
+ timings.get_thread_ms = 0;
74
+
75
+ if (!thread) return null;
76
+ const finalThread = thread;
77
+
78
+ const patientReply = await getLastMessages(code);
79
+ timings.get_messages_ms = 0;
80
+
81
+ if (!patientReply) {
82
+ logger.info('[replyAssistantCore] No relevant data found for this assistant.');
83
+ return null;
84
+ }
85
+
86
+ const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
87
+ logger.info(`[replyAssistantCore] Processing ${patientReply.length} messages in parallel`);
88
+ const processResult = await processThreadMessage(code, patientReply, provider);
89
+
90
+ const { results: processResults, timings: processTimings } = processResult;
91
+ timings.process_messages_ms = 0;
92
+
93
+ logger.debug('[replyAssistantCore] Process timings breakdown', { processTimings });
94
+
95
+ if (processTimings) {
96
+ timings.process_messages_breakdown = {
97
+ download_ms: processTimings.download_ms || 0,
98
+ image_analysis_ms: processTimings.image_analysis_ms || 0,
99
+ audio_transcription_ms: processTimings.audio_transcription_ms || 0,
100
+ url_generation_ms: processTimings.url_generation_ms || 0,
101
+ total_media_ms: processTimings.total_media_ms || 0
102
+ };
103
+ }
104
+
105
+ const patientMsg = processResults.some(r => r.isPatient);
106
+ const urls = processResults.filter(r => r.url).map(r => ({ url: r.url }));
107
+ const allMessagesToAdd = processResults.flatMap(r => r.messages || []);
108
+ const allTempFiles = processResults.flatMap(r => r.tempFiles || []);
109
+
110
+ if (allMessagesToAdd.length > 0) {
111
+ logger.info(`[replyAssistantCore] Adding ${allMessagesToAdd.length} messages to thread in batch`);
112
+ await withThreadRecovery(
113
+ async (thread = finalThread) => {
114
+ const threadId = thread.getConversationId();
115
+ await provider.addMessage({ threadId, messages: allMessagesToAdd });
116
+ },
117
+ finalThread,
118
+ process.env.VARIANT || 'assistants'
119
+ );
120
+ }
121
+
122
+ await Promise.all(processResults.map(r => updateMessageRecord(r.reply, finalThread)));
123
+ await cleanupFiles(allTempFiles);
124
+
125
+ if (urls.length > 0) {
126
+ logger.info(`[replyAssistantCore] Processing ${urls.length} URLs for PDF combination`);
127
+ const pdfResult = await combineImagesToPDF({ code });
128
+ timings.pdf_combination_ms = 0;
129
+ const { pdfBuffer, processedFiles } = pdfResult;
130
+ logger.info(`[replyAssistantCore] PDF combination complete: ${processedFiles?.length || 0} files processed`);
131
+
132
+ if (pdfBuffer) {
133
+ const key = `${code}-${Date.now()}-combined.pdf`;
134
+ const bucket = runtimeConfig.get('AWS_S3_BUCKET_NAME');
135
+ if (bucket) {
136
+ await AWS.uploadBufferToS3(pdfBuffer, bucket, key, 'application/pdf');
137
+ }
138
+ }
139
+
140
+ if (processedFiles && processedFiles.length) {
141
+ cleanupFiles(processedFiles);
142
+ }
143
+ }
144
+
145
+ if (!patientMsg || finalThread.stopped) return null;
146
+
147
+ const assistant = getAssistantById(finalThread.getAssistantId(), finalThread);
148
+ const runResult = await runAssistantWithRetries(finalThread, assistant, runOptions, patientReply);
149
+ timings.run_assistant_ms = 0;
150
+ timings.total_ms = Date.now() - startTotal;
151
+
152
+ const { run, output, completed, retries, predictionTimeMs, tools_executed } = runResult;
153
+
154
+ logger.info('[Assistant Reply Complete]', {
155
+ code: code ? `${code.substring(0, 3)}***${code.slice(-4)}` : 'unknown',
156
+ messageCount: patientReply.length,
157
+ hasMedia: urls.length > 0,
158
+ retries,
159
+ totalMs: timings.total_ms,
160
+ toolsExecuted: tools_executed?.length || 0
161
+ });
162
+
163
+ if (output && predictionTimeMs) {
164
+ logger.debug('[replyAssistantCore] Storing metrics with timing_breakdown', {
165
+ timing_breakdown: timings,
166
+ has_breakdown: !!timings.process_messages_breakdown
167
+ });
168
+
169
+ await PredictionMetrics.create({
170
+ message_id: `${code}-${Date.now()}`,
171
+ numero: code,
172
+ assistant_id: finalThread.getAssistantId(),
173
+ thread_id: finalThread.getConversationId(),
174
+ prediction_time_ms: predictionTimeMs,
175
+ retry_count: retries,
176
+ completed: completed,
177
+ timing_breakdown: timings
178
+ }).catch(err => logger.error('[replyAssistantCore] Failed to store metrics:', err));
179
+ }
180
+
181
+ return { output, tools_executed };
182
+ } catch (error) {
183
+ logger.error('[replyAssistantCore] Error:', { error: error.message });
184
+ throw error;
185
+ }
186
+ };
187
+
188
+ const switchAssistantCore = async (code, assistant_id) => {
189
+ try {
190
+ const thread = await Thread.findOne({ code });
191
+ if (!thread) {
192
+ return null;
193
+ }
194
+
195
+ const updateFields = {
196
+ assistant_id,
197
+ stopped: false,
198
+ updatedAt: new Date()
199
+ };
200
+
201
+ await Thread.updateOne({ code }, { $set: updateFields });
202
+ return { success: true, assistant_id };
203
+ } catch (error) {
204
+ logger.info(error);
205
+ return null;
206
+ }
207
+ };
208
+
209
+ module.exports = {
210
+ createAssistantCore,
211
+ addMsgAssistantCore,
212
+ addInstructionCore,
213
+ replyAssistantCore,
214
+ switchAssistantCore
215
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "2.5.1",
3
+ "version": "2.5.2-fix",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",