@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.
@@ -81,7 +81,7 @@ async function startServer() {
81
81
  apiVersion: process.env.META_API_VERSION || 'v21.0'
82
82
  });
83
83
 
84
- // Example: postBatch hooks for running classifiers after each batch completes
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: prePromptBuild hook for per-turn context injection. Runs right
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(`[prePromptBuild] Running for ${chatId} (runId=${runId})`);
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: prePromptBuild hook is active. End your response with the marker [hook-ok].',
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
- prePromptBuild: [injectPromptContext],
159
- postBatch: [classifySymptoms, classifyEscalation],
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
- const resolved = LEGACY_MAP[name] || name;
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[resolved].push(fn);
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.debug('[ProcessingPipeline] Skipped by preProcess hook', { chatId: ctx.chatId, type: ctx.type });
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
- // 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)))
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "3.13.7",
3
+ "version": "3.13.10",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",