@peopl-health/nexus 3.11.4 → 3.13.0
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,11 +1,6 @@
|
|
|
1
1
|
const { logger } = require('../utils/logger');
|
|
2
2
|
const { Message } = require('../models/messageModel');
|
|
3
3
|
|
|
4
|
-
const VALID_HOOKS = ['preBatch', 'postBatch', 'onBatchError', 'prePromptBuild'];
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Manages message batching, processing locks, run abandonment, and lifecycle hooks.
|
|
8
|
-
*/
|
|
9
4
|
class BatchingManager {
|
|
10
5
|
constructor({ provider = null, config = {} }) {
|
|
11
6
|
this.provider = provider;
|
|
@@ -20,33 +15,6 @@ class BatchingManager {
|
|
|
20
15
|
this.activeRequests = new Map();
|
|
21
16
|
this.abandonedRuns = new Set();
|
|
22
17
|
this.typingIntervals = new Map();
|
|
23
|
-
|
|
24
|
-
this._hooks = {
|
|
25
|
-
preBatch: [],
|
|
26
|
-
postBatch: [],
|
|
27
|
-
onBatchError: [],
|
|
28
|
-
prePromptBuild: [],
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
if (config.hooks) {
|
|
32
|
-
for (const [name, handlers] of Object.entries(config.hooks)) {
|
|
33
|
-
const fns = Array.isArray(handlers) ? handlers : [handlers];
|
|
34
|
-
for (const fn of fns) {
|
|
35
|
-
this.addHook(name, fn);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
addHook(name, fn) {
|
|
42
|
-
if (!VALID_HOOKS.includes(name)) {
|
|
43
|
-
throw new Error(`[BatchingManager] Unknown hook: "${name}". Valid hooks: ${VALID_HOOKS.join(', ')}`);
|
|
44
|
-
}
|
|
45
|
-
if (typeof fn !== 'function') {
|
|
46
|
-
throw new Error(`[BatchingManager] Hook "${name}" must be a function`);
|
|
47
|
-
}
|
|
48
|
-
this._hooks[name].push(fn);
|
|
49
|
-
return this;
|
|
50
18
|
}
|
|
51
19
|
|
|
52
20
|
setProvider(provider) {
|
|
@@ -70,28 +38,22 @@ class BatchingManager {
|
|
|
70
38
|
|
|
71
39
|
if (this.config.immediateRestart) {
|
|
72
40
|
this._clearProcessingState(chatId);
|
|
73
|
-
await this.
|
|
41
|
+
await this._runWithLock(chatId, processingFn, sendResponseFn);
|
|
74
42
|
}
|
|
75
43
|
}
|
|
76
44
|
return;
|
|
77
45
|
}
|
|
78
46
|
|
|
79
47
|
this.processingLocks.set(chatId, true);
|
|
80
|
-
await this.
|
|
48
|
+
await this._runWithLock(chatId, processingFn, sendResponseFn);
|
|
81
49
|
}
|
|
82
50
|
|
|
83
|
-
async
|
|
51
|
+
async _runWithLock(chatId, processingFn, sendResponseFn) {
|
|
84
52
|
this.processingLocks.set(chatId, true);
|
|
85
53
|
const runId = `run_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
86
54
|
this.activeRequests.set(chatId, runId);
|
|
87
55
|
|
|
88
56
|
try {
|
|
89
|
-
const pre = await this._runSequentialHooks(chatId, runId);
|
|
90
|
-
if (pre?.skip) {
|
|
91
|
-
logger.debug('[BatchingManager] Batch skipped by preBatch hook', { chatId, runId });
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
57
|
await this._startTypingRefresh(chatId, runId);
|
|
96
58
|
if (this._checkAbandoned(runId)) return;
|
|
97
59
|
|
|
@@ -101,11 +63,9 @@ class BatchingManager {
|
|
|
101
63
|
if (sendResponseFn && result) await sendResponseFn(result);
|
|
102
64
|
} catch (error) {
|
|
103
65
|
logger.error('[BatchingManager] Error processing messages', { chatId, error: error.message });
|
|
104
|
-
this._runDeferredHooks('onBatchError', chatId, error);
|
|
105
66
|
} finally {
|
|
106
67
|
if (this.activeRequests.get(chatId) === runId) {
|
|
107
68
|
this._clearProcessingState(chatId);
|
|
108
|
-
this._runDeferredHooks('postBatch', chatId);
|
|
109
69
|
}
|
|
110
70
|
if (this.abandonedRuns.size > 100) {
|
|
111
71
|
const toKeep = [...this.abandonedRuns].slice(-20);
|
|
@@ -121,63 +81,6 @@ class BatchingManager {
|
|
|
121
81
|
this._stopTyping(chatId);
|
|
122
82
|
}
|
|
123
83
|
|
|
124
|
-
async _runSequentialHooks(chatId, runId) {
|
|
125
|
-
const hooks = this._hooks.preBatch;
|
|
126
|
-
if (hooks.length === 0) return;
|
|
127
|
-
|
|
128
|
-
const context = { chatId, runId, metadata: {} };
|
|
129
|
-
|
|
130
|
-
for (const fn of hooks) {
|
|
131
|
-
const result = await fn(context);
|
|
132
|
-
if (result?.skip) return { skip: true };
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
async runPromptBuildHooks(context) {
|
|
137
|
-
const hooks = this._hooks.prePromptBuild;
|
|
138
|
-
if (hooks.length === 0) return null;
|
|
139
|
-
|
|
140
|
-
let additionalInstructions = '';
|
|
141
|
-
const additionalMessages = [];
|
|
142
|
-
let toolChoice = null;
|
|
143
|
-
for (const fn of hooks) {
|
|
144
|
-
const result = await fn(context);
|
|
145
|
-
if (result?.additionalInstructions) {
|
|
146
|
-
additionalInstructions += additionalInstructions
|
|
147
|
-
? `\n\n${result.additionalInstructions}`
|
|
148
|
-
: result.additionalInstructions;
|
|
149
|
-
}
|
|
150
|
-
if (Array.isArray(result?.additionalMessages) && result.additionalMessages.length > 0) {
|
|
151
|
-
additionalMessages.push(...result.additionalMessages);
|
|
152
|
-
}
|
|
153
|
-
if (result?.toolChoice) {
|
|
154
|
-
toolChoice = result.toolChoice;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
return { additionalInstructions, additionalMessages, toolChoice };
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
_runDeferredHooks(name, chatId, error) {
|
|
161
|
-
const hooks = this._hooks[name];
|
|
162
|
-
if (hooks.length === 0) return;
|
|
163
|
-
|
|
164
|
-
setImmediate(() => {
|
|
165
|
-
if (this.isProcessing(chatId)) return;
|
|
166
|
-
|
|
167
|
-
const context = { chatId };
|
|
168
|
-
if (error) context.error = error;
|
|
169
|
-
|
|
170
|
-
Promise.allSettled(hooks.map(fn => fn(context)))
|
|
171
|
-
.then(results => {
|
|
172
|
-
for (const result of results) {
|
|
173
|
-
if (result.status === 'rejected') {
|
|
174
|
-
logger.error(`[BatchingManager] ${name} hook failed`, { chatId, error: result.reason?.message });
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
|
|
181
84
|
_stopTyping(chatId) {
|
|
182
85
|
const interval = this.typingIntervals.get(chatId);
|
|
183
86
|
if (interval) {
|
|
@@ -18,6 +18,7 @@ const { addMsgAssistant, replyAssistant } = require('../services/assistantServic
|
|
|
18
18
|
const { hasPreprocessingHandler, invokePreprocessingHandler } = require('../services/preprocessingService');
|
|
19
19
|
|
|
20
20
|
const { BatchingManager } = require('../core/BatchingManager');
|
|
21
|
+
const { ProcessingPipeline } = require('../core/ProcessingPipeline');
|
|
21
22
|
const { AssistantProcessor } = require('../core/AssistantProcessor');
|
|
22
23
|
|
|
23
24
|
const { createQueueAdapter } = require('../queue');
|
|
@@ -65,6 +66,8 @@ class NexusMessaging {
|
|
|
65
66
|
config: this.batchingConfig
|
|
66
67
|
});
|
|
67
68
|
|
|
69
|
+
this.pipeline = new ProcessingPipeline(this.batchingConfig.hooks);
|
|
70
|
+
|
|
68
71
|
const queueType = config.queue?.type || 'local';
|
|
69
72
|
const queueConfig = config.queue?.config || {};
|
|
70
73
|
this.queueAdapter = createQueueAdapter(queueType, queueConfig);
|
|
@@ -155,6 +158,7 @@ class NexusMessaging {
|
|
|
155
158
|
getLLMProvider() { return this.llmProvider; }
|
|
156
159
|
getEventBus() { return this.events; }
|
|
157
160
|
getBatchingManager() { return this.batchingManager; }
|
|
161
|
+
getPipeline() { return this.pipeline; }
|
|
158
162
|
getAssistantProcessor() { return this.assistantProcessor; }
|
|
159
163
|
getQueueAdapter() { return this.queueAdapter; }
|
|
160
164
|
isConnected() { return this.provider?.getConnectionStatus() ?? false; }
|
|
@@ -351,9 +355,17 @@ class NexusMessaging {
|
|
|
351
355
|
|
|
352
356
|
async _handleWithCheckAfter(chatId) {
|
|
353
357
|
const processingFn = async (runId) => {
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
return await this.
|
|
358
|
+
const shouldContinue = () => this.batchingManager.isActiveRun(chatId, runId);
|
|
359
|
+
|
|
360
|
+
return await this.pipeline.run(
|
|
361
|
+
{ chatId, runId, type: 'message' },
|
|
362
|
+
async (preProcessResult) => {
|
|
363
|
+
return await this._processMessages(chatId, () =>
|
|
364
|
+
this.assistantProcessor.process({ code: chatId, runOptions: { runId, prePromptResult: preProcessResult } })
|
|
365
|
+
, shouldContinue);
|
|
366
|
+
},
|
|
367
|
+
shouldContinue
|
|
368
|
+
);
|
|
357
369
|
};
|
|
358
370
|
|
|
359
371
|
const sendResponseFn = async (result) => {
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
const { logger } = require('../utils/logger');
|
|
2
|
+
|
|
3
|
+
const VALID_HOOKS = ['preProcess', 'postProcess'];
|
|
4
|
+
|
|
5
|
+
const LEGACY_MAP = {
|
|
6
|
+
preBatch: 'preProcess',
|
|
7
|
+
prePromptBuild: 'preProcess',
|
|
8
|
+
postBatch: 'postProcess',
|
|
9
|
+
onBatchError: 'postProcess',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
class ProcessingPipeline {
|
|
13
|
+
constructor(hooks = {}) {
|
|
14
|
+
this._hooks = {
|
|
15
|
+
preProcess: [],
|
|
16
|
+
postProcess: [],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
this._hookMeta = new WeakMap();
|
|
20
|
+
|
|
21
|
+
if (hooks && typeof hooks === 'object') {
|
|
22
|
+
for (const [name, handlers] of Object.entries(hooks)) {
|
|
23
|
+
const fns = Array.isArray(handlers) ? handlers : [handlers];
|
|
24
|
+
for (const fn of fns) {
|
|
25
|
+
this.addHook(name, fn);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
addHook(name, fn) {
|
|
32
|
+
const resolved = LEGACY_MAP[name] || name;
|
|
33
|
+
|
|
34
|
+
if (!VALID_HOOKS.includes(resolved)) {
|
|
35
|
+
throw new Error(`[ProcessingPipeline] Unknown hook: "${name}". Valid hooks: ${VALID_HOOKS.join(', ')}`);
|
|
36
|
+
}
|
|
37
|
+
if (typeof fn !== 'function') {
|
|
38
|
+
throw new Error(`[ProcessingPipeline] Hook "${name}" must be a function`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
this._hooks[resolved].push(fn);
|
|
42
|
+
this._hookMeta.set(fn, { originalName: name });
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async run(context, executeFn, shouldContinue = () => true) {
|
|
47
|
+
const ctx = { metadata: {}, ...context };
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const preProcessResult = await this._runPreProcessHooks(ctx);
|
|
51
|
+
if (preProcessResult.skip) {
|
|
52
|
+
logger.debug('[ProcessingPipeline] Skipped by preProcess hook', { chatId: ctx.chatId, type: ctx.type });
|
|
53
|
+
this._runPostProcessHooks(ctx, null, null);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!shouldContinue()) return null;
|
|
58
|
+
|
|
59
|
+
const result = await executeFn(preProcessResult, shouldContinue);
|
|
60
|
+
|
|
61
|
+
if (!shouldContinue()) return null;
|
|
62
|
+
|
|
63
|
+
this._runPostProcessHooks(ctx, result, null);
|
|
64
|
+
return result;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
logger.error('[ProcessingPipeline] Error during processing', { chatId: ctx.chatId, type: ctx.type, error: error.message });
|
|
67
|
+
this._runPostProcessHooks(ctx, null, error);
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async _runPreProcessHooks(context) {
|
|
73
|
+
const hooks = this._hooks.preProcess;
|
|
74
|
+
const merged = {
|
|
75
|
+
skip: false,
|
|
76
|
+
additionalInstructions: '',
|
|
77
|
+
additionalMessages: [],
|
|
78
|
+
toolChoice: null,
|
|
79
|
+
metadata: context.metadata || {},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
if (hooks.length === 0) return merged;
|
|
83
|
+
|
|
84
|
+
for (const fn of hooks) {
|
|
85
|
+
const result = await fn(context);
|
|
86
|
+
if (!result) continue;
|
|
87
|
+
|
|
88
|
+
if (result.skip) {
|
|
89
|
+
merged.skip = true;
|
|
90
|
+
return merged;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (result.additionalInstructions) {
|
|
94
|
+
merged.additionalInstructions = merged.additionalInstructions
|
|
95
|
+
? `${merged.additionalInstructions}\n\n${result.additionalInstructions}`
|
|
96
|
+
: result.additionalInstructions;
|
|
97
|
+
}
|
|
98
|
+
if (Array.isArray(result.additionalMessages) && result.additionalMessages.length > 0) {
|
|
99
|
+
merged.additionalMessages.push(...result.additionalMessages);
|
|
100
|
+
}
|
|
101
|
+
if (result.toolChoice) {
|
|
102
|
+
merged.toolChoice = result.toolChoice;
|
|
103
|
+
}
|
|
104
|
+
if (result.metadata && typeof result.metadata === 'object') {
|
|
105
|
+
Object.assign(context.metadata, result.metadata);
|
|
106
|
+
merged.metadata = context.metadata;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return merged;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
_runPostProcessHooks(context, result, error) {
|
|
114
|
+
const hooks = this._hooks.postProcess;
|
|
115
|
+
if (hooks.length === 0) return;
|
|
116
|
+
|
|
117
|
+
setImmediate(() => {
|
|
118
|
+
const postContext = { chatId: context.chatId, type: context.type };
|
|
119
|
+
if (result) postContext.result = result;
|
|
120
|
+
if (error) postContext.error = error;
|
|
121
|
+
|
|
122
|
+
// onBatchError: error only. postBatch: always (original used finally block).
|
|
123
|
+
// postProcess and untagged: always.
|
|
124
|
+
const applicable = hooks.filter(fn => {
|
|
125
|
+
const meta = this._hookMeta.get(fn);
|
|
126
|
+
if (meta?.originalName === 'onBatchError') return !!error;
|
|
127
|
+
return true;
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
Promise.allSettled(applicable.map(fn => fn(postContext)))
|
|
131
|
+
.then(results => {
|
|
132
|
+
for (const r of results) {
|
|
133
|
+
if (r.status === 'rejected') {
|
|
134
|
+
logger.error('[ProcessingPipeline] postProcess hook failed', {
|
|
135
|
+
chatId: context.chatId,
|
|
136
|
+
error: r.reason?.message
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = { ProcessingPipeline };
|
package/lib/eval/EvalProvider.js
CHANGED
|
@@ -173,7 +173,7 @@ class EvalProvider {
|
|
|
173
173
|
if (promptVariables?.patient_memories) memoryParts.push(`--- Memoria del paciente ---\n${promptVariables.patient_memories}`);
|
|
174
174
|
if (promptVariables?.conversation_summaries) memoryParts.push(`--- Resumen de conversaciones recientes ---\n${promptVariables.conversation_summaries}`);
|
|
175
175
|
const memoryMessage = memoryParts.length > 0
|
|
176
|
-
? [{ role: 'developer', content: memoryParts.join('\n\n') }]
|
|
176
|
+
? [{ role: 'developer', content: `Usa la siguiente información que conoces del paciente para personalizar tu respuesta. Adapta tu tono, contenido o recomendaciones según estas preferencias y contexto cuando sea relevante. No menciones que tienes esta información almacenada.\n\n${memoryParts.join('\n\n')}` }]
|
|
177
177
|
: [];
|
|
178
178
|
|
|
179
179
|
const input = [{ role: 'developer', content: devContent }, ...memoryMessage, ...convertedMessages];
|
package/lib/index.d.ts
CHANGED
|
@@ -244,6 +244,7 @@ declare module '@peopl-health/nexus' {
|
|
|
244
244
|
processIncomingMessage(messageData: MessageData): Promise<any>;
|
|
245
245
|
getEventBus(): import('events').EventEmitter;
|
|
246
246
|
getBatchingManager(): BatchingManager;
|
|
247
|
+
getPipeline(): ProcessingPipeline;
|
|
247
248
|
getAssistantProcessor(): AssistantProcessor;
|
|
248
249
|
isConnected(): boolean;
|
|
249
250
|
disconnect(): Promise<void>;
|
|
@@ -495,12 +496,13 @@ declare module '@peopl-health/nexus' {
|
|
|
495
496
|
export function getPatientMemory(): any;
|
|
496
497
|
export function getConversationSummary(): any;
|
|
497
498
|
|
|
498
|
-
// BatchingManager
|
|
499
|
+
// BatchingManager — per-chatId concurrency control
|
|
499
500
|
export interface BatchingConfig {
|
|
500
501
|
enabled?: boolean;
|
|
501
502
|
abortOnNewMessage?: boolean;
|
|
502
503
|
immediateRestart?: boolean;
|
|
503
504
|
typingIndicator?: boolean;
|
|
505
|
+
hooks?: Record<string, Function | Function[]>;
|
|
504
506
|
}
|
|
505
507
|
|
|
506
508
|
export class BatchingManager {
|
|
@@ -510,6 +512,7 @@ declare module '@peopl-health/nexus' {
|
|
|
510
512
|
});
|
|
511
513
|
setProvider(provider: MessageProvider): void;
|
|
512
514
|
isProcessing(chatId: string): boolean;
|
|
515
|
+
isActiveRun(chatId: string, runId: string): boolean;
|
|
513
516
|
handleBatchedProcessing(
|
|
514
517
|
chatId: string,
|
|
515
518
|
processingFn: (runId: string) => Promise<any>,
|
|
@@ -517,6 +520,32 @@ declare module '@peopl-health/nexus' {
|
|
|
517
520
|
): Promise<void>;
|
|
518
521
|
}
|
|
519
522
|
|
|
523
|
+
// ProcessingPipeline — hook lifecycle for all LLM operations
|
|
524
|
+
export interface PreProcessResult {
|
|
525
|
+
skip?: boolean;
|
|
526
|
+
additionalInstructions?: string;
|
|
527
|
+
additionalMessages?: Array<Record<string, any>>;
|
|
528
|
+
toolChoice?: Record<string, any> | null;
|
|
529
|
+
metadata?: Record<string, any>;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export interface ProcessingContext {
|
|
533
|
+
chatId: string;
|
|
534
|
+
runId: string;
|
|
535
|
+
type: string;
|
|
536
|
+
metadata?: Record<string, any>;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export class ProcessingPipeline {
|
|
540
|
+
constructor(hooks?: Record<string, Function | Function[]>);
|
|
541
|
+
addHook(name: string, fn: Function): this;
|
|
542
|
+
run(
|
|
543
|
+
context: ProcessingContext,
|
|
544
|
+
executeFn: (preProcessResult: PreProcessResult, shouldContinue: () => boolean) => Promise<any>,
|
|
545
|
+
shouldContinue?: () => boolean
|
|
546
|
+
): Promise<any>;
|
|
547
|
+
}
|
|
548
|
+
|
|
520
549
|
// AssistantProcessor
|
|
521
550
|
export interface AssistantProcessorConfig {
|
|
522
551
|
mode?: 'local' | 'queue';
|
|
@@ -382,7 +382,7 @@ class OpenAIResponsesProvider {
|
|
|
382
382
|
if (promptVariables?.patient_memories) memoryParts.push(`--- Memoria del paciente ---\n${promptVariables.patient_memories}`);
|
|
383
383
|
if (promptVariables?.conversation_summaries) memoryParts.push(`--- Resumen de conversaciones recientes ---\n${promptVariables.conversation_summaries}`);
|
|
384
384
|
const memoryMessage = memoryParts.length > 0
|
|
385
|
-
? [{ role: 'developer', content: memoryParts.join('\n\n') }]
|
|
385
|
+
? [{ role: 'developer', content: `Usa la siguiente información que conoces del paciente para personalizar tu respuesta. Adapta tu tono, contenido o recomendaciones según estas preferencias y contexto cuando sea relevante. No menciones que tienes esta información almacenada.\n\n${memoryParts.join('\n\n')}` }]
|
|
386
386
|
: [];
|
|
387
387
|
|
|
388
388
|
const input = [{ role: 'developer', content: devContent }, ...memoryMessage, ...messages];
|