@peopl-health/nexus 3.13.1 → 3.13.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,9 +5,9 @@ const { logger } = require('../utils/logger');
5
5
  const { getThreadInfo, switchThreadStoppedStatus } = require('../helpers/threadHelper');
6
6
 
7
7
  const { getRecordByFilter } = require('../services/airtableService');
8
- const { createAssistant, addMsgAssistant, addInsAssistant, switchAssistant } = require('../services/assistantService');
8
+ const { createAssistant, addMsgAssistant, switchAssistant } = require('../services/assistantService');
9
9
 
10
- const { sendMessage } = require('../core/NexusMessaging');
10
+ const { sendMessage, processInstruction, processSystemMessage } = require('../core/NexusMessaging');
11
11
 
12
12
  const _updateThreadFlag = async (req, res, field, successMsg, errorMsg) => {
13
13
  const { code } = req.body;
@@ -36,8 +36,7 @@ const addInsAssistantController = async (req, res) => {
36
36
  if (!code) return res.status(400).json({ success: false, error: 'Code is required' });
37
37
 
38
38
  try {
39
- const reply = await addInsAssistant(code, instruction, 'developer');
40
- if (reply) await sendMessage({ code, body: reply, fileType: 'text', origin: 'assistant' });
39
+ await processInstruction(code, instruction, 'developer');
41
40
  return res.status(200).json({ success: true, message: 'Instruction added to assistant' });
42
41
  } catch (error) {
43
42
  logger.error('[AssistantController] Add instruction error', { error: error.message, code });
@@ -50,8 +49,11 @@ const addMsgAssistantController = async (req, res) => {
50
49
  if (!code) return res.status(400).json({ success: false, error: 'Code is required' });
51
50
 
52
51
  try {
53
- const assistantReply = await addMsgAssistant(code, messages, role, reply);
54
- if (assistantReply) await sendMessage({ code, body: assistantReply, fileType: 'text', origin: 'assistant' });
52
+ if (reply) {
53
+ await processSystemMessage(code, messages, role);
54
+ } else {
55
+ await addMsgAssistant(code, messages, role);
56
+ }
55
57
  return res.status(200).json({ success: true, message: 'Message added to assistant' });
56
58
  } catch (error) {
57
59
  logger.error('[AssistantController] Add message error', { error: error.message, code, role });
@@ -1,30 +1,71 @@
1
- /**
2
- * Handles assistant message processing with local or queue modes.
3
- */
1
+ const { sanitizeOutput } = require('../utils/formatUtils');
2
+ const { getThread } = require('../helpers/threadHelper');
3
+ const { runAssistantWithRetries } = require('../helpers/assistantHelper');
4
+ const { getAssistantById } = require('../services/assistantResolver');
5
+
4
6
  class AssistantProcessor {
5
- constructor({ mode = 'local', queueAdapter = null, sendMessage = null, replyAssistant = null }) {
6
- Object.assign(this, { mode, queueAdapter, sendMessage, replyAssistant });
7
+ constructor({ mode = 'local', queueAdapter = null, sendMessage = null, preProcessMessages = null, storeRunMetrics = null }) {
8
+ Object.assign(this, { mode, queueAdapter, sendMessage, preProcessMessages, storeRunMetrics });
7
9
  if (mode === 'queue' && queueAdapter) {
8
- queueAdapter.process('assistant.process', (payload) => this._processLocal(payload));
10
+ queueAdapter.process('assistant.process', (payload) => this._processViaLocal(payload));
9
11
  }
10
12
  }
11
13
 
12
- setReplyAssistant(fn) { this.replyAssistant = fn; }
13
14
  setSendMessage(fn) { this.sendMessage = fn; }
14
15
 
16
+ async resolveThread(code) {
17
+ const thread = await getThread(code);
18
+ if (!thread) return null;
19
+ const assistant = getAssistantById(thread.getAssistantId(), thread);
20
+ return { thread, assistant };
21
+ }
22
+
23
+ async executeLLM(thread, assistant, runOptions = {}, messages = null) {
24
+ const startTime = Date.now();
25
+ const runResult = await runAssistantWithRetries(thread, assistant, runOptions, messages);
26
+ const predictionTimeMs = Date.now() - startTime;
27
+
28
+ const output = sanitizeOutput(runResult?.output);
29
+ const run = runResult?.run;
30
+
31
+ return {
32
+ output,
33
+ tools_executed: runResult?.tools_executed,
34
+ prompt: run?.prompt || null,
35
+ preset: run?.preset || null,
36
+ response_id: run?.id || null,
37
+ run,
38
+ predictionTimeMs,
39
+ retries: runResult?.retries || 0,
40
+ completed: runResult?.completed,
41
+ };
42
+ }
43
+
15
44
  async process({ code, body = null, runOptions = {} }) {
16
45
  if (!code) throw new Error('code is required for assistant processing');
17
46
 
18
- const result = (this.mode === 'queue')
47
+ return (this.mode === 'queue')
19
48
  ? await this._processViaQueue({ code, body, runOptions })
20
- : await this._processLocal({ code, body, runOptions });
21
-
22
- return result;
49
+ : await this._processViaLocal({ code, body, runOptions });
23
50
  }
24
51
 
25
- async _processLocal({ code, body = null, runOptions = {} }) {
26
- if (!this.replyAssistant) throw new Error('replyAssistant function not configured');
27
- return await this.replyAssistant(code, body, null, runOptions);
52
+ async _processViaLocal({ code, body = null, runOptions = {} }) {
53
+ const resolved = await this.resolveThread(code);
54
+ if (!resolved) return null;
55
+ const { thread, assistant } = resolved;
56
+
57
+ if (this.preProcessMessages) {
58
+ const preProcessed = await this.preProcessMessages(code, body, thread);
59
+ if (!preProcessed.shouldProcess) return null;
60
+
61
+ const result = await this.executeLLM(thread, assistant, runOptions, preProcessed.messages);
62
+ if (this.storeRunMetrics) await this.storeRunMetrics(code, thread, result, preProcessed.timings);
63
+ return { ...result, timings: preProcessed.timings };
64
+ }
65
+
66
+ const result = await this.executeLLM(thread, assistant, runOptions);
67
+ if (this.storeRunMetrics) await this.storeRunMetrics(code, thread, result);
68
+ return result;
28
69
  }
29
70
 
30
71
  async _processViaQueue({ code, body, runOptions }) {
@@ -33,13 +74,23 @@ class AssistantProcessor {
33
74
  return await this.queueAdapter.waitForResult(jobId, 120000);
34
75
  }
35
76
 
77
+ async processDirect({ code, runOptions = {} }) {
78
+ if (!code) throw new Error('code is required for direct processing');
79
+
80
+ const resolved = await this.resolveThread(code);
81
+ if (!resolved) return null;
82
+
83
+ const result = await this.executeLLM(resolved.thread, resolved.assistant, runOptions);
84
+ if (this.storeRunMetrics) await this.storeRunMetrics(code, resolved.thread, result);
85
+ return result;
86
+ }
87
+
36
88
  async sendResponse(code, result) {
37
89
  if (!this.sendMessage) throw new Error('sendMessage function not configured');
38
90
  if (!result?.output) return null;
39
91
  await this.sendMessage({ code, body: result.output, processed: true, origin: 'assistant', tools_executed: result.tools_executed, prompt: result.prompt, preset: result.preset, response_id: result.response_id });
40
92
  return result.output;
41
93
  }
42
-
43
94
  }
44
95
 
45
96
  module.exports = { AssistantProcessor };
@@ -6,15 +6,16 @@ const { connect } = require('../config/mongoConfig');
6
6
 
7
7
  const { logger } = require('../utils/logger');
8
8
 
9
- const { Message } = require('../models/messageModel');
9
+ const { Message, insertMessage } = require('../models/messageModel');
10
10
  const { Thread } = require('../models/threadModel');
11
11
 
12
12
  const { setEventBus: setStatusEventBus } = require('../helpers/messageStatusHelper');
13
13
  const { ensureThreadExists } = require('../helpers/threadHelper');
14
+ const { storeRunMetrics } = require('../helpers/metricsHelper');
14
15
 
15
16
  const { createMessagingProvider } = require('../adapters/registry');
16
17
 
17
- const { addMsgAssistant, replyAssistant } = require('../services/assistantService');
18
+ const { addMsgAssistant, preProcessMessages } = require('../services/assistantService');
18
19
  const { hasPreprocessingHandler, invokePreprocessingHandler } = require('../services/preprocessingService');
19
20
 
20
21
  const { BatchingManager } = require('../core/BatchingManager');
@@ -76,7 +77,8 @@ class NexusMessaging {
76
77
  mode: config.assistant?.mode || 'local',
77
78
  queueAdapter: this.queueAdapter,
78
79
  sendMessage: this.sendMessage.bind(this),
79
- replyAssistant
80
+ preProcessMessages,
81
+ storeRunMetrics,
80
82
  });
81
83
  }
82
84
 
@@ -350,29 +352,112 @@ class NexusMessaging {
350
352
  }
351
353
 
352
354
  /*
353
- * MESSAGE BATCHING
355
+ * PROCESSING PIPELINE
354
356
  */
355
357
 
356
- async _handleWithCheckAfter(chatId) {
358
+ async _executeWithPipeline(chatId, type, mode, executeFn) {
359
+ let capturedResult = null;
360
+
357
361
  const processingFn = async (runId) => {
358
362
  const shouldContinue = () => this.batchingManager.isActiveRun(chatId, runId);
359
-
360
- return await this.pipeline.run(
361
- { chatId, runId, type: 'message' },
362
- async (preProcessResult) => {
363
- return await this._processMessages(chatId, () =>
364
- this.assistantProcessor.process({ code: chatId, runOptions: { runId, prePromptResult: preProcessResult } })
365
- , shouldContinue);
366
- },
363
+ capturedResult = await this.pipeline.run(
364
+ { chatId, runId, type },
365
+ executeFn,
367
366
  shouldContinue
368
367
  );
368
+ return capturedResult;
369
369
  };
370
370
 
371
371
  const sendResponseFn = async (result) => {
372
372
  await this.assistantProcessor.sendResponse(chatId, result);
373
373
  };
374
374
 
375
- await this.batchingManager.handleBatchedProcessing(chatId, processingFn, sendResponseFn);
375
+ if (mode === 'queue') {
376
+ await this.batchingManager.enqueueProcessing(chatId, processingFn, sendResponseFn);
377
+ } else {
378
+ await this.batchingManager.handleBatchedProcessing(chatId, processingFn, sendResponseFn);
379
+ }
380
+
381
+ return capturedResult;
382
+ }
383
+
384
+ async _handleWithCheckAfter(chatId) {
385
+ await this._executeWithPipeline(chatId, 'message', 'preempt',
386
+ async (preProcessResult, shouldContinue) => {
387
+ return await this._processMessages(chatId, () =>
388
+ this.assistantProcessor.process({ code: chatId, runOptions: { prePromptResult: preProcessResult } })
389
+ , shouldContinue);
390
+ }
391
+ );
392
+ }
393
+
394
+ async processInstruction(code, instruction, role = 'developer') {
395
+ const assistantId = await this._getThreadAssistantId(code);
396
+ await insertMessage({
397
+ nombre_whatsapp: 'Instruction',
398
+ numero: code,
399
+ body: instruction,
400
+ message_id: `instruction_${Date.now()}_${Math.random().toString(36).substring(7)}`,
401
+ from_me: true,
402
+ processed: true,
403
+ origin: 'instruction',
404
+ assistant_id: assistantId,
405
+ raw: { role }
406
+ });
407
+
408
+ const result = await this._executeWithPipeline(code, 'instruction', 'queue',
409
+ async (preProcessResult) => {
410
+ return await this.assistantProcessor.processDirect({
411
+ code,
412
+ runOptions: {
413
+ prePromptResult: preProcessResult,
414
+ additionalInstructions: instruction,
415
+ additionalMessages: [{ role, content: instruction }],
416
+ toolChoice: 'none',
417
+ }
418
+ });
419
+ }
420
+ );
421
+
422
+ return result?.output || null;
423
+ }
424
+
425
+ async processSystemMessage(code, messages, role = 'system') {
426
+ const normalizedMessages = Array.isArray(messages) ? messages : [messages];
427
+ const assistantId = await this._getThreadAssistantId(code);
428
+
429
+ for (let i = 0; i < normalizedMessages.length; i++) {
430
+ await insertMessage({
431
+ nombre_whatsapp: 'System',
432
+ numero: code,
433
+ body: normalizedMessages[i],
434
+ message_id: `system_${Date.now()}_${i}_${Math.random().toString(36).substring(7)}`,
435
+ from_me: true,
436
+ processed: true,
437
+ origin: 'system',
438
+ assistant_id: assistantId,
439
+ raw: { role }
440
+ });
441
+ }
442
+
443
+ const result = await this._executeWithPipeline(code, 'system', 'queue',
444
+ async (preProcessResult) => {
445
+ return await this.assistantProcessor.processDirect({
446
+ code,
447
+ runOptions: {
448
+ prePromptResult: preProcessResult,
449
+ toolChoice: 'none',
450
+ }
451
+ });
452
+ }
453
+ );
454
+
455
+ return result?.output || null;
456
+ }
457
+
458
+ async _getThreadAssistantId(code) {
459
+ const thread = await Thread.findOne({ code }).select('assistant_id prompt_id').lean();
460
+ return thread?.prompt_id || thread?.assistant_id || null;
376
461
  }
377
462
 
378
463
  async _processMessages(chatId, processingFn, shouldFinalize = () => true) {
@@ -450,6 +535,14 @@ const sendScheduledMessage = async (scheduledMessage) => {
450
535
  return await requireDefaultInstance().sendScheduledMessage(scheduledMessage);
451
536
  };
452
537
 
538
+ const processInstruction = async (code, instruction, role) => {
539
+ return await requireDefaultInstance().processInstruction(code, instruction, role);
540
+ };
541
+
542
+ const processSystemMessage = async (code, messages, role) => {
543
+ return await requireDefaultInstance().processSystemMessage(code, messages, role);
544
+ };
545
+
453
546
  const getEventBus = () => getDefaultInstance()?.getEventBus();
454
547
 
455
548
  const _resetDefaultInstance = () => { defaultInstance = null; };
@@ -458,8 +551,11 @@ module.exports = {
458
551
  NexusMessaging,
459
552
  sendMessage,
460
553
  sendScheduledMessage,
554
+ processInstruction,
555
+ processSystemMessage,
461
556
  setDefaultInstance,
462
557
  getDefaultInstance,
558
+ requireDefaultInstance,
463
559
  getProvider,
464
560
  requireProvider,
465
561
  getEventBus,
@@ -0,0 +1,62 @@
1
+ const { logger } = require('../utils/logger');
2
+ const { getPredictionMetrics } = require('../models/predictionMetricsModel');
3
+
4
+ const storeRunMetrics = async (code, thread, result, timings = {}) => {
5
+ const { output, run, predictionTimeMs, retries, completed } = result;
6
+ if (!output || !predictionTimeMs) return;
7
+
8
+ const usage = run?.usage || null;
9
+ const model = run?.model || null;
10
+
11
+ logger.info('[Assistant Reply Complete]', {
12
+ code: code ? `${code.substring(0, 3)}***${code.slice(-4)}` : 'unknown',
13
+ retries,
14
+ totalMs: timings.total_ms,
15
+ toolsExecuted: result.tools_executed?.length || 0,
16
+ token_usage: usage ? {
17
+ input_tokens: usage.input_tokens,
18
+ output_tokens: usage.output_tokens,
19
+ total_tokens: usage.total_tokens,
20
+ model,
21
+ } : undefined,
22
+ });
23
+
24
+ const tokenUsage = usage ? {
25
+ input_tokens: usage.input_tokens || 0,
26
+ output_tokens: usage.output_tokens || 0,
27
+ total_tokens: usage.total_tokens || 0,
28
+ model: model || undefined,
29
+ } : undefined;
30
+
31
+ await getPredictionMetrics().create({
32
+ message_id: `${code}-${Date.now()}`,
33
+ numero: code,
34
+ assistant_id: thread.getAssistantId(),
35
+ prediction_time_ms: predictionTimeMs,
36
+ retry_count: retries,
37
+ completed,
38
+ timing_breakdown: timings,
39
+ token_usage: tokenUsage,
40
+ prompt_config: run?.prompt || null,
41
+ response_id: run?.id || null,
42
+ resolved_prompt: run?.resolved_prompt || null,
43
+ snippet_ids: run?.snippet_ids || [],
44
+ tool_ids: run?.tool_ids || [],
45
+ preset_id: run?.preset_id || null,
46
+ preset_version: run?.preset_version || null,
47
+ preset: run?.preset || null,
48
+ }).catch(err => logger.error('[storeRunMetrics] Failed to store metrics', { error: err.message }));
49
+
50
+ const alertThreshold = parseInt(process.env.TOKEN_ALERT_THRESHOLD, 10);
51
+ if (alertThreshold && usage?.total_tokens > alertThreshold) {
52
+ logger.warn('[storeRunMetrics] Token usage spike detected', {
53
+ code: code ? `${code.substring(0, 3)}***${code.slice(-4)}` : 'unknown',
54
+ total_tokens: usage.total_tokens,
55
+ threshold: alertThreshold,
56
+ model,
57
+ assistant_id: thread.getAssistantId(),
58
+ });
59
+ }
60
+ };
61
+
62
+ module.exports = { storeRunMetrics };
package/lib/index.d.ts CHANGED
@@ -246,6 +246,8 @@ declare module '@peopl-health/nexus' {
246
246
  getBatchingManager(): BatchingManager;
247
247
  getPipeline(): ProcessingPipeline;
248
248
  getAssistantProcessor(): AssistantProcessor;
249
+ processInstruction(code: string, instruction: string, role?: string): Promise<string | null>;
250
+ processSystemMessage(code: string, messages: string | string[], role?: string): Promise<string | null>;
249
251
  isConnected(): boolean;
250
252
  disconnect(): Promise<void>;
251
253
  }
@@ -556,21 +558,35 @@ declare module '@peopl-health/nexus' {
556
558
  mode?: 'local' | 'queue';
557
559
  queueAdapter?: QueueAdapter;
558
560
  sendMessage?: (messageData: MessageData) => Promise<any>;
559
- replyAssistant?: (code: string, body?: string, thread?: any, options?: any) => Promise<any>;
561
+ preProcessMessages?: (code: string, body: any, thread: any) => Promise<{ shouldProcess: boolean; messages: any[] | null; timings: Record<string, any> }>;
562
+ storeRunMetrics?: (code: string, thread: any, result: any, timings?: Record<string, any>) => Promise<void>;
560
563
  }
561
564
 
562
565
  export interface ProcessInput {
563
566
  code: string;
564
- messageData: MessageData;
565
- thread?: any;
567
+ body?: any;
566
568
  runOptions?: Record<string, any>;
567
569
  }
568
570
 
571
+ export interface LLMResult {
572
+ output: string;
573
+ tools_executed?: any[];
574
+ prompt?: string | null;
575
+ preset?: string | null;
576
+ response_id?: string | null;
577
+ run?: any;
578
+ predictionTimeMs?: number;
579
+ retries?: number;
580
+ completed?: boolean;
581
+ }
582
+
569
583
  export class AssistantProcessor {
570
584
  constructor(config: AssistantProcessorConfig);
571
- setReplyAssistant(fn: AssistantProcessorConfig['replyAssistant']): void;
572
585
  setSendMessage(fn: AssistantProcessorConfig['sendMessage']): void;
573
- process(input: ProcessInput): Promise<{ output: string; tools_executed?: any[]; prompt?: string; response_id?: string } | null>;
586
+ resolveThread(code: string): Promise<{ thread: any; assistant: any } | null>;
587
+ executeLLM(thread: any, assistant: any, runOptions?: Record<string, any>, messages?: any[]): Promise<LLMResult>;
588
+ process(input: ProcessInput): Promise<LLMResult | null>;
589
+ processDirect(input: { code: string; runOptions?: Record<string, any> }): Promise<LLMResult | null>;
574
590
  sendResponse(code: string, result: any): Promise<string | null>;
575
591
  }
576
592
  }
@@ -3,14 +3,12 @@ const runtimeConfig = require('../config/runtimeConfig');
3
3
  const { Historial_Clinico_ID } = require('../config/airtableConfig');
4
4
 
5
5
  const { logger } = require('../utils/logger');
6
- const { sanitizeOutput } = require('../utils/formatUtils');
7
6
  const { withTracing } = require('../utils/tracingDecorator.js');
8
7
 
9
8
  const { Thread } = require('../models/threadModel.js');
10
- const { getPredictionMetrics } = require('../models/predictionMetricsModel');
11
9
  const { insertMessage } = require('../models/messageModel');
12
10
 
13
- const { getCurRow, runAssistantWithRetries } = require('../helpers/assistantHelper.js');
11
+ const { getCurRow } = require('../helpers/assistantHelper.js');
14
12
  const { getThread, switchThreadStoppedStatus, setThreadPromptId } = require('../helpers/threadHelper.js');
15
13
  const { processThreadMessage } = require('../helpers/processHelper.js');
16
14
  const { getLastNMessages, storeProcessedContent } = require('../helpers/messageHelper.js');
@@ -66,40 +64,34 @@ const createAssistantCore = async (code, assistant_id, _messages = [], force = f
66
64
  }
67
65
  };
68
66
 
69
- const addMsgAssistantCore = async (code, inMessages, role = 'system', reply = false, skipSystemMessage = false) => {
67
+ const addMsgAssistantCore = async (code, inMessages, role = 'system') => {
70
68
  const thread = await getThread(code);
71
69
  if (!thread) return null;
72
70
 
73
71
  try {
74
72
  const messages = Array.isArray(inMessages) ? inMessages : [inMessages];
75
73
 
76
- if (!skipSystemMessage) {
77
- for (let i = 0; i < messages.length; i++) {
78
- const message = messages[i];
79
- try {
80
- const message_id = `system_${Date.now()}_${i}_${Math.random().toString(36).substring(7)}`;
81
- await insertMessage({
82
- nombre_whatsapp: 'System',
83
- numero: code,
84
- body: message,
85
- message_id: message_id,
86
- from_me: true,
87
- processed: true,
88
- origin: 'system',
89
- assistant_id: thread.getAssistantId(),
90
- raw: { role: role }
91
- });
92
- } catch (err) {
93
- logger.error('[addMsgAssistant] Error saving system message', { err });
94
- }
74
+ for (let i = 0; i < messages.length; i++) {
75
+ const message = messages[i];
76
+ try {
77
+ const message_id = `system_${Date.now()}_${i}_${Math.random().toString(36).substring(7)}`;
78
+ await insertMessage({
79
+ nombre_whatsapp: 'System',
80
+ numero: code,
81
+ body: message,
82
+ message_id: message_id,
83
+ from_me: true,
84
+ processed: true,
85
+ origin: 'system',
86
+ assistant_id: thread.getAssistantId(),
87
+ raw: { role: role }
88
+ });
89
+ } catch (err) {
90
+ logger.error('[addMsgAssistant] Error saving system message', { err });
95
91
  }
96
92
  }
97
93
 
98
- if (!reply) return null;
99
-
100
- const assistant = getAssistantById(thread.getAssistantId(), thread);
101
- const runResult = await runAssistantWithRetries(thread, assistant, { toolChoice: 'none' });
102
- return runResult?.output || null;
94
+ return null;
103
95
  } catch (error) {
104
96
  logger.error('[addMsgAssistant] Error adding message', { error: error.message, code, role });
105
97
  return null;
@@ -111,73 +103,50 @@ const addInstructionCore = async (code, instruction, role = 'system') => {
111
103
  if (!thread) return null;
112
104
 
113
105
  try {
114
- const assistant = getAssistantById(thread.getAssistantId(), thread);
115
-
116
- try {
117
- const message_id = `instruction_${Date.now()}_${Math.random().toString(36).substring(7)}`;
118
- await insertMessage({
119
- nombre_whatsapp: 'Instruction',
120
- numero: code,
121
- body: instruction,
122
- message_id: message_id,
123
- from_me: true,
124
- processed: true,
125
- origin: 'instruction',
126
- assistant_id: thread.getAssistantId(),
127
- raw: { role: role }
128
- });
129
- } catch (err) {
130
- logger.error('[addInstruction] Error saving instruction message', { err });
131
- }
132
-
133
- const runResult = await runAssistantWithRetries(thread, assistant, {
134
- additionalInstructions: instruction,
135
- additionalMessages: [
136
- { role: role, content: instruction }
137
- ],
138
- toolChoice: 'none'
106
+ const message_id = `instruction_${Date.now()}_${Math.random().toString(36).substring(7)}`;
107
+ await insertMessage({
108
+ nombre_whatsapp: 'Instruction',
109
+ numero: code,
110
+ body: instruction,
111
+ message_id: message_id,
112
+ from_me: true,
113
+ processed: true,
114
+ origin: 'instruction',
115
+ assistant_id: thread.getAssistantId(),
116
+ raw: { role: role }
139
117
  });
140
-
141
- logger.info('[addInstruction] Run response', { output: runResult?.output });
142
- return runResult?.output || null;
118
+ return null;
143
119
  } catch (error) {
144
120
  logger.error('[addInstruction] Error adding instruction', { error: error.message, code, role });
145
121
  return null;
146
122
  }
147
123
  };
148
124
 
149
- const replyAssistantCore = async (code, message_ = null, thread_ = null, runOptions = {}) => {
125
+ const preProcessMessagesCore = async (code, message_ = null, thread) => {
150
126
  const timings = {};
151
- const startTotal = Date.now();
152
-
153
- try {
154
- const threadStart = Date.now();
155
- const thread = thread_ || await getThread(code);
156
- timings.get_thread_ms = Date.now() - threadStart;
157
-
158
- if (!thread) return null;
159
127
 
128
+ try {
160
129
  const messagesStart = Date.now();
161
130
  const beforeCheckpoint = message_?.createdAt ?? null;
162
131
  const lastMessage = await getLastNMessages(code, 1, beforeCheckpoint, {
163
132
  query: { from_me: false }
164
133
  });
165
134
  timings.get_messages_ms = Date.now() - messagesStart;
166
-
135
+
167
136
  if (!lastMessage || lastMessage.length === 0) {
168
- logger.info('[replyAssistant] No relevant data found for this assistant.');
169
- return null;
137
+ logger.info('[preProcessMessages] No relevant data found for this assistant.');
138
+ return { shouldProcess: false, messages: null, timings };
170
139
  }
171
140
 
172
141
  const provider = createLLMProvider({ variant: runtimeConfig.get('VARIANT') });
173
- logger.info(`[replyAssistant] Processing ${lastMessage.length} messages in parallel`);
142
+ logger.info(`[preProcessMessages] Processing ${lastMessage.length} messages in parallel`);
174
143
  const processStart = Date.now();
175
144
  const processResult = await processThreadMessage(code, lastMessage, provider);
176
-
145
+
177
146
  const { results: processResults, timings: processTimings } = processResult;
178
147
  timings.process_messages_ms = Date.now() - processStart;
179
-
180
- logger.debug('[replyAssistant] Process timings breakdown', { processTimings });
148
+
149
+ logger.debug('[preProcessMessages] Process timings breakdown', { processTimings });
181
150
 
182
151
  if (processTimings) {
183
152
  timings.process_messages_breakdown = {
@@ -194,7 +163,7 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
194
163
  const allTempFiles = processResults.flatMap(r => r.tempFiles || []);
195
164
 
196
165
  await Promise.all(processResults.map(r => {
197
- const processedContent = r.messages && r.messages.length > 0
166
+ const processedContent = r.messages && r.messages.length > 0
198
167
  ? r.messages
199
168
  .filter(msg => msg.content.text !== r.reply?.body)
200
169
  .map(msg => msg.content.text)
@@ -206,13 +175,13 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
206
175
  await cleanupFiles(allTempFiles);
207
176
 
208
177
  if (urls.length > 0) {
209
- logger.info(`[replyAssistant] Processing ${urls.length} URLs for PDF combination`);
178
+ logger.info(`[preProcessMessages] Processing ${urls.length} URLs for PDF combination`);
210
179
  const pdfStart = Date.now();
211
180
  const pdfResult = await combineImagesToPDF({ code });
212
181
  timings.pdf_combination_ms = Date.now() - pdfStart;
213
182
  const { pdfBuffer, processedFiles } = pdfResult;
214
- logger.info(`[replyAssistant] PDF combination complete: ${processedFiles?.length || 0} files processed`);
215
-
183
+ logger.info(`[preProcessMessages] PDF combination complete: ${processedFiles?.length || 0} files processed`);
184
+
216
185
  if (pdfBuffer) {
217
186
  const key = `${code}-${Date.now()}-combined.pdf`;
218
187
  const bucket = runtimeConfig.get('AWS_S3_BUCKET_NAME');
@@ -220,107 +189,20 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
220
189
  await AWS.uploadBufferToS3(pdfBuffer, bucket, key, 'application/pdf');
221
190
  }
222
191
  }
223
-
192
+
224
193
  if (processedFiles && processedFiles.length) {
225
194
  await cleanupFiles(processedFiles);
226
195
  }
227
196
  }
228
197
 
229
198
  if (!patientMsg || thread.stopped) {
230
- logger.info('[replyAssistant] Skipping AI processing', { patientMsg, stopped: thread.stopped, code });
231
- return null;
199
+ logger.info('[preProcessMessages] Skipping AI processing', { patientMsg, stopped: thread.stopped, code });
200
+ return { shouldProcess: false, messages: null, timings };
232
201
  }
233
202
 
234
- const assistant = getAssistantById(thread.getAssistantId(), thread);
235
- const runStart = Date.now();
236
- const runResult = await runAssistantWithRetries(thread, assistant, runOptions, lastMessage);
237
- timings.run_assistant_ms = Date.now() - runStart;
238
- timings.total_ms = Date.now() - startTotal;
239
-
240
- const { output: rawOutput, completed, retries, predictionTimeMs, tools_executed } = runResult;
241
- const run = runResult.run;
242
- const usage = run?.usage || null;
243
- const model = run?.model || null;
244
-
245
- const output = sanitizeOutput(rawOutput);
246
- if (rawOutput !== output) {
247
- logger.debug('[replyAssistant] Output sanitized', {
248
- originalLength: rawOutput?.length || 0,
249
- sanitizedLength: output?.length || 0,
250
- removedContent: rawOutput?.length ? 'brackets_removed' : 'none'
251
- });
252
- }
253
-
254
- logger.info('[Assistant Response]', { output });
255
- logger.info('[Assistant Reply Complete]', {
256
- code: code ? `${code.substring(0, 3)}***${code.slice(-4)}` : 'unknown',
257
- messageCount: lastMessage.length,
258
- hasMedia: urls.length > 0,
259
- retries,
260
- totalMs: timings.total_ms,
261
- toolsExecuted: tools_executed?.length || 0,
262
- token_usage: usage ? {
263
- input_tokens: usage.input_tokens,
264
- output_tokens: usage.output_tokens,
265
- total_tokens: usage.total_tokens,
266
- model,
267
- } : undefined,
268
- });
269
-
270
- if (output && predictionTimeMs) {
271
- logger.debug('[replyAssistant] Storing metrics with timing_breakdown', {
272
- timing_breakdown: timings,
273
- has_breakdown: !!timings.process_messages_breakdown
274
- });
275
-
276
- const tokenUsage = usage ? {
277
- input_tokens: usage.input_tokens || 0,
278
- output_tokens: usage.output_tokens || 0,
279
- total_tokens: usage.total_tokens || 0,
280
- model: model || undefined,
281
- } : undefined;
282
-
283
- await getPredictionMetrics().create({
284
- message_id: `${code}-${Date.now()}`,
285
- numero: code,
286
- assistant_id: thread.getAssistantId(),
287
- prediction_time_ms: predictionTimeMs,
288
- retry_count: retries,
289
- completed: completed,
290
- timing_breakdown: timings,
291
- token_usage: tokenUsage,
292
- prompt_config: run?.prompt || null,
293
- response_id: run?.id || null,
294
- context_message_count: lastMessage?.length || null,
295
- resolved_prompt: run?.resolved_prompt || null,
296
- snippet_ids: run?.snippet_ids || [],
297
- tool_ids: run?.tool_ids || [],
298
- preset_id: run?.preset_id || null,
299
- preset_version: run?.preset_version || null,
300
- preset: run?.preset || null,
301
- }).catch(err => logger.error('[replyAssistant] Failed to store metrics', { error: err.message }));
302
-
303
- const alertThreshold = parseInt(process.env.TOKEN_ALERT_THRESHOLD, 10);
304
- if (alertThreshold && usage?.total_tokens > alertThreshold) {
305
- logger.warn('[replyAssistant] Token usage spike detected', {
306
- code: code ? `${code.substring(0, 3)}***${code.slice(-4)}` : 'unknown',
307
- total_tokens: usage.total_tokens,
308
- threshold: alertThreshold,
309
- model,
310
- assistant_id: thread.getAssistantId(),
311
- });
312
- }
313
- }
314
-
315
- return { output, tools_executed, prompt: run?.prompt || null, preset: run?.preset || null, response_id: run?.id || null };
203
+ return { shouldProcess: true, messages: lastMessage, timings };
316
204
  } catch (error) {
317
- logger.error('[replyAssistant] Error in reply', {
318
- error: error.message,
319
- stack: error.stack,
320
- code: code ? `${code.substring(0, 3)}***${code.slice(-4)}` : 'unknown',
321
- hasCustomThread: !!thread_,
322
- hasMessage: !!message_
323
- });
205
+ logger.error('[preProcessMessages] Error', { error: error.message, code });
324
206
  throw error;
325
207
  }
326
208
  };
@@ -371,11 +253,9 @@ module.exports = {
371
253
  'instruction.role': role,
372
254
  'operation.type': 'add_instruction'
373
255
  })),
374
- replyAssistant: withTracing(replyAssistantCore, 'assistant_reply', (code, message_, thread_, runOptions) => ({
256
+ preProcessMessages: withTracing(preProcessMessagesCore, 'pre_process_messages', (code) => ({
375
257
  'assistant.thread_code': code,
376
- 'assistant.has_message': !!message_,
377
- 'assistant.has_custom_thread': !!thread_,
378
- 'assistant.has_run_options': !!runOptions && Object.keys(runOptions).length > 0
258
+ 'operation.type': 'pre_process_messages'
379
259
  })),
380
260
  switchAssistant: withTracing(switchAssistantCore, 'switch_assistant', (code, assistant_id) => ({
381
261
  'assistant.thread_code': code,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "3.13.1",
3
+ "version": "3.13.3",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",