@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,
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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, 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.
|
|
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
|
-
|
|
47
|
+
return (this.mode === 'queue')
|
|
19
48
|
? await this._processViaQueue({ code, body, runOptions })
|
|
20
|
-
: await this.
|
|
21
|
-
|
|
22
|
-
return result;
|
|
49
|
+
: await this._processViaLocal({ code, body, runOptions });
|
|
23
50
|
}
|
|
24
51
|
|
|
25
|
-
async
|
|
26
|
-
|
|
27
|
-
|
|
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,
|
|
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
|
-
|
|
80
|
+
preProcessMessages,
|
|
81
|
+
storeRunMetrics,
|
|
80
82
|
});
|
|
81
83
|
}
|
|
82
84
|
|
|
@@ -350,29 +352,112 @@ class NexusMessaging {
|
|
|
350
352
|
}
|
|
351
353
|
|
|
352
354
|
/*
|
|
353
|
-
*
|
|
355
|
+
* PROCESSING PIPELINE
|
|
354
356
|
*/
|
|
355
357
|
|
|
356
|
-
async
|
|
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
|
-
|
|
361
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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'
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
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('[
|
|
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(`[
|
|
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('[
|
|
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(`[
|
|
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(`[
|
|
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('[
|
|
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
|
-
|
|
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('[
|
|
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
|
-
|
|
256
|
+
preProcessMessages: withTracing(preProcessMessagesCore, 'pre_process_messages', (code) => ({
|
|
375
257
|
'assistant.thread_code': code,
|
|
376
|
-
'
|
|
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,
|