@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.
- package/README.md +4 -9
- package/lib/adapters/BaileysProvider.js +4 -2
- package/lib/adapters/MessageProvider.js +2 -2
- package/lib/adapters/TwilioProvider.js +7 -3
- package/lib/controllers/assistantController.js +2 -6
- package/lib/controllers/bugReportController.js +2 -2
- package/lib/controllers/conversationController.js +13 -13
- package/lib/controllers/interactionController.js +2 -2
- package/lib/controllers/messageController.js +6 -5
- package/lib/controllers/qualityMessageController.js +3 -2
- package/lib/core/AssistantProcessor.js +3 -3
- package/lib/core/BatchingManager.js +6 -5
- package/lib/core/NexusMessaging.js +225 -154
- package/lib/core/PhiProcessor.js +113 -0
- package/lib/eval/EvalProvider.js +6 -1
- package/lib/helpers/baileysHelper.js +3 -1
- package/lib/helpers/conversationWindowHelper.js +4 -4
- package/lib/helpers/deliveryAttemptHelper.js +3 -1
- package/lib/helpers/filesHelper.js +2 -5
- package/lib/helpers/messageHelper.js +10 -71
- package/lib/helpers/messageStatusHelper.js +3 -3
- package/lib/helpers/nerHelper.js +64 -0
- package/lib/helpers/templateRecoveryHelper.js +2 -0
- package/lib/index.d.ts +16 -1
- package/lib/jobs/ScheduledMessageJob.js +15 -23
- package/lib/jobs/TemplateApprovalJob.js +4 -1
- package/lib/memory/DefaultMemoryManager.js +5 -5
- package/lib/memory/SessionManager.js +3 -6
- package/lib/models/deliveryAttemptModel.js +1 -1
- package/lib/models/globalEntityMapModel.js +27 -0
- package/lib/models/messageModel.js +0 -94
- package/lib/models/tokenMapModel.js +28 -0
- package/lib/providers/NerClient.js +43 -0
- package/lib/providers/OpenAIResponsesProvider.js +9 -7
- package/lib/providers/OpenAIResponsesProviderTools.js +26 -9
- package/lib/services/assistantService.js +20 -11
- package/lib/services/dashboardService.js +4 -4
- package/lib/services/globalEntityService.js +59 -0
- package/lib/services/messageService.js +107 -0
- package/lib/services/patientService.js +3 -2
- package/lib/services/tokenMapService.js +100 -0
- package/lib/storage/MongoStorage.js +9 -14
- package/lib/utils/tokenMapUtils.js +12 -0
- 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
|
|
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.
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
:
|
|
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',
|
|
256
|
-
|
|
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
|
-
|
|
259
|
-
return
|
|
276
|
+
}
|
|
277
|
+
return result;
|
|
260
278
|
}
|
|
261
279
|
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
if (!
|
|
265
|
-
|
|
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
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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.
|
|
284
|
-
await this.
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
|
294
|
-
if (chatId) ensureThreadExists(chatId);
|
|
314
|
+
const saveFn = this._saveFn;
|
|
295
315
|
|
|
296
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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.
|
|
364
|
-
|
|
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.
|
|
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.
|
|
374
|
-
|
|
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) {
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
522
|
+
const messageData = {
|
|
523
|
+
pushName: 'Instruction',
|
|
524
|
+
code,
|
|
471
525
|
body: instruction,
|
|
472
|
-
|
|
473
|
-
|
|
526
|
+
messageId: `instruction_${Date.now()}_${Math.random().toString(36).substring(7)}`,
|
|
527
|
+
fromMe: true,
|
|
474
528
|
processed: true,
|
|
475
529
|
origin: 'instruction',
|
|
476
|
-
|
|
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.
|
|
482
|
-
|
|
483
|
-
|
|
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:
|
|
488
|
-
additionalMessages: [{ role, content:
|
|
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
|
|
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
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
|
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
|
+
};
|