@peopl-health/nexus 3.13.2 → 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.
@@ -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, runDirect = null }) {
6
- Object.assign(this, { mode, queueAdapter, sendMessage, replyAssistant, runDirect });
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 }) {
@@ -35,8 +76,13 @@ class AssistantProcessor {
35
76
 
36
77
  async processDirect({ code, runOptions = {} }) {
37
78
  if (!code) throw new Error('code is required for direct processing');
38
- if (!this.runDirect) throw new Error('runDirect function not configured');
39
- return await this.runDirect(code, runOptions);
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;
40
86
  }
41
87
 
42
88
  async sendResponse(code, result) {
@@ -45,7 +91,6 @@ class AssistantProcessor {
45
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 });
46
92
  return result.output;
47
93
  }
48
-
49
94
  }
50
95
 
51
96
  module.exports = { AssistantProcessor };
@@ -11,10 +11,11 @@ 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, runDirect } = 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,8 +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
- runDirect
80
+ preProcessMessages,
81
+ storeRunMetrics,
81
82
  });
82
83
  }
83
84
 
@@ -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
@@ -558,23 +558,35 @@ declare module '@peopl-health/nexus' {
558
558
  mode?: 'local' | 'queue';
559
559
  queueAdapter?: QueueAdapter;
560
560
  sendMessage?: (messageData: MessageData) => Promise<any>;
561
- replyAssistant?: (code: string, body?: string, thread?: any, options?: any) => Promise<any>;
562
- runDirect?: (code: string, runOptions?: Record<string, 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>;
563
563
  }
564
564
 
565
565
  export interface ProcessInput {
566
566
  code: string;
567
- messageData: MessageData;
568
- thread?: any;
567
+ body?: any;
569
568
  runOptions?: Record<string, any>;
570
569
  }
571
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
+
572
583
  export class AssistantProcessor {
573
584
  constructor(config: AssistantProcessorConfig);
574
- setReplyAssistant(fn: AssistantProcessorConfig['replyAssistant']): void;
575
585
  setSendMessage(fn: AssistantProcessorConfig['sendMessage']): void;
576
- process(input: ProcessInput): Promise<{ output: string; tools_executed?: any[]; prompt?: string; response_id?: string } | null>;
577
- processDirect(input: { code: string; runOptions?: Record<string, any> }): 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>;
578
590
  sendResponse(code: string, result: any): Promise<string | null>;
579
591
  }
580
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');
@@ -124,38 +122,31 @@ const addInstructionCore = async (code, instruction, role = 'system') => {
124
122
  }
125
123
  };
126
124
 
127
- const replyAssistantCore = async (code, message_ = null, thread_ = null, runOptions = {}) => {
125
+ const preProcessMessagesCore = async (code, message_ = null, thread) => {
128
126
  const timings = {};
129
- const startTotal = Date.now();
130
-
131
- try {
132
- const threadStart = Date.now();
133
- const thread = thread_ || await getThread(code);
134
- timings.get_thread_ms = Date.now() - threadStart;
135
-
136
- if (!thread) return null;
137
127
 
128
+ try {
138
129
  const messagesStart = Date.now();
139
130
  const beforeCheckpoint = message_?.createdAt ?? null;
140
131
  const lastMessage = await getLastNMessages(code, 1, beforeCheckpoint, {
141
132
  query: { from_me: false }
142
133
  });
143
134
  timings.get_messages_ms = Date.now() - messagesStart;
144
-
135
+
145
136
  if (!lastMessage || lastMessage.length === 0) {
146
- logger.info('[replyAssistant] No relevant data found for this assistant.');
147
- return null;
137
+ logger.info('[preProcessMessages] No relevant data found for this assistant.');
138
+ return { shouldProcess: false, messages: null, timings };
148
139
  }
149
140
 
150
141
  const provider = createLLMProvider({ variant: runtimeConfig.get('VARIANT') });
151
- logger.info(`[replyAssistant] Processing ${lastMessage.length} messages in parallel`);
142
+ logger.info(`[preProcessMessages] Processing ${lastMessage.length} messages in parallel`);
152
143
  const processStart = Date.now();
153
144
  const processResult = await processThreadMessage(code, lastMessage, provider);
154
-
145
+
155
146
  const { results: processResults, timings: processTimings } = processResult;
156
147
  timings.process_messages_ms = Date.now() - processStart;
157
-
158
- logger.debug('[replyAssistant] Process timings breakdown', { processTimings });
148
+
149
+ logger.debug('[preProcessMessages] Process timings breakdown', { processTimings });
159
150
 
160
151
  if (processTimings) {
161
152
  timings.process_messages_breakdown = {
@@ -172,7 +163,7 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
172
163
  const allTempFiles = processResults.flatMap(r => r.tempFiles || []);
173
164
 
174
165
  await Promise.all(processResults.map(r => {
175
- const processedContent = r.messages && r.messages.length > 0
166
+ const processedContent = r.messages && r.messages.length > 0
176
167
  ? r.messages
177
168
  .filter(msg => msg.content.text !== r.reply?.body)
178
169
  .map(msg => msg.content.text)
@@ -184,13 +175,13 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
184
175
  await cleanupFiles(allTempFiles);
185
176
 
186
177
  if (urls.length > 0) {
187
- logger.info(`[replyAssistant] Processing ${urls.length} URLs for PDF combination`);
178
+ logger.info(`[preProcessMessages] Processing ${urls.length} URLs for PDF combination`);
188
179
  const pdfStart = Date.now();
189
180
  const pdfResult = await combineImagesToPDF({ code });
190
181
  timings.pdf_combination_ms = Date.now() - pdfStart;
191
182
  const { pdfBuffer, processedFiles } = pdfResult;
192
- logger.info(`[replyAssistant] PDF combination complete: ${processedFiles?.length || 0} files processed`);
193
-
183
+ logger.info(`[preProcessMessages] PDF combination complete: ${processedFiles?.length || 0} files processed`);
184
+
194
185
  if (pdfBuffer) {
195
186
  const key = `${code}-${Date.now()}-combined.pdf`;
196
187
  const bucket = runtimeConfig.get('AWS_S3_BUCKET_NAME');
@@ -198,128 +189,20 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
198
189
  await AWS.uploadBufferToS3(pdfBuffer, bucket, key, 'application/pdf');
199
190
  }
200
191
  }
201
-
192
+
202
193
  if (processedFiles && processedFiles.length) {
203
194
  await cleanupFiles(processedFiles);
204
195
  }
205
196
  }
206
197
 
207
198
  if (!patientMsg || thread.stopped) {
208
- logger.info('[replyAssistant] Skipping AI processing', { patientMsg, stopped: thread.stopped, code });
209
- return null;
199
+ logger.info('[preProcessMessages] Skipping AI processing', { patientMsg, stopped: thread.stopped, code });
200
+ return { shouldProcess: false, messages: null, timings };
210
201
  }
211
202
 
212
- const assistant = getAssistantById(thread.getAssistantId(), thread);
213
- const runStart = Date.now();
214
- const runResult = await runAssistantWithRetries(thread, assistant, runOptions, lastMessage);
215
- timings.run_assistant_ms = Date.now() - runStart;
216
- timings.total_ms = Date.now() - startTotal;
217
-
218
- const { output: rawOutput, completed, retries, predictionTimeMs, tools_executed } = runResult;
219
- const run = runResult.run;
220
- const usage = run?.usage || null;
221
- const model = run?.model || null;
222
-
223
- const output = sanitizeOutput(rawOutput);
224
- if (rawOutput !== output) {
225
- logger.debug('[replyAssistant] Output sanitized', {
226
- originalLength: rawOutput?.length || 0,
227
- sanitizedLength: output?.length || 0,
228
- removedContent: rawOutput?.length ? 'brackets_removed' : 'none'
229
- });
230
- }
231
-
232
- logger.info('[Assistant Response]', { output });
233
- logger.info('[Assistant Reply Complete]', {
234
- code: code ? `${code.substring(0, 3)}***${code.slice(-4)}` : 'unknown',
235
- messageCount: lastMessage.length,
236
- hasMedia: urls.length > 0,
237
- retries,
238
- totalMs: timings.total_ms,
239
- toolsExecuted: tools_executed?.length || 0,
240
- token_usage: usage ? {
241
- input_tokens: usage.input_tokens,
242
- output_tokens: usage.output_tokens,
243
- total_tokens: usage.total_tokens,
244
- model,
245
- } : undefined,
246
- });
247
-
248
- if (output && predictionTimeMs) {
249
- logger.debug('[replyAssistant] Storing metrics with timing_breakdown', {
250
- timing_breakdown: timings,
251
- has_breakdown: !!timings.process_messages_breakdown
252
- });
253
-
254
- const tokenUsage = usage ? {
255
- input_tokens: usage.input_tokens || 0,
256
- output_tokens: usage.output_tokens || 0,
257
- total_tokens: usage.total_tokens || 0,
258
- model: model || undefined,
259
- } : undefined;
260
-
261
- await getPredictionMetrics().create({
262
- message_id: `${code}-${Date.now()}`,
263
- numero: code,
264
- assistant_id: thread.getAssistantId(),
265
- prediction_time_ms: predictionTimeMs,
266
- retry_count: retries,
267
- completed: completed,
268
- timing_breakdown: timings,
269
- token_usage: tokenUsage,
270
- prompt_config: run?.prompt || null,
271
- response_id: run?.id || null,
272
- context_message_count: lastMessage?.length || null,
273
- resolved_prompt: run?.resolved_prompt || null,
274
- snippet_ids: run?.snippet_ids || [],
275
- tool_ids: run?.tool_ids || [],
276
- preset_id: run?.preset_id || null,
277
- preset_version: run?.preset_version || null,
278
- preset: run?.preset || null,
279
- }).catch(err => logger.error('[replyAssistant] Failed to store metrics', { error: err.message }));
280
-
281
- const alertThreshold = parseInt(process.env.TOKEN_ALERT_THRESHOLD, 10);
282
- if (alertThreshold && usage?.total_tokens > alertThreshold) {
283
- logger.warn('[replyAssistant] Token usage spike detected', {
284
- code: code ? `${code.substring(0, 3)}***${code.slice(-4)}` : 'unknown',
285
- total_tokens: usage.total_tokens,
286
- threshold: alertThreshold,
287
- model,
288
- assistant_id: thread.getAssistantId(),
289
- });
290
- }
291
- }
292
-
293
- return { output, tools_executed, prompt: run?.prompt || null, preset: run?.preset || null, response_id: run?.id || null };
294
- } catch (error) {
295
- logger.error('[replyAssistant] Error in reply', {
296
- error: error.message,
297
- stack: error.stack,
298
- code: code ? `${code.substring(0, 3)}***${code.slice(-4)}` : 'unknown',
299
- hasCustomThread: !!thread_,
300
- hasMessage: !!message_
301
- });
302
- throw error;
303
- }
304
- };
305
-
306
- const runDirectCore = async (code, runOptions = {}) => {
307
- const thread = await getThread(code);
308
- if (!thread) return null;
309
-
310
- try {
311
- const assistant = getAssistantById(thread.getAssistantId(), thread);
312
- const runResult = await runAssistantWithRetries(thread, assistant, runOptions);
313
- const output = sanitizeOutput(runResult?.output);
314
- return {
315
- output,
316
- tools_executed: runResult?.tools_executed,
317
- prompt: runResult?.run?.prompt || null,
318
- preset: runResult?.run?.preset || null,
319
- response_id: runResult?.run?.id || null
320
- };
203
+ return { shouldProcess: true, messages: lastMessage, timings };
321
204
  } catch (error) {
322
- logger.error('[runDirect] Error', { error: error.message, code });
205
+ logger.error('[preProcessMessages] Error', { error: error.message, code });
323
206
  throw error;
324
207
  }
325
208
  };
@@ -370,15 +253,9 @@ module.exports = {
370
253
  'instruction.role': role,
371
254
  'operation.type': 'add_instruction'
372
255
  })),
373
- replyAssistant: withTracing(replyAssistantCore, 'assistant_reply', (code, message_, thread_, runOptions) => ({
374
- 'assistant.thread_code': code,
375
- 'assistant.has_message': !!message_,
376
- 'assistant.has_custom_thread': !!thread_,
377
- 'assistant.has_run_options': !!runOptions && Object.keys(runOptions).length > 0
378
- })),
379
- runDirect: withTracing(runDirectCore, 'run_direct', (code) => ({
256
+ preProcessMessages: withTracing(preProcessMessagesCore, 'pre_process_messages', (code) => ({
380
257
  'assistant.thread_code': code,
381
- 'operation.type': 'run_direct'
258
+ 'operation.type': 'pre_process_messages'
382
259
  })),
383
260
  switchAssistant: withTracing(switchAssistantCore, 'switch_assistant', (code, assistant_id) => ({
384
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.2",
3
+ "version": "3.13.3",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",