@peopl-health/nexus 3.13.2 → 3.13.4

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,51 +1,75 @@
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, storeRunMetrics = null }) {
8
+ Object.assign(this, { mode, queueAdapter, sendMessage, storeRunMetrics });
7
9
  if (mode === 'queue' && queueAdapter) {
8
- queueAdapter.process('assistant.process', (payload) => this._processLocal(payload));
10
+ queueAdapter.process('assistant.process', (payload) => this._executeLocal(payload));
9
11
  }
10
12
  }
11
13
 
12
- setReplyAssistant(fn) { this.replyAssistant = fn; }
13
14
  setSendMessage(fn) { this.sendMessage = fn; }
14
15
 
15
- async process({ code, body = null, runOptions = {} }) {
16
- if (!code) throw new Error('code is required for assistant processing');
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
+ }
17
22
 
18
- const result = (this.mode === 'queue')
19
- ? await this._processViaQueue({ code, body, runOptions })
20
- : await this._processLocal({ code, body, runOptions });
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;
21
27
 
22
- return result;
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
+
44
+ async process({ code, runOptions = {}, messages = null }) {
45
+ if (!code) throw new Error('code is required for assistant processing');
46
+
47
+ return (this.mode === 'queue')
48
+ ? await this._executeViaQueue({ code, runOptions })
49
+ : await this._executeLocal({ code, runOptions, messages });
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 _executeLocal({ code, runOptions = {}, messages = null }) {
53
+ const resolved = await this.resolveThread(code);
54
+ if (!resolved) return null;
55
+
56
+ const result = await this.executeLLM(resolved.thread, resolved.assistant, runOptions, messages);
57
+ if (this.storeRunMetrics) await this.storeRunMetrics(code, resolved.thread, result);
58
+ return result;
28
59
  }
29
60
 
30
- async _processViaQueue({ code, body, runOptions }) {
61
+ async _executeViaQueue({ code, runOptions }) {
31
62
  if (!this.queueAdapter) throw new Error('queueAdapter is required for queue mode');
32
- const jobId = await this.queueAdapter.enqueue('assistant.process', { code, body, runOptions });
63
+ const jobId = await this.queueAdapter.enqueue('assistant.process', { code, runOptions });
33
64
  return await this.queueAdapter.waitForResult(jobId, 120000);
34
65
  }
35
66
 
36
- async processDirect({ code, runOptions = {} }) {
37
- 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);
40
- }
41
-
42
67
  async sendResponse(code, result) {
43
68
  if (!this.sendMessage) throw new Error('sendMessage function not configured');
44
69
  if (!result?.output) return null;
45
70
  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
71
  return result.output;
47
72
  }
48
-
49
73
  }
50
74
 
51
75
  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,7 @@ 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
+ storeRunMetrics,
81
81
  });
82
82
  }
83
83
 
@@ -383,9 +383,22 @@ class NexusMessaging {
383
383
  async _handleWithCheckAfter(chatId) {
384
384
  await this._executeWithPipeline(chatId, 'message', 'preempt',
385
385
  async (preProcessResult, shouldContinue) => {
386
- return await this._processMessages(chatId, () =>
387
- this.assistantProcessor.process({ code: chatId, runOptions: { prePromptResult: preProcessResult } })
388
- , shouldContinue);
386
+ return await this._processMessages(chatId, async () => {
387
+ const resolved = await this.assistantProcessor.resolveThread(chatId);
388
+ if (!resolved) return null;
389
+
390
+ const preProcessed = await preProcessMessages(chatId, null, resolved.thread);
391
+ if (!preProcessed.shouldProcess) return null;
392
+
393
+ const result = await this.assistantProcessor.executeLLM(
394
+ resolved.thread, resolved.assistant,
395
+ { prePromptResult: preProcessResult },
396
+ preProcessed.messages
397
+ );
398
+
399
+ if (storeRunMetrics) await storeRunMetrics(chatId, resolved.thread, result, preProcessed.timings);
400
+ return { ...result, timings: preProcessed.timings };
401
+ }, shouldContinue);
389
402
  }
390
403
  );
391
404
  }
@@ -406,7 +419,7 @@ class NexusMessaging {
406
419
 
407
420
  const result = await this._executeWithPipeline(code, 'instruction', 'queue',
408
421
  async (preProcessResult) => {
409
- return await this.assistantProcessor.processDirect({
422
+ return await this.assistantProcessor.process({
410
423
  code,
411
424
  runOptions: {
412
425
  prePromptResult: preProcessResult,
@@ -441,7 +454,7 @@ class NexusMessaging {
441
454
 
442
455
  const result = await this._executeWithPipeline(code, 'system', 'queue',
443
456
  async (preProcessResult) => {
444
- return await this.assistantProcessor.processDirect({
457
+ return await this.assistantProcessor.process({
445
458
  code,
446
459
  runOptions: {
447
460
  prePromptResult: preProcessResult,
@@ -3,9 +3,6 @@ const fsSync = require('fs');
3
3
  const path = require('path');
4
4
  const { execFile } = require('child_process');
5
5
 
6
- const sharp = require('sharp');
7
- const { PDFDocument } = require('pdf-lib');
8
-
9
6
  const { downloadFileFromS3 } = require('../config/awsConfig.js');
10
7
 
11
8
  const { sanitizeFilename } = require('../utils/sanitizerUtils.js');
@@ -76,43 +73,6 @@ async function convertPdfToImages(pdfName, existingPdfPath = null) {
76
73
  });
77
74
  }
78
75
 
79
- async function combineImagesToPDF(config) {
80
- const { code, extensions = ['jpg', 'jpeg', 'png', 'tiff'], sortNumerically = true } = config;
81
- const inputDir = path.join(__dirname, 'assets', 'tmp');
82
- const files = await fs.readdir(inputDir);
83
-
84
- const imageFiles = files.filter(file => {
85
- const ext = path.extname(file).toLowerCase().slice(1);
86
- return extensions.includes(ext) && (!code || file.startsWith(code));
87
- });
88
-
89
- imageFiles.sort((a, b) => {
90
- if (!sortNumerically) return a.localeCompare(b);
91
- const aNum = a.match(/\d+/)?.[0];
92
- const bNum = b.match(/\d+/)?.[0];
93
- return (aNum && bNum) ? parseInt(aNum) - parseInt(bNum) : a.localeCompare(b);
94
- });
95
-
96
- const pdfDoc = await PDFDocument.create();
97
- const processedFiles = [];
98
-
99
- for (const file of imageFiles) {
100
- try {
101
- const filePath = path.join(inputDir, file);
102
- const imageBuffer = await fs.readFile(filePath);
103
- const pngBuffer = await sharp(imageBuffer).toFormat('png').toBuffer();
104
- const { width, height } = await sharp(imageBuffer).metadata();
105
- const img = await pdfDoc.embedPng(pngBuffer);
106
- pdfDoc.addPage([width, height]).drawImage(img, { x: 0, y: 0, width, height });
107
- processedFiles.push(filePath);
108
- } catch (error) {
109
- logger.error(`Error processing file ${file}`, { error: error.message });
110
- }
111
- }
112
-
113
- return { pdfBuffer: await pdfDoc.save(), processedFiles };
114
- }
115
-
116
76
  const cleanupFiles = async (files) => {
117
77
  if (!files?.length) return;
118
78
 
@@ -168,7 +128,6 @@ async function downloadMediaAndCreateFile(code, reply) {
168
128
 
169
129
  module.exports = {
170
130
  convertPdfToImages,
171
- combineImagesToPDF,
172
131
  cleanupFiles,
173
132
  downloadMediaAndCreateFile
174
133
  };
@@ -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 };
@@ -138,6 +138,15 @@ const processMediaFilesCore = async (code, reply, provider) => {
138
138
 
139
139
  if (!reply.media) return { messagesChat: [], url: null, tempFiles: [], timings };
140
140
 
141
+ if (reply.media?.metadata?.processedContent) {
142
+ return {
143
+ messagesChat: [{ type: 'text', text: reply.media.metadata.processedContent }],
144
+ url: null,
145
+ tempFiles: [],
146
+ timings
147
+ };
148
+ }
149
+
141
150
  const { result: fileNames, duration } = await withTracing(
142
151
  downloadMediaAndCreateFile, 'download_media',
143
152
  () => ({ 'media.message_id': reply.message_id, 'media.type': reply.media?.mediaType }),
package/lib/index.d.ts CHANGED
@@ -558,23 +558,33 @@ 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
+ storeRunMetrics?: (code: string, thread: any, result: any, timings?: Record<string, any>) => Promise<void>;
563
562
  }
564
563
 
565
564
  export interface ProcessInput {
566
565
  code: string;
567
- messageData: MessageData;
568
- thread?: any;
569
566
  runOptions?: Record<string, any>;
567
+ messages?: any[] | null;
568
+ }
569
+
570
+ export interface LLMResult {
571
+ output: string;
572
+ tools_executed?: any[];
573
+ prompt?: string | null;
574
+ preset?: string | null;
575
+ response_id?: string | null;
576
+ run?: any;
577
+ predictionTimeMs?: number;
578
+ retries?: number;
579
+ completed?: boolean;
570
580
  }
571
581
 
572
582
  export class AssistantProcessor {
573
583
  constructor(config: AssistantProcessorConfig);
574
- setReplyAssistant(fn: AssistantProcessorConfig['replyAssistant']): void;
575
584
  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>;
585
+ resolveThread(code: string): Promise<{ thread: any; assistant: any } | null>;
586
+ executeLLM(thread: any, assistant: any, runOptions?: Record<string, any>, messages?: any[]): Promise<LLMResult>;
587
+ process(input: ProcessInput): Promise<LLMResult | null>;
578
588
  sendResponse(code: string, result: any): Promise<string | null>;
579
589
  }
580
590
  }
@@ -1,20 +1,17 @@
1
- const AWS = require('../config/awsConfig.js');
2
1
  const runtimeConfig = require('../config/runtimeConfig');
3
2
  const { Historial_Clinico_ID } = require('../config/airtableConfig');
4
3
 
5
4
  const { logger } = require('../utils/logger');
6
- const { sanitizeOutput } = require('../utils/formatUtils');
7
5
  const { withTracing } = require('../utils/tracingDecorator.js');
8
6
 
9
7
  const { Thread } = require('../models/threadModel.js');
10
- const { getPredictionMetrics } = require('../models/predictionMetricsModel');
11
8
  const { insertMessage } = require('../models/messageModel');
12
9
 
13
- const { getCurRow, runAssistantWithRetries } = require('../helpers/assistantHelper.js');
10
+ const { getCurRow } = require('../helpers/assistantHelper.js');
14
11
  const { getThread, switchThreadStoppedStatus, setThreadPromptId } = require('../helpers/threadHelper.js');
15
12
  const { processThreadMessage } = require('../helpers/processHelper.js');
16
13
  const { getLastNMessages, storeProcessedContent } = require('../helpers/messageHelper.js');
17
- const { combineImagesToPDF, cleanupFiles } = require('../helpers/filesHelper.js');
14
+ const { cleanupFiles } = require('../helpers/filesHelper.js');
18
15
 
19
16
  const { createLLMProvider } = require('../providers/createLLMProvider');
20
17
  const { getAssistantById } = require('./assistantResolver');
@@ -124,38 +121,31 @@ const addInstructionCore = async (code, instruction, role = 'system') => {
124
121
  }
125
122
  };
126
123
 
127
- const replyAssistantCore = async (code, message_ = null, thread_ = null, runOptions = {}) => {
124
+ const preProcessMessagesCore = async (code, message_ = null, thread) => {
128
125
  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
126
 
127
+ try {
138
128
  const messagesStart = Date.now();
139
129
  const beforeCheckpoint = message_?.createdAt ?? null;
140
130
  const lastMessage = await getLastNMessages(code, 1, beforeCheckpoint, {
141
131
  query: { from_me: false }
142
132
  });
143
133
  timings.get_messages_ms = Date.now() - messagesStart;
144
-
134
+
145
135
  if (!lastMessage || lastMessage.length === 0) {
146
- logger.info('[replyAssistant] No relevant data found for this assistant.');
147
- return null;
136
+ logger.info('[preProcessMessages] No relevant data found for this assistant.');
137
+ return { shouldProcess: false, messages: null, timings };
148
138
  }
149
139
 
150
140
  const provider = createLLMProvider({ variant: runtimeConfig.get('VARIANT') });
151
- logger.info(`[replyAssistant] Processing ${lastMessage.length} messages in parallel`);
141
+ logger.info(`[preProcessMessages] Processing ${lastMessage.length} messages in parallel`);
152
142
  const processStart = Date.now();
153
143
  const processResult = await processThreadMessage(code, lastMessage, provider);
154
-
144
+
155
145
  const { results: processResults, timings: processTimings } = processResult;
156
146
  timings.process_messages_ms = Date.now() - processStart;
157
-
158
- logger.debug('[replyAssistant] Process timings breakdown', { processTimings });
147
+
148
+ logger.debug('[preProcessMessages] Process timings breakdown', { processTimings });
159
149
 
160
150
  if (processTimings) {
161
151
  timings.process_messages_breakdown = {
@@ -168,11 +158,10 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
168
158
  }
169
159
 
170
160
  const patientMsg = processResults.some(r => r.isPatient);
171
- const urls = processResults.filter(r => r.url).map(r => ({ url: r.url }));
172
161
  const allTempFiles = processResults.flatMap(r => r.tempFiles || []);
173
162
 
174
163
  await Promise.all(processResults.map(r => {
175
- const processedContent = r.messages && r.messages.length > 0
164
+ const processedContent = r.messages && r.messages.length > 0
176
165
  ? r.messages
177
166
  .filter(msg => msg.content.text !== r.reply?.body)
178
167
  .map(msg => msg.content.text)
@@ -183,143 +172,14 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
183
172
 
184
173
  await cleanupFiles(allTempFiles);
185
174
 
186
- if (urls.length > 0) {
187
- logger.info(`[replyAssistant] Processing ${urls.length} URLs for PDF combination`);
188
- const pdfStart = Date.now();
189
- const pdfResult = await combineImagesToPDF({ code });
190
- timings.pdf_combination_ms = Date.now() - pdfStart;
191
- const { pdfBuffer, processedFiles } = pdfResult;
192
- logger.info(`[replyAssistant] PDF combination complete: ${processedFiles?.length || 0} files processed`);
193
-
194
- if (pdfBuffer) {
195
- const key = `${code}-${Date.now()}-combined.pdf`;
196
- const bucket = runtimeConfig.get('AWS_S3_BUCKET_NAME');
197
- if (bucket) {
198
- await AWS.uploadBufferToS3(pdfBuffer, bucket, key, 'application/pdf');
199
- }
200
- }
201
-
202
- if (processedFiles && processedFiles.length) {
203
- await cleanupFiles(processedFiles);
204
- }
205
- }
206
-
207
175
  if (!patientMsg || thread.stopped) {
208
- logger.info('[replyAssistant] Skipping AI processing', { patientMsg, stopped: thread.stopped, code });
209
- return null;
210
- }
211
-
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
- }
176
+ logger.info('[preProcessMessages] Skipping AI processing', { patientMsg, stopped: thread.stopped, code });
177
+ return { shouldProcess: false, messages: null, timings };
291
178
  }
292
179
 
293
- return { output, tools_executed, prompt: run?.prompt || null, preset: run?.preset || null, response_id: run?.id || null };
180
+ return { shouldProcess: true, messages: lastMessage, timings };
294
181
  } 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
- };
321
- } catch (error) {
322
- logger.error('[runDirect] Error', { error: error.message, code });
182
+ logger.error('[preProcessMessages] Error', { error: error.message, code });
323
183
  throw error;
324
184
  }
325
185
  };
@@ -370,15 +230,9 @@ module.exports = {
370
230
  'instruction.role': role,
371
231
  'operation.type': 'add_instruction'
372
232
  })),
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) => ({
233
+ preProcessMessages: withTracing(preProcessMessagesCore, 'pre_process_messages', (code) => ({
380
234
  'assistant.thread_code': code,
381
- 'operation.type': 'run_direct'
235
+ 'operation.type': 'pre_process_messages'
382
236
  })),
383
237
  switchAssistant: withTracing(switchAssistantCore, 'switch_assistant', (code, assistant_id) => ({
384
238
  '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.4",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",