@peopl-health/nexus 3.13.7 → 3.13.10
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/examples/basic-usage.js +6 -6
- package/lib/controllers/assistantController.js +4 -4
- package/lib/controllers/conversationController.js +8 -6
- package/lib/controllers/messageController.js +13 -7
- package/lib/core/NexusMessaging.js +10 -8
- package/lib/core/ProcessingPipeline.js +18 -24
- package/lib/models/messageModel.js +4 -1
- package/lib/services/conversationService.js +2 -2
- package/lib/storage/MongoStorage.js +2 -1
- package/package.json +1 -1
package/examples/basic-usage.js
CHANGED
|
@@ -81,7 +81,7 @@ async function startServer() {
|
|
|
81
81
|
apiVersion: process.env.META_API_VERSION || 'v21.0'
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
-
// Example:
|
|
84
|
+
// Example: postProcess hooks for running classifiers after each batch completes
|
|
85
85
|
async function classifySymptoms({ chatId }) {
|
|
86
86
|
console.log(`[symptomClassifier] Running for ${chatId}`);
|
|
87
87
|
// Replace with actual classifier logic: check eligibility, fetch messages, classify via LLM, persist results
|
|
@@ -94,7 +94,7 @@ async function startServer() {
|
|
|
94
94
|
console.log(`[escalationRouting] Done for ${chatId}`);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
// Example:
|
|
97
|
+
// Example: preProcess hook for per-turn context injection. Runs right
|
|
98
98
|
// before the assistant prompt is sent to the model. Use this to inject
|
|
99
99
|
// per-turn instructions, escalation acknowledgments, runtime feature flags, etc.
|
|
100
100
|
// Return { additionalInstructions, additionalMessages } to inject context,
|
|
@@ -104,7 +104,7 @@ async function startServer() {
|
|
|
104
104
|
// salience option (developer message at the end of history). To test the
|
|
105
105
|
// others, comment the active `return` and uncomment one of the alternatives.
|
|
106
106
|
async function injectPromptContext({ chatId, runId }) {
|
|
107
|
-
console.log(`[
|
|
107
|
+
console.log(`[preProcess] Running for ${chatId} (runId=${runId})`);
|
|
108
108
|
// Replace with your own lookup — e.g. query a pending escalations store,
|
|
109
109
|
// check feature flags, fetch session state, etc.
|
|
110
110
|
|
|
@@ -114,7 +114,7 @@ async function startServer() {
|
|
|
114
114
|
//return {
|
|
115
115
|
//additionalMessages: [{
|
|
116
116
|
//role: 'developer',
|
|
117
|
-
//content: 'TEST:
|
|
117
|
+
//content: 'TEST: preProcess hook is active. End your response with the marker [hook-ok].',
|
|
118
118
|
//}],
|
|
119
119
|
//};
|
|
120
120
|
|
|
@@ -155,8 +155,8 @@ async function startServer() {
|
|
|
155
155
|
enabled: true, // Enable check-after processing
|
|
156
156
|
checkDelayMs: 100, // Delay before checking for new messages (ms)
|
|
157
157
|
hooks: {
|
|
158
|
-
|
|
159
|
-
|
|
158
|
+
preProcess: [injectPromptContext],
|
|
159
|
+
postProcess: [classifySymptoms, classifyEscalation],
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
162
|
}
|
|
@@ -33,11 +33,11 @@ const stopAssistantController = (req, res) =>
|
|
|
33
33
|
_updateThreadFlag(req, res, 'stop', 'Assistant stopped', 'Failed to stop assistant');
|
|
34
34
|
|
|
35
35
|
const addInsAssistantController = async (req, res) => {
|
|
36
|
-
const { code, instruction } = req.body;
|
|
36
|
+
const { code, instruction, triggeredBy } = req.body;
|
|
37
37
|
if (!code) return res.status(400).json({ success: false, error: 'Code is required' });
|
|
38
38
|
|
|
39
39
|
try {
|
|
40
|
-
await processInstruction(code, instruction, 'developer');
|
|
40
|
+
await processInstruction(code, instruction, 'developer', { triggeredBy });
|
|
41
41
|
return res.status(200).json({ success: true, message: 'Instruction added to assistant' });
|
|
42
42
|
} catch (error) {
|
|
43
43
|
logger.error('[AssistantController] Add instruction error', { error: error.message, code });
|
|
@@ -46,12 +46,12 @@ const addInsAssistantController = async (req, res) => {
|
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
const addMsgAssistantController = async (req, res) => {
|
|
49
|
-
const { code, messages, role = 'system', reply = false } = req.body;
|
|
49
|
+
const { code, messages, role = 'system', reply = false, triggeredBy } = req.body;
|
|
50
50
|
if (!code) return res.status(400).json({ success: false, error: 'Code is required' });
|
|
51
51
|
|
|
52
52
|
try {
|
|
53
53
|
if (reply) {
|
|
54
|
-
await processSystemMessage(code, messages, role);
|
|
54
|
+
await processSystemMessage(code, messages, role, { triggeredBy });
|
|
55
55
|
} else {
|
|
56
56
|
await addMsgAssistant(code, messages, role);
|
|
57
57
|
}
|
|
@@ -116,7 +116,7 @@ const getConversationMessagesController = async (req, res) => {
|
|
|
116
116
|
|
|
117
117
|
const getConversationReplyController = async (req, res) => {
|
|
118
118
|
try {
|
|
119
|
-
const { phoneNumber, message, mediaData, contentSid, variables } = req.body;
|
|
119
|
+
const { phoneNumber, message, mediaData, contentSid, variables, triggeredBy } = req.body;
|
|
120
120
|
|
|
121
121
|
if (!phoneNumber || (!message && !mediaData && !contentSid)) {
|
|
122
122
|
return res.status(400).json({
|
|
@@ -130,7 +130,8 @@ const getConversationReplyController = async (req, res) => {
|
|
|
130
130
|
const messageData = {
|
|
131
131
|
code: formattedPhoneNumber,
|
|
132
132
|
fileType: 'text',
|
|
133
|
-
_fromConversationReply: true
|
|
133
|
+
_fromConversationReply: true,
|
|
134
|
+
triggeredBy: triggeredBy || null
|
|
134
135
|
};
|
|
135
136
|
|
|
136
137
|
if (contentSid) {
|
|
@@ -460,7 +461,7 @@ const markMessagesAsReadController = async (req, res) => {
|
|
|
460
461
|
|
|
461
462
|
const sendTemplateToNewNumberController = async (req, res) => {
|
|
462
463
|
try {
|
|
463
|
-
const { phoneNumber, templateId, variables } = req.body;
|
|
464
|
+
const { phoneNumber, templateId, variables, triggeredBy } = req.body;
|
|
464
465
|
|
|
465
466
|
if (!phoneNumber || !templateId) {
|
|
466
467
|
return res.status(400).json({
|
|
@@ -471,7 +472,8 @@ const sendTemplateToNewNumberController = async (req, res) => {
|
|
|
471
472
|
|
|
472
473
|
const messageData = {
|
|
473
474
|
code: ensureWhatsAppFormat(phoneNumber),
|
|
474
|
-
contentSid: templateId
|
|
475
|
+
contentSid: templateId,
|
|
476
|
+
triggeredBy: triggeredBy || null
|
|
475
477
|
};
|
|
476
478
|
|
|
477
479
|
if (variables && Object.keys(variables).length > 0) {
|
|
@@ -621,7 +623,7 @@ const searchMessagesByNumberController = async (req, res) => {
|
|
|
621
623
|
|
|
622
624
|
const startConversationController = async (req, res) => {
|
|
623
625
|
try {
|
|
624
|
-
const { phoneNumber, name, message } = req.body;
|
|
626
|
+
const { phoneNumber, name, message, triggeredBy } = req.body;
|
|
625
627
|
|
|
626
628
|
if (!phoneNumber || !message) {
|
|
627
629
|
return res.status(400).json({
|
|
@@ -631,7 +633,7 @@ const startConversationController = async (req, res) => {
|
|
|
631
633
|
}
|
|
632
634
|
|
|
633
635
|
const formattedPhoneNumber = ensureWhatsAppFormat(phoneNumber);
|
|
634
|
-
const result = await startConversation(formattedPhoneNumber, message, name);
|
|
636
|
+
const result = await startConversation(formattedPhoneNumber, message, name, { triggeredBy });
|
|
635
637
|
|
|
636
638
|
res.status(201).json({
|
|
637
639
|
success: true,
|
|
@@ -83,7 +83,8 @@ const sendMessageController = async (req, res) => {
|
|
|
83
83
|
hidePreview = false,
|
|
84
84
|
contentSid = null,
|
|
85
85
|
variables = null,
|
|
86
|
-
frontendId = null
|
|
86
|
+
frontendId = null,
|
|
87
|
+
triggeredBy = null
|
|
87
88
|
} = req.body || {};
|
|
88
89
|
|
|
89
90
|
try {
|
|
@@ -96,7 +97,8 @@ const sendMessageController = async (req, res) => {
|
|
|
96
97
|
code: ensureWhatsAppFormat(code),
|
|
97
98
|
author: runtimeConfig.get('USER_DB_MONGO'),
|
|
98
99
|
extraDelay: 0,
|
|
99
|
-
frontendId
|
|
100
|
+
frontendId,
|
|
101
|
+
triggeredBy
|
|
100
102
|
};
|
|
101
103
|
|
|
102
104
|
const { result, saved } = await _sendAndPersist(payload);
|
|
@@ -120,7 +122,8 @@ const sendBulkMessageController = async (req, res) => {
|
|
|
120
122
|
timeZone = 'Etc/GMT',
|
|
121
123
|
hidePreview = false,
|
|
122
124
|
contentSid = null,
|
|
123
|
-
variables = null
|
|
125
|
+
variables = null,
|
|
126
|
+
triggeredBy = null
|
|
124
127
|
} = req.body || {};
|
|
125
128
|
const author = runtimeConfig.get('USER_DB_MONGO');
|
|
126
129
|
const sendMoment = sendTime ? moment.tz(sendTime, timeZone) + 20000 : new Date();
|
|
@@ -138,7 +141,8 @@ const sendBulkMessageController = async (req, res) => {
|
|
|
138
141
|
sendTime: new Date(sendMoment + extraDelay),
|
|
139
142
|
code: ensureWhatsAppFormat(recipient),
|
|
140
143
|
author,
|
|
141
|
-
extraDelay: extraDelay += Math.floor(Math.random() * 5001) + 5000
|
|
144
|
+
extraDelay: extraDelay += Math.floor(Math.random() * 5001) + 5000,
|
|
145
|
+
triggeredBy
|
|
142
146
|
});
|
|
143
147
|
}
|
|
144
148
|
|
|
@@ -160,7 +164,8 @@ const sendBulkMessageAirtableController = async (req, res) => {
|
|
|
160
164
|
hidePreview = false,
|
|
161
165
|
contentSid = null,
|
|
162
166
|
condition = '1',
|
|
163
|
-
variables = null
|
|
167
|
+
variables = null,
|
|
168
|
+
triggeredBy = null
|
|
164
169
|
} = req.body || {};
|
|
165
170
|
const author = runtimeConfig.get('USER_DB_MONGO');
|
|
166
171
|
const sendMoment = sendTime ? moment.tz(sendTime, timeZone) + 20000 : new Date();
|
|
@@ -189,12 +194,13 @@ const sendBulkMessageAirtableController = async (req, res) => {
|
|
|
189
194
|
|
|
190
195
|
payloads.push({
|
|
191
196
|
fileUrl, message: customMessage, fileType, contentSid, hidePreview,
|
|
192
|
-
variables: isFlowTemplate
|
|
197
|
+
variables: isFlowTemplate
|
|
193
198
|
? await ensureFlowTokenInVariables(contentSid, variables)
|
|
194
199
|
: variables,
|
|
195
200
|
timeZone: timeZone || null,
|
|
196
201
|
sendTime: new Date(sendMoment + extraDelay),
|
|
197
|
-
code, author
|
|
202
|
+
code, author,
|
|
203
|
+
triggeredBy
|
|
198
204
|
});
|
|
199
205
|
extraDelay += Math.floor(Math.random() * 5001) + 5000;
|
|
200
206
|
}
|
|
@@ -401,7 +401,7 @@ class NexusMessaging {
|
|
|
401
401
|
);
|
|
402
402
|
}
|
|
403
403
|
|
|
404
|
-
async processInstruction(code, instruction, role = 'developer') {
|
|
404
|
+
async processInstruction(code, instruction, role = 'developer', { triggeredBy } = {}) {
|
|
405
405
|
const assistantId = await this._getThreadAssistantId(code);
|
|
406
406
|
await insertMessage({
|
|
407
407
|
nombre_whatsapp: 'Instruction',
|
|
@@ -412,7 +412,8 @@ class NexusMessaging {
|
|
|
412
412
|
processed: true,
|
|
413
413
|
origin: 'instruction',
|
|
414
414
|
assistant_id: assistantId,
|
|
415
|
-
raw: { role }
|
|
415
|
+
raw: { role },
|
|
416
|
+
triggeredBy: triggeredBy || null
|
|
416
417
|
});
|
|
417
418
|
|
|
418
419
|
const result = await this._executeWithPipeline(code, 'instruction', 'queue',
|
|
@@ -432,7 +433,7 @@ class NexusMessaging {
|
|
|
432
433
|
return result?.output || null;
|
|
433
434
|
}
|
|
434
435
|
|
|
435
|
-
async processSystemMessage(code, messages, role = 'system') {
|
|
436
|
+
async processSystemMessage(code, messages, role = 'system', { triggeredBy } = {}) {
|
|
436
437
|
const normalizedMessages = Array.isArray(messages) ? messages : [messages];
|
|
437
438
|
const assistantId = await this._getThreadAssistantId(code);
|
|
438
439
|
|
|
@@ -446,7 +447,8 @@ class NexusMessaging {
|
|
|
446
447
|
processed: true,
|
|
447
448
|
origin: 'system',
|
|
448
449
|
assistant_id: assistantId,
|
|
449
|
-
raw: { role }
|
|
450
|
+
raw: { role },
|
|
451
|
+
triggeredBy: triggeredBy || null
|
|
450
452
|
});
|
|
451
453
|
}
|
|
452
454
|
|
|
@@ -544,12 +546,12 @@ const sendScheduledMessage = async (scheduledMessage) => {
|
|
|
544
546
|
return await requireDefaultInstance().sendScheduledMessage(scheduledMessage);
|
|
545
547
|
};
|
|
546
548
|
|
|
547
|
-
const processInstruction = async (code, instruction, role) => {
|
|
548
|
-
return await requireDefaultInstance().processInstruction(code, instruction, role);
|
|
549
|
+
const processInstruction = async (code, instruction, role, options) => {
|
|
550
|
+
return await requireDefaultInstance().processInstruction(code, instruction, role, options);
|
|
549
551
|
};
|
|
550
552
|
|
|
551
|
-
const processSystemMessage = async (code, messages, role) => {
|
|
552
|
-
return await requireDefaultInstance().processSystemMessage(code, messages, role);
|
|
553
|
+
const processSystemMessage = async (code, messages, role, options) => {
|
|
554
|
+
return await requireDefaultInstance().processSystemMessage(code, messages, role, options);
|
|
553
555
|
};
|
|
554
556
|
|
|
555
557
|
const getEventBus = () => getDefaultInstance()?.getEventBus();
|
|
@@ -2,13 +2,6 @@ const { logger } = require('../utils/logger');
|
|
|
2
2
|
|
|
3
3
|
const VALID_HOOKS = ['preProcess', 'postProcess'];
|
|
4
4
|
|
|
5
|
-
const LEGACY_MAP = {
|
|
6
|
-
preBatch: 'preProcess',
|
|
7
|
-
prePromptBuild: 'preProcess',
|
|
8
|
-
postBatch: 'postProcess',
|
|
9
|
-
onBatchError: 'postProcess',
|
|
10
|
-
};
|
|
11
|
-
|
|
12
5
|
class ProcessingPipeline {
|
|
13
6
|
constructor(hooks = {}) {
|
|
14
7
|
this._hooks = {
|
|
@@ -16,8 +9,6 @@ class ProcessingPipeline {
|
|
|
16
9
|
postProcess: [],
|
|
17
10
|
};
|
|
18
11
|
|
|
19
|
-
this._hookMeta = new WeakMap();
|
|
20
|
-
|
|
21
12
|
if (hooks && typeof hooks === 'object') {
|
|
22
13
|
for (const [name, handlers] of Object.entries(hooks)) {
|
|
23
14
|
const fns = Array.isArray(handlers) ? handlers : [handlers];
|
|
@@ -29,27 +20,38 @@ class ProcessingPipeline {
|
|
|
29
20
|
}
|
|
30
21
|
|
|
31
22
|
addHook(name, fn) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (!VALID_HOOKS.includes(resolved)) {
|
|
23
|
+
if (!VALID_HOOKS.includes(name)) {
|
|
35
24
|
throw new Error(`[ProcessingPipeline] Unknown hook: "${name}". Valid hooks: ${VALID_HOOKS.join(', ')}`);
|
|
36
25
|
}
|
|
37
26
|
if (typeof fn !== 'function') {
|
|
38
27
|
throw new Error(`[ProcessingPipeline] Hook "${name}" must be a function`);
|
|
39
28
|
}
|
|
40
29
|
|
|
41
|
-
this._hooks[
|
|
42
|
-
this._hookMeta.set(fn, { originalName: name });
|
|
30
|
+
this._hooks[name].push(fn);
|
|
43
31
|
return this;
|
|
44
32
|
}
|
|
45
33
|
|
|
46
34
|
async run(context, executeFn, shouldContinue = () => true) {
|
|
47
35
|
const ctx = { metadata: {}, ...context };
|
|
36
|
+
const preProcessCount = this._hooks.preProcess.length;
|
|
37
|
+
const postProcessCount = this._hooks.postProcess.length;
|
|
38
|
+
|
|
39
|
+
logger.info('[ProcessingPipeline] Running', { chatId: ctx.chatId, type: ctx.type, preProcessHooks: preProcessCount, postProcessHooks: postProcessCount });
|
|
48
40
|
|
|
49
41
|
try {
|
|
50
42
|
const preProcessResult = await this._runPreProcessHooks(ctx);
|
|
43
|
+
|
|
44
|
+
logger.debug('[ProcessingPipeline] preProcess complete', {
|
|
45
|
+
chatId: ctx.chatId,
|
|
46
|
+
type: ctx.type,
|
|
47
|
+
skip: preProcessResult.skip,
|
|
48
|
+
hasAdditionalInstructions: !!preProcessResult.additionalInstructions,
|
|
49
|
+
additionalMessagesCount: preProcessResult.additionalMessages.length,
|
|
50
|
+
hasToolChoice: preProcessResult.toolChoice != null,
|
|
51
|
+
});
|
|
52
|
+
|
|
51
53
|
if (preProcessResult.skip) {
|
|
52
|
-
logger.
|
|
54
|
+
logger.info('[ProcessingPipeline] Skipped by preProcess hook', { chatId: ctx.chatId, type: ctx.type });
|
|
53
55
|
this._runPostProcessHooks(ctx, null, null);
|
|
54
56
|
return null;
|
|
55
57
|
}
|
|
@@ -119,15 +121,7 @@ class ProcessingPipeline {
|
|
|
119
121
|
if (result) postContext.result = result;
|
|
120
122
|
if (error) postContext.error = error;
|
|
121
123
|
|
|
122
|
-
|
|
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)))
|
|
124
|
+
Promise.allSettled(hooks.map(fn => fn(postContext)))
|
|
131
125
|
.then(results => {
|
|
132
126
|
for (const r of results) {
|
|
133
127
|
if (r.status === 'rejected') {
|
|
@@ -88,7 +88,8 @@ const messageSchema = new mongoose.Schema({
|
|
|
88
88
|
},
|
|
89
89
|
prompt: { type: Object, default: null },
|
|
90
90
|
preset: { type: Object, default: null },
|
|
91
|
-
response_id: { type: String, default: null }
|
|
91
|
+
response_id: { type: String, default: null },
|
|
92
|
+
triggeredBy: { type: String, default: null }
|
|
92
93
|
}, { timestamps: true });
|
|
93
94
|
|
|
94
95
|
messageSchema.index({ numero: 1, createdAt: -1 });
|
|
@@ -105,6 +106,8 @@ messageSchema.index({ createdAt: -1 }, { name: 'global_sort_idx' });
|
|
|
105
106
|
|
|
106
107
|
messageSchema.index({ 'statusInfo.recoveryMessageId': 1 }, { name: 'recovery_message_id_idx', sparse: true });
|
|
107
108
|
|
|
109
|
+
messageSchema.index({ triggeredBy: 1, createdAt: -1 }, { name: 'triggered_by_idx', sparse: true });
|
|
110
|
+
|
|
108
111
|
messageSchema.pre('save', function (next) {
|
|
109
112
|
if (this.timestamp) {
|
|
110
113
|
this.timestamp = moment.tz(this.timestamp, 'America/Mexico_City').toDate();
|
|
@@ -130,7 +130,7 @@ const processConversations = async (threads, airtableNameMap) => {
|
|
|
130
130
|
}
|
|
131
131
|
};
|
|
132
132
|
|
|
133
|
-
const startConversation = async (phoneNumber, message, name) => {
|
|
133
|
+
const startConversation = async (phoneNumber, message, name, { triggeredBy } = {}) => {
|
|
134
134
|
|
|
135
135
|
const existing = await Thread.findOne({ code: phoneNumber });
|
|
136
136
|
if (existing) {
|
|
@@ -200,7 +200,7 @@ const startConversation = async (phoneNumber, message, name) => {
|
|
|
200
200
|
{ upsert: true, new: true }
|
|
201
201
|
);
|
|
202
202
|
logger.info('[StartConversation] Thread ready', { phoneNumber, threadId: thread._id });
|
|
203
|
-
await sendMessage({ code: phoneNumber, contentSid: twilioContent.sid, variables: {} });
|
|
203
|
+
await sendMessage({ code: phoneNumber, contentSid: twilioContent.sid, variables: {}, triggeredBy: triggeredBy || null });
|
|
204
204
|
logger.info('[StartConversation] Template sent successfully', { phoneNumber, templateSid: twilioContent.sid });
|
|
205
205
|
await TemplateModel.updateOne({ sid: twilioContent.sid }, { status: 'APPROVED' });
|
|
206
206
|
try {
|
|
@@ -91,7 +91,8 @@ class MongoStorage {
|
|
|
91
91
|
preset: messageData.preset || null,
|
|
92
92
|
response_id: messageData.response_id || null,
|
|
93
93
|
statusInfo: messageData.statusInfo || null,
|
|
94
|
-
clinical_context: messageData.clinicalContext || null
|
|
94
|
+
clinical_context: messageData.clinicalContext || null,
|
|
95
|
+
triggeredBy: messageData.triggeredBy || null
|
|
95
96
|
};
|
|
96
97
|
return values;
|
|
97
98
|
}
|