@peopl-health/nexus 3.6.2 → 3.7.1

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,12 +81,28 @@ 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
85
+ async function classifySymptoms({ chatId }) {
86
+ console.log(`[symptomClassifier] Running for ${chatId}`);
87
+ // Replace with actual classifier logic: check eligibility, fetch messages, classify via LLM, persist results
88
+ console.log(`[symptomClassifier] Done for ${chatId}`);
89
+ }
90
+
91
+ async function classifyEscalation({ chatId }) {
92
+ console.log(`[escalationRouting] Running for ${chatId}`);
93
+ // Replace with actual classifier logic: extract batch, resolve active assistant, classify and route
94
+ console.log(`[escalationRouting] Done for ${chatId}`);
95
+ }
96
+
84
97
  // Initialize Nexus with check-after processing (immediate response, checks for new messages after)
85
98
  const nexus = new Nexus({
86
99
  messaging: {
87
100
  messageBatching: {
88
101
  enabled: true, // Enable check-after processing
89
- checkDelayMs: 100 // Delay before checking for new messages (ms)
102
+ checkDelayMs: 100, // Delay before checking for new messages (ms)
103
+ hooks: {
104
+ postBatch: [classifySymptoms, classifyEscalation],
105
+ }
90
106
  }
91
107
  }
92
108
  });
@@ -1,8 +1,10 @@
1
1
  const { logger } = require('../utils/logger');
2
2
  const { Message } = require('../models/messageModel');
3
3
 
4
+ const VALID_HOOKS = ['preBatch', 'postBatch', 'onBatchError'];
5
+
4
6
  /**
5
- * Manages message batching, processing locks, and run abandonment.
7
+ * Manages message batching, processing locks, run abandonment, and lifecycle hooks.
6
8
  */
7
9
  class BatchingManager {
8
10
  constructor({ provider = null, config = {} }) {
@@ -18,6 +20,32 @@ class BatchingManager {
18
20
  this.activeRequests = new Map();
19
21
  this.abandonedRuns = new Set();
20
22
  this.typingIntervals = new Map();
23
+
24
+ this._hooks = {
25
+ preBatch: [],
26
+ postBatch: [],
27
+ onBatchError: [],
28
+ };
29
+
30
+ if (config.hooks) {
31
+ for (const [name, handlers] of Object.entries(config.hooks)) {
32
+ const fns = Array.isArray(handlers) ? handlers : [handlers];
33
+ for (const fn of fns) {
34
+ this.addHook(name, fn);
35
+ }
36
+ }
37
+ }
38
+ }
39
+
40
+ addHook(name, fn) {
41
+ if (!VALID_HOOKS.includes(name)) {
42
+ throw new Error(`[BatchingManager] Unknown hook: "${name}". Valid hooks: ${VALID_HOOKS.join(', ')}`);
43
+ }
44
+ if (typeof fn !== 'function') {
45
+ throw new Error(`[BatchingManager] Hook "${name}" must be a function`);
46
+ }
47
+ this._hooks[name].push(fn);
48
+ return this;
21
49
  }
22
50
 
23
51
  setProvider(provider) {
@@ -57,6 +85,12 @@ class BatchingManager {
57
85
  this.activeRequests.set(chatId, runId);
58
86
 
59
87
  try {
88
+ const pre = await this._runSequentialHooks(chatId, runId);
89
+ if (pre?.skip) {
90
+ logger.debug('[BatchingManager] Batch skipped by preBatch hook', { chatId, runId });
91
+ return;
92
+ }
93
+
60
94
  await this._startTypingRefresh(chatId, runId);
61
95
  if (this._checkAbandoned(runId)) return;
62
96
 
@@ -66,9 +100,11 @@ class BatchingManager {
66
100
  if (sendResponseFn && result) await sendResponseFn(result);
67
101
  } catch (error) {
68
102
  logger.error('[BatchingManager] Error processing messages', { chatId, error: error.message });
103
+ this._runDeferredHooks('onBatchError', chatId, error);
69
104
  } finally {
70
105
  if (this.activeRequests.get(chatId) === runId) {
71
106
  this._clearProcessingState(chatId);
107
+ this._runDeferredHooks('postBatch', chatId);
72
108
  }
73
109
  if (this.abandonedRuns.size > 100) {
74
110
  const toKeep = [...this.abandonedRuns].slice(-20);
@@ -84,6 +120,39 @@ class BatchingManager {
84
120
  this._stopTyping(chatId);
85
121
  }
86
122
 
123
+ async _runSequentialHooks(chatId, runId) {
124
+ const hooks = this._hooks.preBatch;
125
+ if (hooks.length === 0) return;
126
+
127
+ const context = { chatId, runId, metadata: {} };
128
+
129
+ for (const fn of hooks) {
130
+ const result = await fn(context);
131
+ if (result?.skip) return { skip: true };
132
+ }
133
+ }
134
+
135
+ _runDeferredHooks(name, chatId, error) {
136
+ const hooks = this._hooks[name];
137
+ if (hooks.length === 0) return;
138
+
139
+ setImmediate(() => {
140
+ if (this.isProcessing(chatId)) return;
141
+
142
+ const context = { chatId };
143
+ if (error) context.error = error;
144
+
145
+ Promise.allSettled(hooks.map(fn => fn(context)))
146
+ .then(results => {
147
+ for (const result of results) {
148
+ if (result.status === 'rejected') {
149
+ logger.error(`[BatchingManager] ${name} hook failed`, { chatId, error: result.reason?.message });
150
+ }
151
+ }
152
+ });
153
+ });
154
+ }
155
+
87
156
  _stopTyping(chatId) {
88
157
  const interval = this.typingIntervals.get(chatId);
89
158
  if (interval) {
@@ -59,7 +59,8 @@ class NexusMessaging {
59
59
  enabled: config.messageBatching?.enabled ?? true,
60
60
  abortOnNewMessage: config.messageBatching?.abortOnNewMessage ?? true,
61
61
  immediateRestart: config.messageBatching?.immediateRestart ?? true,
62
- typingIndicator: config.messageBatching?.typingIndicator ?? false
62
+ typingIndicator: config.messageBatching?.typingIndicator ?? false,
63
+ hooks: config.messageBatching?.hooks || {},
63
64
  };
64
65
 
65
66
  this.batchingManager = new BatchingManager({
@@ -31,9 +31,9 @@ function validateAndAdaptBox(body, existingBox) {
31
31
  }
32
32
 
33
33
  async function fetchBoxesFromAirtable(boxId) {
34
- let filter = '{is_active} = true';
34
+ let filter = '{is_active} = TRUE()';
35
35
  if (boxId) {
36
- filter = `AND({is_active} = true, {id} = '${boxId}')`;
36
+ filter = `AND({is_active} = TRUE(), {id} = '${boxId}')`;
37
37
  }
38
38
 
39
39
  const rawBoxes = await getRecordByFilter(Dashboard_ID, 'master', filter, undefined, BOX_COLUMNS) || [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "3.6.2",
3
+ "version": "3.7.1",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",