@peopl-health/nexus 4.4.4 → 4.5.21

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.
Files changed (45) hide show
  1. package/README.md +4 -9
  2. package/lib/adapters/BaileysProvider.js +4 -2
  3. package/lib/adapters/MessageProvider.js +2 -2
  4. package/lib/adapters/TwilioProvider.js +7 -3
  5. package/lib/controllers/assistantController.js +2 -6
  6. package/lib/controllers/bugReportController.js +2 -2
  7. package/lib/controllers/conversationController.js +13 -13
  8. package/lib/controllers/interactionController.js +2 -2
  9. package/lib/controllers/messageController.js +6 -5
  10. package/lib/controllers/qualityMessageController.js +3 -2
  11. package/lib/core/AssistantProcessor.js +3 -3
  12. package/lib/core/BatchingManager.js +6 -5
  13. package/lib/core/NexusMessaging.js +230 -155
  14. package/lib/core/PhiProcessor.js +113 -0
  15. package/lib/eval/EvalProvider.js +6 -1
  16. package/lib/helpers/baileysHelper.js +3 -1
  17. package/lib/helpers/conversationWindowHelper.js +4 -4
  18. package/lib/helpers/deliveryAttemptHelper.js +3 -1
  19. package/lib/helpers/filesHelper.js +2 -5
  20. package/lib/helpers/messageHelper.js +10 -71
  21. package/lib/helpers/messageStatusHelper.js +3 -3
  22. package/lib/helpers/nerHelper.js +64 -0
  23. package/lib/helpers/templateRecoveryHelper.js +2 -0
  24. package/lib/index.d.ts +16 -1
  25. package/lib/jobs/ScheduledMessageJob.js +15 -23
  26. package/lib/jobs/TemplateApprovalJob.js +4 -1
  27. package/lib/memory/DefaultMemoryManager.js +5 -5
  28. package/lib/memory/SessionManager.js +3 -6
  29. package/lib/models/deliveryAttemptModel.js +1 -1
  30. package/lib/models/globalEntityMapModel.js +27 -0
  31. package/lib/models/messageModel.js +0 -94
  32. package/lib/models/tokenMapModel.js +28 -0
  33. package/lib/providers/NerClient.js +43 -0
  34. package/lib/providers/OpenAIResponsesProvider.js +9 -7
  35. package/lib/providers/OpenAIResponsesProviderTools.js +26 -9
  36. package/lib/services/assistantService.js +20 -11
  37. package/lib/services/dashboardService.js +4 -4
  38. package/lib/services/globalEntityService.js +59 -0
  39. package/lib/services/messageService.js +107 -0
  40. package/lib/services/metaService.js +5 -13
  41. package/lib/services/patientService.js +3 -2
  42. package/lib/services/tokenMapService.js +100 -0
  43. package/lib/storage/MongoStorage.js +9 -14
  44. package/lib/utils/tokenMapUtils.js +12 -0
  45. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @peopl-health/nexus
2
2
 
3
- A concise, configurable messaging and assistant toolkit for WhatsApp. It supports Twilio (production‑ready) and Baileys (limited), optional Mongo storage, OpenAI assistants, templates/flows via Twilio Content API, and an event/middleware model so apps can customize behavior.
3
+ A concise, configurable messaging and assistant toolkit for WhatsApp. It supports Twilio (production‑ready) and Baileys (limited), optional Mongo storage, OpenAI assistants, and templates/flows via Twilio Content API.
4
4
 
5
5
  ## Install
6
6
 
@@ -108,17 +108,12 @@ await nexus.initialize({ storage: 'src', storageConfig: { /* ... */ } });
108
108
  await nexus.initialize({ storage: new MyStorageClass(/* ... */) });
109
109
  ```
110
110
 
111
- ## Middleware & Events
111
+ ## Events
112
112
 
113
- Add middleware per type or global; subscribe to events.
113
+ Subscribe to internal events via the event bus.
114
114
  ```js
115
115
  const bus = nexus.getMessaging().getEventBus();
116
116
  bus.on('message:received', (m) => console.log('rx', m.id));
117
-
118
- nexus.getMessaging().use('message', async (msg, nx, next) => {
119
- // sanitize/annotate
120
- return next();
121
- });
122
117
  ```
123
118
 
124
119
  ## Assistants (Optional)
@@ -190,4 +185,4 @@ await nexus.getStorage().setConfig('media.bucketName', process.env.AWS_S3_BUCKET
190
185
  Tips
191
186
  - Connect Mongo before `app.listen()` to avoid Mongoose buffering timeouts.
192
187
  - If you initialize OpenAI, pass `llm: 'openai'` and `llmConfig: { apiKey }` to `nexus.initialize`.
193
- - Use the event bus + middleware for custom routing and transformations without forking the default handlers.
188
+ - Use the event bus for observability and custom reactions without forking the default handlers.
@@ -135,7 +135,9 @@ class BaileysProvider extends MessageProvider {
135
135
  success: true,
136
136
  messageId: result?.key?.id,
137
137
  provider: 'baileys',
138
- result
138
+ result,
139
+ delivered: true,
140
+ deferred: false
139
141
  };
140
142
  } catch (error) {
141
143
  throw new Error(`Baileys send failed: ${error.message}`);
@@ -172,7 +174,7 @@ class BaileysProvider extends MessageProvider {
172
174
  }
173
175
  }, delay);
174
176
 
175
- return { scheduled: true, delay };
177
+ return { scheduled: true, delay, delivered: false, deferred: true };
176
178
  }
177
179
 
178
180
  getConnectionStatus() {
@@ -38,7 +38,7 @@ class MessageProvider {
38
38
  * @param {string} messageData.fileType - File type (text, image, document, audio)
39
39
  * @param {Object} messageData.variables - Template variables
40
40
  * @param {string} messageData.contentSid - Template content SID
41
- * @returns {Promise<Object>} Message result
41
+ * @returns {Promise<Object>} Result with `delivered` and `deferred` boolean flags.
42
42
  */
43
43
  async sendMessage(messageData) {
44
44
  // eslint-disable-next-line no-unused-vars
@@ -49,7 +49,7 @@ class MessageProvider {
49
49
  /**
50
50
  * Send a scheduled message
51
51
  * @param {Object} scheduledMessage - Scheduled message data
52
- * @returns {Promise<void>}
52
+ * @returns {Promise<Object>} Result with `delivered: false, deferred: true`.
53
53
  */
54
54
  async sendScheduledMessage(scheduledMessage) {
55
55
  // eslint-disable-next-line no-unused-vars
@@ -64,7 +64,9 @@ class TwilioProvider extends MessageProvider {
64
64
  provider: 'twilio',
65
65
  status: 'bench_suppressed',
66
66
  result: { sid: benchSid, status: 'bench_suppressed' },
67
- finalize: { sid: benchSid, status: 'bench_suppressed' }
67
+ finalize: { sid: benchSid, status: 'bench_suppressed' },
68
+ delivered: false,
69
+ deferred: false
68
70
  };
69
71
  }
70
72
 
@@ -158,7 +160,9 @@ class TwilioProvider extends MessageProvider {
158
160
  provider: 'twilio',
159
161
  status: result.status,
160
162
  result,
161
- finalize: { sid: chunks ? null : result.sid, status: result.status?.toLowerCase() || null }
163
+ finalize: { sid: chunks ? null : result.sid, status: result.status?.toLowerCase() || null },
164
+ delivered: true,
165
+ deferred: false
162
166
  };
163
167
  }
164
168
 
@@ -263,7 +267,7 @@ class TwilioProvider extends MessageProvider {
263
267
  }
264
268
  }, delay);
265
269
 
266
- return { scheduled: true, delay };
270
+ return { scheduled: true, delay, delivered: false, deferred: true };
267
271
  }
268
272
 
269
273
  supportsMessageStorage() {
@@ -6,7 +6,7 @@ const { logger } = require('../utils/logger');
6
6
  const { getThreadInfo, switchThreadStoppedStatus } = require('../helpers/threadHelper');
7
7
 
8
8
  const { getRecordByFilter } = require('../services/airtableService');
9
- const { createAssistant, addMsgAssistant, switchAssistant } = require('../services/assistantService');
9
+ const { createAssistant, switchAssistant } = require('../services/assistantService');
10
10
 
11
11
  const { sendMessage, processInstruction, processSystemMessage } = require('../core/NexusMessaging');
12
12
 
@@ -42,11 +42,7 @@ const addMsgAssistantController = async (req, res) => {
42
42
  if (!code) return res.status(400).json({ success: false, error: 'Code is required' });
43
43
 
44
44
  try {
45
- if (reply) {
46
- await processSystemMessage(code, messages, role, { triggeredBy });
47
- } else {
48
- await addMsgAssistant(code, messages, role);
49
- }
45
+ await processSystemMessage(code, messages, role, { reply, triggeredBy });
50
46
  return res.status(200).json({ success: true, message: 'Message added to assistant' });
51
47
  } catch (error) {
52
48
  logger.error('[AssistantController] Add message error', { error: error.message, code, role });
@@ -3,10 +3,10 @@ const runtimeConfig = require('../config/runtimeConfig');
3
3
 
4
4
  const { logger } = require('../utils/logger');
5
5
 
6
- const { Message } = require('../models/messageModel');
7
6
  const { getBug, VALID_SEVERITIES, UPDATABLE_FIELDS } = require('../models/bugModel');
8
7
 
9
8
  const { addRecord, getRecordByFilter } = require('../services/airtableService');
9
+ const { getMessages } = require('../services/messageService');
10
10
 
11
11
  async function logBugReportToAirtable(reportedBug) {
12
12
  const {
@@ -16,7 +16,7 @@ async function logBugReportToAirtable(reportedBug) {
16
16
  try {
17
17
  let conversation = null;
18
18
  if (messages?.length) {
19
- const msgs = await Message.find({ _id: { $in: messages } }).sort({ createdAt: 1 });
19
+ const msgs = await getMessages({ _id: { $in: messages } }, { sort: { createdAt: 1 } });
20
20
  conversation = msgs.map(msg => {
21
21
  const timestamp = msg.createdAt.toISOString().slice(0, 16).replace('T', ' ');
22
22
  const role = msg.from_me ? 'Assistant' : 'Patient';
@@ -13,6 +13,7 @@ const { ensureWhatsAppFormat } = require('../helpers/twilioHelper');
13
13
 
14
14
  const { getRecordByFilter, updateRecordByFilter } = require('../services/airtableService');
15
15
  const { fetchConversationData, processConversations, startConversation } = require('../services/conversationService');
16
+ const { getMessages, countMessages, aggregateMessages } = require('../services/messageService');
16
17
 
17
18
  const { sendMessage } = require('../core/NexusMessaging');
18
19
 
@@ -25,7 +26,7 @@ const getConversationController = async (req, res) => {
25
26
  const skip = (page - 1) * limit;
26
27
  const filter = req.query.filter || 'all';
27
28
 
28
- if (!Message || await Message.countDocuments({}) === 0) {
29
+ if (!Message || await countMessages({}) === 0) {
29
30
  return res.status(200).json({
30
31
  success: true,
31
32
  conversations: [],
@@ -88,8 +89,8 @@ const getConversationMessagesController = async (req, res) => {
88
89
  query.createdAt = { $lt: new Date(before) };
89
90
  }
90
91
 
91
- const total = await Message.countDocuments(query);
92
- const messages = await Message.find(query).sort({ createdAt: -1 }).skip(skip).limit(limit).lean();
92
+ const total = await countMessages(query);
93
+ const messages = await getMessages(query, { sort: { createdAt: -1 }, skip, limit });
93
94
  const totalPages = Math.ceil(total / limit);
94
95
 
95
96
  res.status(200).json({
@@ -190,7 +191,7 @@ const searchConversationsController = async (req, res) => {
190
191
 
191
192
  const escapedQuery = query.replace(/\+/g, '\\+');
192
193
 
193
- const matches = await Message.aggregate([
194
+ const matches = await aggregateMessages([
194
195
  {
195
196
  $facet: {
196
197
  primary: [
@@ -329,7 +330,7 @@ const getConversationsByNameController = async (req, res) => {
329
330
  }
330
331
  ];
331
332
 
332
- const [facetResult] = await Message.aggregate(pipeline, { allowDiskUse: true });
333
+ const [facetResult] = await aggregateMessages(pipeline, { allowDiskUse: true });
333
334
 
334
335
  const conversations = facetResult?.data ?? [];
335
336
  const total = facetResult?.totalCount[0]?.total ?? 0;
@@ -365,7 +366,7 @@ const getConversationsByNameController = async (req, res) => {
365
366
  const getNewMessagesController = async (req, res) => {
366
367
  try {
367
368
  const { phoneNumber } = req.params;
368
- const { after, limit = 20 } = req.query;
369
+ const { after } = req.query;
369
370
 
370
371
  if (!phoneNumber) {
371
372
  return res.status(400).json({
@@ -381,7 +382,7 @@ const getNewMessagesController = async (req, res) => {
381
382
  });
382
383
  }
383
384
 
384
- const lastMessage = await Message.findById(after).lean();
385
+ const [lastMessage] = await getMessages({ _id: after }, { limit: 1 });
385
386
 
386
387
  if (!lastMessage) {
387
388
  return res.status(404).json({
@@ -396,10 +397,9 @@ const getNewMessagesController = async (req, res) => {
396
397
  createdAt: { $gt: lastMessage.createdAt }
397
398
  };
398
399
 
399
- const messages = await Message.find(query)
400
- .sort({ createdAt: 1 })
401
- .limit(parseInt(limit))
402
- .lean();
400
+ const raw = parseInt(req.query.limit, 10);
401
+ const clampedLimit = Number.isFinite(raw) ? Math.min(Math.max(raw, 1), 100) : 20;
402
+ const messages = await getMessages(query, { sort: { createdAt: 1 }, limit: clampedLimit });
403
403
 
404
404
  res.status(200).json({
405
405
  success: true,
@@ -559,8 +559,8 @@ const searchMessagesByNumberController = async (req, res) => {
559
559
 
560
560
  const mongoSort = { createdAt: -1 };
561
561
 
562
- const total = await Message.countDocuments(mongoQuery);
563
- const messages = await Message.find(mongoQuery).sort(mongoSort).skip(skip).limit(limit).lean();
562
+ const total = await countMessages(mongoQuery);
563
+ const messages = await getMessages(mongoQuery, { sort: mongoSort, skip, limit });
564
564
  const totalPages = Math.ceil(total / limit);
565
565
 
566
566
  res.status(200).json({
@@ -4,13 +4,13 @@ const { Logging_ID } = require('../config/airtableConfig');
4
4
  const { logger } = require('../utils/logger');
5
5
 
6
6
  const { getInteraction } = require('../models/interactionModel');
7
- const { Message } = require('../models/messageModel');
8
7
 
9
8
  const { addRecord, getRecordByFilter } = require('../services/airtableService');
9
+ const { getMessages } = require('../services/messageService');
10
10
 
11
11
  async function logInteractionToAirtable(messageIds, whatsapp_id, reporter, quality, description, type, medical_note) {
12
12
  try {
13
- const messageObjects = await Message.find({ _id: { $in: messageIds } }).sort({ createdAt: -1 });
13
+ const messageObjects = await getMessages({ _id: { $in: messageIds } }, { sort: { createdAt: -1 } });
14
14
 
15
15
  const conversation = messageObjects.map(msg => {
16
16
  const timestamp = msg.createdAt.toISOString().slice(0, 16).replace('T', ' ');
@@ -3,7 +3,6 @@ const mongoose = require('mongoose');
3
3
 
4
4
  const { logger } = require('../utils/logger');
5
5
 
6
- const { Message } = require('../models/messageModel.js');
7
6
  const { ScheduledMessage } = require('../models/agendaMessageModel.js');
8
7
  const { DeliveryAttempt } = require('../models/deliveryAttemptModel.js');
9
8
  const FlowRouting = require('../models/flowRoutingModel.js');
@@ -12,6 +11,7 @@ const { ensureWhatsAppFormat } = require('../helpers/twilioHelper');
12
11
  const { ensureFlowTokenInVariables } = require('../helpers/templateFlowControllerHelper');
13
12
 
14
13
  const { getRecordByFilter } = require('../services/airtableService');
14
+ const { getMessages } = require('../services/messageService');
15
15
 
16
16
  const { sendMessage, sendScheduledMessage, getDefaultInstance } = require('../core/NexusMessaging');
17
17
 
@@ -215,9 +215,10 @@ const getLastInteractionController = async (req, res) => {
215
215
 
216
216
  try {
217
217
  const normalizedCode = ensureWhatsAppFormat(code);
218
- const lastMessage = await Message.findOne({
219
- $or: [{ numero: normalizedCode }, { group_id: normalizedCode }]
220
- }).sort({ createdAt: -1 });
218
+ const [lastMessage] = await getMessages(
219
+ { $or: [{ numero: normalizedCode }, { group_id: normalizedCode }] },
220
+ { sort: { createdAt: -1 }, limit: 1 }
221
+ );
221
222
 
222
223
  if (!lastMessage) {
223
224
  return res.status(404).json({ success: false, error: 'No messages found' });
@@ -259,7 +260,7 @@ const checkMessageStatusController = async (req, res) => {
259
260
  }
260
261
 
261
262
  try {
262
- const msg = await Message.findOne({ content_sid: contentSid, numero: ensureWhatsAppFormat(code) });
263
+ const [msg] = await getMessages({ content_sid: contentSid, numero: ensureWhatsAppFormat(code) }, { limit: 1 });
263
264
  if (!msg) {
264
265
  return res.status(404).json({ success: false, error: 'Message not found' });
265
266
  }
@@ -1,7 +1,8 @@
1
1
  const { logger } = require('../utils/logger');
2
2
 
3
3
  const { getQualityMessage } = require('../models/qualityMessageModel');
4
- const { Message } = require('../models/messageModel');
4
+
5
+ const { getMessages } = require('../services/messageService');
5
6
 
6
7
  const VALID_QUALITIES = ['low', 'medium', 'high'];
7
8
 
@@ -15,7 +16,7 @@ const addQualityVoteController = async (req, res) => {
15
16
  return res.status(400).json({ success: false, error: 'Quality must be low, medium, or high' });
16
17
  }
17
18
 
18
- const message = await Message.findById(message_id);
19
+ const [message] = await getMessages({ _id: message_id }, { limit: 1 });
19
20
  if (!message) return res.status(404).json({ success: false, error: 'Message not found' });
20
21
 
21
22
  const qualityVote = await getQualityMessage().findOneAndUpdate(
@@ -4,8 +4,8 @@ const { runAssistantWithRetries } = require('../helpers/assistantHelper');
4
4
  const { getAssistantById } = require('../services/assistantResolver');
5
5
 
6
6
  class AssistantProcessor {
7
- constructor({ mode = 'local', queueAdapter = null, sendMessage = null, storeRunMetrics = null }) {
8
- Object.assign(this, { mode, queueAdapter, sendMessage, storeRunMetrics });
7
+ constructor({ mode = 'local', queueAdapter = null, sendMessage = null, phiProcessor = null, storeRunMetrics = null }) {
8
+ Object.assign(this, { mode, queueAdapter, sendMessage, phiProcessor, storeRunMetrics });
9
9
  if (mode === 'queue' && queueAdapter) {
10
10
  queueAdapter.process('assistant.process', (payload) => this._executeLocal(payload));
11
11
  }
@@ -22,7 +22,7 @@ class AssistantProcessor {
22
22
 
23
23
  async executeLLM(thread, assistant, runOptions = {}, messages = null) {
24
24
  const startTime = Date.now();
25
- const runResult = await runAssistantWithRetries(thread, assistant, runOptions, messages);
25
+ const runResult = await runAssistantWithRetries(thread, assistant, { ...runOptions, phiProcessor: this.phiProcessor }, messages);
26
26
  const predictionTimeMs = Date.now() - startTime;
27
27
 
28
28
  const output = sanitizeOutput(runResult?.output);
@@ -1,5 +1,6 @@
1
1
  const { logger } = require('../utils/logger');
2
- const { Message } = require('../models/messageModel');
2
+
3
+ const { getMessages } = require('../services/messageService');
3
4
 
4
5
  class BatchingManager {
5
6
  constructor({ provider = null, config = {} }) {
@@ -115,10 +116,10 @@ class BatchingManager {
115
116
  async _startTypingRefresh(chatId, runId) {
116
117
  if (!this.config.typingIndicator || !this.provider?.sendTypingIndicator) return;
117
118
 
118
- const lastMessage = await Message.findOne({
119
- numero: chatId, from_me: false, processed: false,
120
- message_id: { $exists: true, $ne: null, $not: /^pending-/ }
121
- }).sort({ createdAt: -1 });
119
+ const [lastMessage] = await getMessages(
120
+ { numero: chatId, from_me: false, processed: false, message_id: { $exists: true, $ne: null, $not: /^pending-/ } },
121
+ { sort: { createdAt: -1 }, limit: 1 }
122
+ );
122
123
 
123
124
  if (!lastMessage?.message_id || this.activeRequests.get(chatId) !== runId) return;
124
125