@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.
- package/lib/core/AssistantProcessor.js +49 -25
- package/lib/core/NexusMessaging.js +21 -8
- package/lib/helpers/filesHelper.js +0 -41
- package/lib/helpers/metricsHelper.js +62 -0
- package/lib/helpers/processHelper.js +9 -0
- package/lib/index.d.ts +17 -7
- package/lib/services/assistantService.js +18 -164
- package/package.json +1 -1
|
@@ -1,51 +1,75 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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,
|
|
6
|
-
Object.assign(this, { mode, queueAdapter, sendMessage,
|
|
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.
|
|
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
|
|
16
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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.
|
|
388
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
577
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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('[
|
|
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(`[
|
|
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('[
|
|
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('[
|
|
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 {
|
|
180
|
+
return { shouldProcess: true, messages: lastMessage, timings };
|
|
294
181
|
} catch (error) {
|
|
295
|
-
logger.error('[
|
|
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
|
-
|
|
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': '
|
|
235
|
+
'operation.type': 'pre_process_messages'
|
|
382
236
|
})),
|
|
383
237
|
switchAssistant: withTracing(switchAssistantCore, 'switch_assistant', (code, assistant_id) => ({
|
|
384
238
|
'assistant.thread_code': code,
|