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