@peopl-health/nexus 4.4.5 → 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 (44) 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 +225 -154
  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/patientService.js +3 -2
  41. package/lib/services/tokenMapService.js +100 -0
  42. package/lib/storage/MongoStorage.js +9 -14
  43. package/lib/utils/tokenMapUtils.js +12 -0
  44. package/package.json +1 -1
@@ -7,7 +7,7 @@ const { setMetaConfig } = require('../config/metaConfig');
7
7
 
8
8
  const { logger } = require('../utils/logger');
9
9
 
10
- const { Message, insertMessage } = require('../models/messageModel');
10
+ const { Message } = require('../models/messageModel');
11
11
  const { Thread } = require('../models/threadModel');
12
12
 
13
13
  const { ensureThreadExists } = require('../helpers/threadHelper');
@@ -19,6 +19,7 @@ const { createMessagingProvider } = require('../adapters/registry');
19
19
 
20
20
  const { preProcessMessages } = require('../services/assistantService');
21
21
  const { hasPreprocessingHandler, invokePreprocessingHandler } = require('../services/preprocessingService');
22
+ const { getMessages, updateMessage } = require('../services/messageService');
22
23
 
23
24
  const { BatchingManager } = require('../core/BatchingManager');
24
25
  const { ProcessingPipeline } = require('../core/ProcessingPipeline');
@@ -28,6 +29,8 @@ const { createQueueAdapter } = require('../queue');
28
29
  const { ScheduledMessageJob } = require('../jobs/ScheduledMessageJob');
29
30
  const { TemplateApprovalJob } = require('../jobs/TemplateApprovalJob');
30
31
 
32
+ const { PhiProcessor } = require('./PhiProcessor');
33
+
31
34
  /**
32
35
  * Core messaging orchestrator for providers, storage, and assistant processing.
33
36
  */
@@ -48,15 +51,6 @@ class NexusMessaging {
48
51
  onFlow: null
49
52
  };
50
53
  this.events = new EventEmitter();
51
- this.middleware = {
52
- any: [],
53
- message: [],
54
- interactive: [],
55
- media: [],
56
- command: [],
57
- keyword: [],
58
- flow: []
59
- };
60
54
 
61
55
  this.batchingConfig = {
62
56
  enabled: config.messageBatching?.enabled ?? true,
@@ -76,17 +70,23 @@ class NexusMessaging {
76
70
  const queueConfig = config.queue?.config || {};
77
71
  this.queueAdapter = createQueueAdapter(queueType, queueConfig);
78
72
 
73
+ this.phiProcessor = new PhiProcessor({
74
+ encode: config.phi?.encode || false,
75
+ ner: config.phi?.ner || null,
76
+ hashSecret: config.phi?.hashSecret || null,
77
+ });
78
+
79
79
  this.assistantProcessor = new AssistantProcessor({
80
80
  mode: config.assistant?.mode || 'local',
81
81
  queueAdapter: this.queueAdapter,
82
- sendMessage: this.sendMessage.bind(this),
82
+ sendMessage: this.sendAssistantReply.bind(this),
83
+ phiProcessor: this.phiProcessor,
83
84
  storeRunMetrics,
84
85
  });
85
86
 
86
87
  this.scheduledMessageJob = new ScheduledMessageJob({
87
88
  queueAdapter: this.queueAdapter,
88
89
  sendMessage: this.sendMessage.bind(this),
89
- requireMessageStorage: () => this.messageStorage,
90
90
  requireProvider: () => this.getProvider()
91
91
  });
92
92
 
@@ -99,6 +99,41 @@ class NexusMessaging {
99
99
  return provider;
100
100
  }
101
101
  });
102
+
103
+ this._providerStores = false;
104
+ this._saveFn = null;
105
+ this._saveMessageFn = async () => null;
106
+ }
107
+
108
+ _selectSaveFn(providerStores) {
109
+ if (!this.messageStorage) return null;
110
+ if (providerStores && this.messageStorage.savePendingMessage) {
111
+ return this.messageStorage.savePendingMessage.bind(this.messageStorage);
112
+ }
113
+ if (!providerStores && this.messageStorage.saveMessage) {
114
+ return this.messageStorage.saveMessage.bind(this.messageStorage);
115
+ }
116
+ return null;
117
+ }
118
+
119
+ async _transport(messageData) {
120
+ if (messageData.contentSid && !messageData.body && this.provider.renderTemplate) {
121
+ const rendered = await this.provider.renderTemplate(messageData.contentSid, messageData.variables);
122
+ if (rendered) messageData = { ...messageData, body: rendered };
123
+ }
124
+
125
+ const result = await this.provider.sendMessage(messageData);
126
+
127
+ if (messageData.parentMessageId && result?.finalize && this.messageStorage?.finalizePendingMessage) {
128
+ await this.messageStorage.finalizePendingMessage(messageData.parentMessageId, result.finalize.sid, {
129
+ status: result.finalize.status,
130
+ updatedAt: new Date()
131
+ });
132
+ }
133
+
134
+ if (messageData.code) ensureThreadExists(messageData.code);
135
+
136
+ return result;
102
137
  }
103
138
 
104
139
  async initialize(options = {}) {
@@ -161,6 +196,9 @@ class NexusMessaging {
161
196
  if (this.provider && typeof this.provider.setMessageStorage === 'function') {
162
197
  this.provider.setMessageStorage(storage);
163
198
  }
199
+ this._providerStores = this.provider?.supportsMessageStorage?.() ?? false;
200
+ this._saveFn = this._selectSaveFn(this._providerStores);
201
+ this._saveMessageFn = this.messageStorage ? (msg) => this.messageStorage.saveMessage(msg) : async () => null;
164
202
  }
165
203
 
166
204
  setHandlers(handlers) {
@@ -185,54 +223,10 @@ class NexusMessaging {
185
223
  getPipeline() { return this.pipeline; }
186
224
  getAssistantProcessor() { return this.assistantProcessor; }
187
225
  getQueueAdapter() { return this.queueAdapter; }
226
+ getPhiProcessor() { return this.phiProcessor; }
188
227
  isConnected() { return this.provider?.getConnectionStatus() ?? false; }
189
228
  isProcessing(chatId) { return this.batchingManager.isProcessing(chatId); }
190
229
 
191
- /*
192
- * MIDDLEWARE
193
- */
194
-
195
- use(typeOrFn, maybeFn) {
196
- if (typeof typeOrFn === 'function') {
197
- this.middleware.any.push(typeOrFn);
198
- return this;
199
- }
200
- const type = String(typeOrFn || '').toLowerCase();
201
- if (!this.middleware[type]) this.middleware[type] = [];
202
- this.middleware[type].push(maybeFn);
203
- return this;
204
- }
205
-
206
- async _runPipeline(type, messageData, finalHandler) {
207
- const chain = [
208
- ...(this.middleware.any || []),
209
- ...(this.middleware[type] || []),
210
- async (ctx) => await finalHandler(ctx)
211
- ];
212
- let idx = -1;
213
- const runner = async (i) => {
214
- if (i <= idx) throw new Error('next() called multiple times');
215
- idx = i;
216
- const fn = chain[i];
217
- if (!fn) return;
218
- return await fn(messageData, this, () => runner(i + 1));
219
- };
220
- return await runner(0);
221
- }
222
-
223
- async _handleWithPipeline(type, handlerKey, messageData, fallback) {
224
- const result = await this._runPipeline(type, messageData, async (ctx) => {
225
- const handler = this.handlers[handlerKey];
226
- if (handler) {
227
- return await handler(ctx, this);
228
- }
229
- if (fallback) {
230
- return await fallback(ctx);
231
- }
232
- });
233
- return result;
234
- }
235
-
236
230
  /*
237
231
  * OUTBOUND MESSAGING
238
232
  */
@@ -242,62 +236,96 @@ class NexusMessaging {
242
236
 
243
237
  if (messageData._fromConversationReply) messageData.processed = true;
244
238
 
245
- if (messageData.contentSid && !messageData.body && this.provider.renderTemplate) {
246
- const rendered = await this.provider.renderTemplate(messageData.contentSid, messageData.variables);
247
- if (rendered) messageData.body = rendered;
248
- }
249
-
250
239
  if (this._needsTemplateRoute(messageData) && !(await isWithin24HourWindow(messageData.code))) {
251
- const parent = messageData.parentMessageId
252
- ? await Message.findById(messageData.parentMessageId)
253
- : await this.messageStorage.savePendingMessage({
240
+ if (messageData.parentMessageId) {
241
+ const [found] = await getMessages({ _id: messageData.parentMessageId }, { limit: 1 });
242
+ if (!found) return { success: false, delivered: false, deferred: true };
243
+ await triggerTemplateRecovery(found, { source: 'preemptive' });
244
+ return { success: true, messageId: String(found._id), status: 'queued', preemptive: true, delivered: false, deferred: true };
245
+ }
246
+ const saveFn = this.messageStorage.savePendingMessage.bind(this.messageStorage);
247
+ return this.phiProcessor.outDataFromPlain(
248
+ {
254
249
  ...messageData,
255
- provider: 'twilio', fromMe: true,
256
- processed: messageData.processed ?? false
250
+ provider: 'twilio',
251
+ fromMe: true,
252
+ processed: messageData.processed ?? false,
253
+ },
254
+ saveFn,
255
+ async ({ body, _id }) => {
256
+ await triggerTemplateRecovery({ _id, body, numero: messageData.code }, { source: 'preemptive' });
257
+ return { success: true, messageId: String(_id), status: 'queued', preemptive: true, delivered: false, deferred: true };
258
+ },
259
+ { onSaved: messageData.onParentMessageId },
260
+ );
261
+ }
262
+
263
+ if (messageData.parentMessageId) {
264
+ return this._transport(messageData);
265
+ }
266
+
267
+ if (!this._providerStores) {
268
+ const encodedBody = await this.phiProcessor.encodeBody(messageData.body, messageData.code);
269
+ const result = await this._transport(messageData);
270
+ if (this.messageStorage?.saveMessage) {
271
+ await this.messageStorage.saveMessage({
272
+ ...messageData, body: encodedBody,
273
+ messageId: result?.messageId, provider: result?.provider,
274
+ fromMe: true, processed: true,
257
275
  });
258
- await triggerTemplateRecovery(parent, { source: 'preemptive' });
259
- return { success: true, messageId: String(parent._id), status: 'queued', preemptive: true };
276
+ }
277
+ return result;
260
278
  }
261
279
 
262
- const providerStores = this.provider.supportsMessageStorage?.() ?? false;
263
- let parentId = messageData.parentMessageId || null;
264
- if (!parentId && providerStores && this.messageStorage?.savePendingMessage) {
265
- const pending = await this.messageStorage.savePendingMessage({
266
- ...messageData,
267
- provider: 'twilio',
268
- fromMe: true,
269
- processed: messageData.processed ?? false
270
- });
271
- parentId = pending?._id || null;
280
+ const saveFn = this._saveFn;
281
+
282
+ if (!saveFn) {
283
+ return this._transport(messageData);
272
284
  }
273
285
 
274
- const result = await this.provider.sendMessage({ ...messageData, parentMessageId: parentId });
286
+ return this.phiProcessor.outDataFromPlain(
287
+ { ...messageData, provider: 'twilio', fromMe: true, processed: messageData.processed ?? false },
288
+ saveFn,
289
+ ({ body, _id }) => this._transport({ ...messageData, body, parentMessageId: _id }),
290
+ { onSaved: messageData.onParentMessageId },
291
+ );
292
+ }
275
293
 
276
- if (parentId && result?.finalize && this.messageStorage?.finalizePendingMessage) {
277
- await this.messageStorage.finalizePendingMessage(parentId, result.finalize.sid, {
278
- status: result.finalize.status,
279
- updatedAt: new Date()
280
- });
294
+ async sendAssistantReply(messageData) {
295
+ if (!this.provider) throw new Error('No provider initialized');
296
+
297
+ if (messageData.parentMessageId) {
298
+ return this._transport(messageData);
281
299
  }
282
300
 
283
- if (this.messageStorage && !providerStores) {
284
- await this.messageStorage.saveMessage({
285
- ...messageData,
286
- messageId: result.messageId,
287
- provider: result.provider,
288
- fromMe: true,
289
- processed: true
290
- });
301
+ if (!this._providerStores) {
302
+ const decodedBody = await this.phiProcessor.decodeBody(messageData.body, messageData.code);
303
+ const result = await this._transport({ ...messageData, body: decodedBody });
304
+ if (this.messageStorage?.saveMessage) {
305
+ await this.messageStorage.saveMessage({
306
+ ...messageData,
307
+ messageId: result?.messageId, provider: result?.provider,
308
+ fromMe: true, processed: true, origin: messageData.origin || 'assistant',
309
+ });
310
+ }
311
+ return result;
291
312
  }
292
313
 
293
- const chatId = messageData.code;
294
- if (chatId) ensureThreadExists(chatId);
314
+ const saveFn = this._saveFn;
295
315
 
296
- return result;
316
+ if (!saveFn) {
317
+ return this._transport(messageData);
318
+ }
319
+
320
+ return this.phiProcessor.outDataFromEncoded(
321
+ { ...messageData, fromMe: true, processed: true, origin: messageData.origin || 'assistant' },
322
+ saveFn,
323
+ ({ body, _id }) => this._transport({ ...messageData, body, parentMessageId: _id }),
324
+ );
297
325
  }
298
326
 
299
327
  _needsTemplateRoute(messageData) {
300
- if (this.provider?.supportsMessageStorage?.() !== true) return false;
328
+ if (!this._providerStores) return false;
301
329
  if (messageData.contentSid) return false;
302
330
  if (!messageData.code) return false;
303
331
  return true;
@@ -310,10 +338,11 @@ class NexusMessaging {
310
338
  if (!scheduledMessage?.sendTime) {
311
339
  throw new Error('sendScheduledMessage requires sendTime');
312
340
  }
313
- return await this.scheduledMessageJob.schedule({
341
+ const job = await this.scheduledMessageJob.schedule({
314
342
  scheduledMessageId: scheduledMessage._id,
315
343
  sendTime: scheduledMessage.sendTime
316
344
  });
345
+ return { ...(job || {}), delivered: false, deferred: true };
317
346
  }
318
347
 
319
348
  async scheduleTemplateApproval({ templateSid, messageId, originalMessageSid = null, attempt = 0 }) {
@@ -330,8 +359,6 @@ class NexusMessaging {
330
359
  */
331
360
 
332
361
  async processIncomingMessage(messageData) {
333
- if (this.messageStorage) await this.messageStorage.saveMessage(messageData);
334
-
335
362
  const chatId = messageData.code;
336
363
 
337
364
  if (chatId) {
@@ -343,41 +370,52 @@ class NexusMessaging {
343
370
  }
344
371
  }
345
372
 
346
- if (chatId && hasPreprocessingHandler()) {
347
- const stop = await invokePreprocessingHandler({ code: chatId, context: 'incomingMessage', message: messageData });
348
- if (stop) return;
349
- }
350
-
351
- if (messageData.interactive) return this.handleInteractive(messageData);
352
- if (messageData.command) return this.handleCommand(messageData);
353
- if (messageData.keyword) return this.handleKeyword(messageData);
354
- if (messageData.flow) return this.handleFlow(messageData);
355
-
356
- if (this.batchingConfig.enabled && chatId) {
357
- return this._handleWithCheckAfter(chatId);
358
- }
359
- return messageData.media ? this.handleMedia(messageData) : this.handleMessage(messageData);
373
+ return this.phiProcessor.inData(
374
+ messageData,
375
+ this._saveMessageFn,
376
+ async ({ body }) => {
377
+ messageData = { ...messageData, body };
378
+ if (chatId && hasPreprocessingHandler()) {
379
+ const stop = await invokePreprocessingHandler({ code: chatId, context: 'incomingMessage', message: messageData });
380
+ if (stop) return;
381
+ }
382
+
383
+ if (messageData.interactive) return this.handleInteractive(messageData);
384
+ if (messageData.command) return this.handleCommand(messageData);
385
+ if (messageData.keyword) return this.handleKeyword(messageData);
386
+ if (messageData.flow) return this.handleFlow(messageData);
387
+
388
+ if (this.batchingConfig.enabled && chatId) return this._handleWithCheckAfter(chatId);
389
+ return messageData.media ? this.handleMedia(messageData) : this.handleMessage(messageData);
390
+ },
391
+ );
360
392
  }
361
393
 
362
394
  async handleMessage(messageData) {
363
- return await this._handleWithPipeline('message', 'onMessage', messageData, async (ctx) => {
364
- return await this.handleMessageWithAssistant(ctx);
365
- });
395
+ if (this.handlers.onMessage) return await this.handlers.onMessage(messageData, this);
396
+ return await this.handleMessageWithAssistant(messageData);
366
397
  }
367
398
 
368
399
  async handleInteractive(messageData) {
369
- return this._handleWithPipeline('interactive', 'onInteractive', messageData);
400
+ if (this.handlers.onInteractive) return await this.handlers.onInteractive(messageData, this);
370
401
  }
371
402
 
372
403
  async handleMedia(messageData) {
373
- return await this._handleWithPipeline('media', 'onMedia', messageData, async (ctx) => {
374
- return await this.handleMediaWithAssistant(ctx);
375
- });
404
+ if (this.handlers.onMedia) return await this.handlers.onMedia(messageData, this);
405
+ return await this.handleMediaWithAssistant(messageData);
376
406
  }
377
407
 
378
- async handleCommand(messageData) { return this._handleWithPipeline('command', 'onCommand', messageData); }
379
- async handleKeyword(messageData) { return this._handleWithPipeline('keyword', 'onKeyword', messageData); }
380
- async handleFlow(messageData) { return this._handleWithPipeline('flow', 'onFlow', messageData); }
408
+ async handleCommand(messageData) {
409
+ if (this.handlers.onCommand) return await this.handlers.onCommand(messageData, this);
410
+ }
411
+
412
+ async handleKeyword(messageData) {
413
+ if (this.handlers.onKeyword) return await this.handlers.onKeyword(messageData, this);
414
+ }
415
+
416
+ async handleFlow(messageData) {
417
+ if (this.handlers.onFlow) return await this.handlers.onFlow(messageData, this);
418
+ }
381
419
 
382
420
  /*
383
421
  * ASSISTANT INTEGRATION
@@ -448,6 +486,22 @@ class NexusMessaging {
448
486
  if (!resolved) return null;
449
487
 
450
488
  const preProcessed = await preProcessMessages(chatId, null, resolved.thread);
489
+
490
+ await Promise.all((preProcessed.pendingMediaUpdates ?? []).map(u =>
491
+ this.phiProcessor.inData(
492
+ { body: u.processedContent, code: chatId },
493
+ ({ body: encoded }) => updateMessage(
494
+ { message_id: u.message_id },
495
+ { $set: {
496
+ assistant_id: u.assistantId,
497
+ thread_id: u.threadId,
498
+ 'media.metadata.processedContent': encoded,
499
+ }}
500
+ ),
501
+ () => null,
502
+ )
503
+ ));
504
+
451
505
  if (!preProcessed.shouldProcess) return null;
452
506
 
453
507
  const result = await this.assistantProcessor.executeLLM(
@@ -465,55 +519,69 @@ class NexusMessaging {
465
519
 
466
520
  async processInstruction(code, instruction, role = 'developer', { triggeredBy } = {}) {
467
521
  const assistantId = await this._getThreadAssistantId(code);
468
- await insertMessage({
469
- nombre_whatsapp: 'Instruction',
470
- numero: code,
522
+ const messageData = {
523
+ pushName: 'Instruction',
524
+ code,
471
525
  body: instruction,
472
- message_id: `instruction_${Date.now()}_${Math.random().toString(36).substring(7)}`,
473
- from_me: true,
526
+ messageId: `instruction_${Date.now()}_${Math.random().toString(36).substring(7)}`,
527
+ fromMe: true,
474
528
  processed: true,
475
529
  origin: 'instruction',
476
- assistant_id: assistantId,
530
+ assistantId,
477
531
  raw: { role },
478
- triggeredBy: triggeredBy || null
479
- });
532
+ triggeredBy: triggeredBy || null,
533
+ silent: true,
534
+ };
480
535
 
481
- const result = await this._executeWithPipeline(code, 'instruction', 'queue',
482
- async (preProcessResult) => {
483
- return await this.assistantProcessor.process({
536
+ const result = await this.phiProcessor.inData(
537
+ messageData,
538
+ this._saveMessageFn,
539
+ ({ body }) => this._executeWithPipeline(code, 'instruction', 'queue',
540
+ async (preProcessResult) => this.assistantProcessor.process({
484
541
  code,
485
542
  runOptions: {
486
543
  prePromptResult: preProcessResult,
487
- additionalInstructions: instruction,
488
- additionalMessages: [{ role, content: instruction }],
544
+ additionalInstructions: body,
545
+ additionalMessages: [{ role, content: body }],
489
546
  toolChoice: 'none',
490
547
  }
491
- });
492
- }
548
+ }),
549
+ ),
493
550
  );
494
551
 
495
552
  return result?.output || null;
496
553
  }
497
554
 
498
- async processSystemMessage(code, messages, role = 'system', { triggeredBy } = {}) {
499
- const normalizedMessages = Array.isArray(messages) ? messages : [messages];
555
+ async processSystemMessage(code, messages, role = 'system', { triggeredBy, reply = true } = {}) {
556
+ const thread = await Thread.findOne({ code }).lean();
557
+ if (!thread) return null;
558
+
500
559
  const assistantId = await this._getThreadAssistantId(code);
560
+ const normalizedMessages = Array.isArray(messages) ? messages : [messages];
501
561
 
502
562
  for (let i = 0; i < normalizedMessages.length; i++) {
503
- await insertMessage({
504
- nombre_whatsapp: 'System',
505
- numero: code,
506
- body: normalizedMessages[i],
507
- message_id: `system_${Date.now()}_${i}_${Math.random().toString(36).substring(7)}`,
508
- from_me: true,
509
- processed: true,
510
- origin: 'system',
511
- assistant_id: assistantId,
512
- raw: { role },
513
- triggeredBy: triggeredBy || null
514
- });
563
+ try {
564
+ const messageData = {
565
+ pushName: 'System',
566
+ code,
567
+ body: normalizedMessages[i],
568
+ messageId: `system_${Date.now()}_${i}_${Math.random().toString(36).substring(7)}`,
569
+ fromMe: true,
570
+ processed: true,
571
+ origin: 'system',
572
+ assistantId,
573
+ raw: { role },
574
+ triggeredBy: triggeredBy || null,
575
+ silent: true,
576
+ };
577
+ await this.phiProcessor.inData(messageData, this._saveMessageFn, () => {});
578
+ } catch (err) {
579
+ logger.error('[processSystemMessage] Error saving system message', { error: err.message, code, index: i });
580
+ }
515
581
  }
516
582
 
583
+ if (!reply) return null;
584
+
517
585
  const result = await this._executeWithPipeline(code, 'system', 'queue',
518
586
  async (preProcessResult) => {
519
587
  return await this.assistantProcessor.process({
@@ -536,7 +604,7 @@ class NexusMessaging {
536
604
 
537
605
  async _processMessages(chatId, processingFn, shouldFinalize = () => true) {
538
606
  const query = { numero: chatId, from_me: false, processed: false };
539
- const unprocessed = await Message.find(query).select('_id');
607
+ const unprocessed = await getMessages(query, { select: '_id' });
540
608
 
541
609
  try {
542
610
  const result = await processingFn();
@@ -622,6 +690,8 @@ const processSystemMessage = async (code, messages, role, options) => {
622
690
 
623
691
  const getEventBus = () => getDefaultInstance()?.getEventBus();
624
692
 
693
+ const getPhiProcessor = () => getDefaultInstance()?.getPhiProcessor();
694
+
625
695
  const _resetDefaultInstance = () => { defaultInstance = null; };
626
696
 
627
697
  module.exports = {
@@ -635,6 +705,7 @@ module.exports = {
635
705
  getDefaultInstance,
636
706
  requireDefaultInstance,
637
707
  getProvider,
708
+ getPhiProcessor,
638
709
  requireProvider,
639
710
  getEventBus,
640
711
  _resetDefaultInstance
@@ -0,0 +1,113 @@
1
+ const { createHmac } = require('crypto');
2
+
3
+ const { logger } = require('../utils/logger');
4
+
5
+ const { PHI_CATEGORIES } = require('../models/tokenMapModel');
6
+
7
+ const { NerClient } = require('../providers/NerClient');
8
+
9
+ const tokenMapService = require('../services/tokenMapService');
10
+
11
+ const { lookupGlobal, lookupGlobalToken } = require('../services/globalEntityService');
12
+
13
+ const TOKEN_REGEX = /<<[A-Z_]+_\d+>>/g;
14
+
15
+ class PhiProcessor {
16
+ constructor({ encode = false, ner, hashSecret } = {}) {
17
+ this._nerClient = null;
18
+ this._hashSecret = hashSecret || null;
19
+ this._phiEnabled = !!encode;
20
+ if (!encode) return;
21
+ if (!ner) {
22
+ logger.error('[PhiProcessor] PHI encoding enabled but no NER config provided — body will be stored as plain text');
23
+ return;
24
+ }
25
+ this._setNerClient(ner);
26
+ }
27
+
28
+ _setNerClient(nerConfig) {
29
+ if (!nerConfig) { this._nerClient = null; return; }
30
+ this._nerClient = typeof nerConfig.detectEntities === 'function' ? nerConfig : new NerClient(nerConfig);
31
+ }
32
+
33
+ hashCode(code) {
34
+ if (!code) return null;
35
+ if (!this._phiEnabled) return code;
36
+ if (!this._hashSecret) {
37
+ logger.warn('[PhiProcessor] PHI hash secret not configured — numero omitted from metadata');
38
+ return null;
39
+ }
40
+ return createHmac('sha256', this._hashSecret).update(code).digest('hex');
41
+ }
42
+
43
+ async encodeBody(body, numero) {
44
+ if (!body || !numero || !this._nerClient) return body;
45
+
46
+ const entities = await this._nerClient.detectEntities(body);
47
+ if (!entities?.length) return body;
48
+
49
+ const sorted = [...entities].sort((a, b) => b.start - a.start);
50
+
51
+ let encoded = body;
52
+ for (const entity of sorted) {
53
+ if (!PHI_CATEGORIES.includes(entity.label)) {
54
+ logger.debug('[PhiProcessor] Unknown label, skipping', { label: entity.label });
55
+ continue;
56
+ }
57
+
58
+ const plaintext = entity.plaintext || encoded.slice(entity.start, entity.end);
59
+ const global = await lookupGlobal(plaintext);
60
+ const token = global
61
+ ? global.token
62
+ : await tokenMapService.encode(numero, plaintext, entity.label);
63
+
64
+ encoded = encoded.slice(0, entity.start) + token + encoded.slice(entity.end);
65
+ }
66
+
67
+ logger.debug('[PhiProcessor] Body encoded', { numero, entityCount: entities.length });
68
+ return encoded;
69
+ }
70
+
71
+ async inData(messageData, saveFn, handler) {
72
+ const encoded = await this.encodeBody(messageData.body, messageData.code);
73
+ const saved = await saveFn({ ...messageData, body: encoded });
74
+ return handler({ body: encoded, _id: saved?._id });
75
+ }
76
+
77
+ async outDataFromPlain(messageData, saveFn, handler, { onSaved } = {}) {
78
+ const encoded = await this.encodeBody(messageData.body, messageData.code);
79
+ const saved = await saveFn({ ...messageData, body: encoded });
80
+ await onSaved?.(saved?._id);
81
+ return handler({ body: messageData.body, _id: saved?._id });
82
+ }
83
+
84
+ async decodeBody(body, numero) {
85
+ if (!body || !numero) return body;
86
+ const tokens = body.match(TOKEN_REGEX);
87
+ if (!tokens) return body;
88
+ let decoded = body;
89
+ try {
90
+ for (const token of new Set(tokens)) {
91
+ const globalPlain = await lookupGlobalToken(token);
92
+ const plaintext = globalPlain ?? await tokenMapService.decode(numero, token);
93
+ if (plaintext && plaintext !== token) decoded = decoded.replaceAll(token, plaintext);
94
+ }
95
+ } catch (e) {
96
+ logger.error('[PhiProcessor] decodeBody failed, returning encoded body', { numero, error: e.message });
97
+ return body;
98
+ }
99
+ return decoded;
100
+ }
101
+
102
+ async outDataFromEncoded(messageData, saveFn, handler) {
103
+ const saved = await saveFn(messageData);
104
+ const decodedBody = this._phiEnabled
105
+ ? await this.decodeBody(messageData.body, messageData.code)
106
+ : messageData.body;
107
+ return handler({ body: decodedBody, _id: saved?._id });
108
+ }
109
+ }
110
+
111
+ module.exports = {
112
+ PhiProcessor,
113
+ };