@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.
- 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 +230 -155
- 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/metaService.js +5 -13
- 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
|
@@ -3,10 +3,11 @@ const EventEmitter = require('events');
|
|
|
3
3
|
const { airtable } = require('../config/airtableConfig');
|
|
4
4
|
const llmConfigModule = require('../config/llmConfig');
|
|
5
5
|
const { connect } = require('../config/mongoConfig');
|
|
6
|
+
const { setMetaConfig } = require('../config/metaConfig');
|
|
6
7
|
|
|
7
8
|
const { logger } = require('../utils/logger');
|
|
8
9
|
|
|
9
|
-
const { Message
|
|
10
|
+
const { Message } = require('../models/messageModel');
|
|
10
11
|
const { Thread } = require('../models/threadModel');
|
|
11
12
|
|
|
12
13
|
const { ensureThreadExists } = require('../helpers/threadHelper');
|
|
@@ -18,6 +19,7 @@ const { createMessagingProvider } = require('../adapters/registry');
|
|
|
18
19
|
|
|
19
20
|
const { preProcessMessages } = require('../services/assistantService');
|
|
20
21
|
const { hasPreprocessingHandler, invokePreprocessingHandler } = require('../services/preprocessingService');
|
|
22
|
+
const { getMessages, updateMessage } = require('../services/messageService');
|
|
21
23
|
|
|
22
24
|
const { BatchingManager } = require('../core/BatchingManager');
|
|
23
25
|
const { ProcessingPipeline } = require('../core/ProcessingPipeline');
|
|
@@ -27,6 +29,8 @@ const { createQueueAdapter } = require('../queue');
|
|
|
27
29
|
const { ScheduledMessageJob } = require('../jobs/ScheduledMessageJob');
|
|
28
30
|
const { TemplateApprovalJob } = require('../jobs/TemplateApprovalJob');
|
|
29
31
|
|
|
32
|
+
const { PhiProcessor } = require('./PhiProcessor');
|
|
33
|
+
|
|
30
34
|
/**
|
|
31
35
|
* Core messaging orchestrator for providers, storage, and assistant processing.
|
|
32
36
|
*/
|
|
@@ -47,15 +51,6 @@ class NexusMessaging {
|
|
|
47
51
|
onFlow: null
|
|
48
52
|
};
|
|
49
53
|
this.events = new EventEmitter();
|
|
50
|
-
this.middleware = {
|
|
51
|
-
any: [],
|
|
52
|
-
message: [],
|
|
53
|
-
interactive: [],
|
|
54
|
-
media: [],
|
|
55
|
-
command: [],
|
|
56
|
-
keyword: [],
|
|
57
|
-
flow: []
|
|
58
|
-
};
|
|
59
54
|
|
|
60
55
|
this.batchingConfig = {
|
|
61
56
|
enabled: config.messageBatching?.enabled ?? true,
|
|
@@ -75,17 +70,23 @@ class NexusMessaging {
|
|
|
75
70
|
const queueConfig = config.queue?.config || {};
|
|
76
71
|
this.queueAdapter = createQueueAdapter(queueType, queueConfig);
|
|
77
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
|
+
|
|
78
79
|
this.assistantProcessor = new AssistantProcessor({
|
|
79
80
|
mode: config.assistant?.mode || 'local',
|
|
80
81
|
queueAdapter: this.queueAdapter,
|
|
81
|
-
sendMessage: this.
|
|
82
|
+
sendMessage: this.sendAssistantReply.bind(this),
|
|
83
|
+
phiProcessor: this.phiProcessor,
|
|
82
84
|
storeRunMetrics,
|
|
83
85
|
});
|
|
84
86
|
|
|
85
87
|
this.scheduledMessageJob = new ScheduledMessageJob({
|
|
86
88
|
queueAdapter: this.queueAdapter,
|
|
87
89
|
sendMessage: this.sendMessage.bind(this),
|
|
88
|
-
requireMessageStorage: () => this.messageStorage,
|
|
89
90
|
requireProvider: () => this.getProvider()
|
|
90
91
|
});
|
|
91
92
|
|
|
@@ -98,10 +99,45 @@ class NexusMessaging {
|
|
|
98
99
|
return provider;
|
|
99
100
|
}
|
|
100
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;
|
|
101
137
|
}
|
|
102
138
|
|
|
103
139
|
async initialize(options = {}) {
|
|
104
|
-
const { provider, providerConfig, mongoUri, messageStorage, llm, llmConfig } = options;
|
|
140
|
+
const { provider, providerConfig, mongoUri, messageStorage, llm, llmConfig, meta } = options;
|
|
105
141
|
|
|
106
142
|
if (mongoUri || process.env.MONGODB_URI) {
|
|
107
143
|
await this.initializeMongoDB(mongoUri);
|
|
@@ -112,6 +148,9 @@ class NexusMessaging {
|
|
|
112
148
|
if (llm && llmConfig) {
|
|
113
149
|
this.initializeLLM(llm, llmConfig);
|
|
114
150
|
}
|
|
151
|
+
if (meta) {
|
|
152
|
+
setMetaConfig(meta);
|
|
153
|
+
}
|
|
115
154
|
if (messageStorage) {
|
|
116
155
|
this.setMessageStorage(messageStorage);
|
|
117
156
|
}
|
|
@@ -157,6 +196,9 @@ class NexusMessaging {
|
|
|
157
196
|
if (this.provider && typeof this.provider.setMessageStorage === 'function') {
|
|
158
197
|
this.provider.setMessageStorage(storage);
|
|
159
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;
|
|
160
202
|
}
|
|
161
203
|
|
|
162
204
|
setHandlers(handlers) {
|
|
@@ -181,54 +223,10 @@ class NexusMessaging {
|
|
|
181
223
|
getPipeline() { return this.pipeline; }
|
|
182
224
|
getAssistantProcessor() { return this.assistantProcessor; }
|
|
183
225
|
getQueueAdapter() { return this.queueAdapter; }
|
|
226
|
+
getPhiProcessor() { return this.phiProcessor; }
|
|
184
227
|
isConnected() { return this.provider?.getConnectionStatus() ?? false; }
|
|
185
228
|
isProcessing(chatId) { return this.batchingManager.isProcessing(chatId); }
|
|
186
229
|
|
|
187
|
-
/*
|
|
188
|
-
* MIDDLEWARE
|
|
189
|
-
*/
|
|
190
|
-
|
|
191
|
-
use(typeOrFn, maybeFn) {
|
|
192
|
-
if (typeof typeOrFn === 'function') {
|
|
193
|
-
this.middleware.any.push(typeOrFn);
|
|
194
|
-
return this;
|
|
195
|
-
}
|
|
196
|
-
const type = String(typeOrFn || '').toLowerCase();
|
|
197
|
-
if (!this.middleware[type]) this.middleware[type] = [];
|
|
198
|
-
this.middleware[type].push(maybeFn);
|
|
199
|
-
return this;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
async _runPipeline(type, messageData, finalHandler) {
|
|
203
|
-
const chain = [
|
|
204
|
-
...(this.middleware.any || []),
|
|
205
|
-
...(this.middleware[type] || []),
|
|
206
|
-
async (ctx) => await finalHandler(ctx)
|
|
207
|
-
];
|
|
208
|
-
let idx = -1;
|
|
209
|
-
const runner = async (i) => {
|
|
210
|
-
if (i <= idx) throw new Error('next() called multiple times');
|
|
211
|
-
idx = i;
|
|
212
|
-
const fn = chain[i];
|
|
213
|
-
if (!fn) return;
|
|
214
|
-
return await fn(messageData, this, () => runner(i + 1));
|
|
215
|
-
};
|
|
216
|
-
return await runner(0);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
async _handleWithPipeline(type, handlerKey, messageData, fallback) {
|
|
220
|
-
const result = await this._runPipeline(type, messageData, async (ctx) => {
|
|
221
|
-
const handler = this.handlers[handlerKey];
|
|
222
|
-
if (handler) {
|
|
223
|
-
return await handler(ctx, this);
|
|
224
|
-
}
|
|
225
|
-
if (fallback) {
|
|
226
|
-
return await fallback(ctx);
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
return result;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
230
|
/*
|
|
233
231
|
* OUTBOUND MESSAGING
|
|
234
232
|
*/
|
|
@@ -238,62 +236,96 @@ class NexusMessaging {
|
|
|
238
236
|
|
|
239
237
|
if (messageData._fromConversationReply) messageData.processed = true;
|
|
240
238
|
|
|
241
|
-
if (messageData.contentSid && !messageData.body && this.provider.renderTemplate) {
|
|
242
|
-
const rendered = await this.provider.renderTemplate(messageData.contentSid, messageData.variables);
|
|
243
|
-
if (rendered) messageData.body = rendered;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
239
|
if (this._needsTemplateRoute(messageData) && !(await isWithin24HourWindow(messageData.code))) {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
:
|
|
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
|
+
{
|
|
250
249
|
...messageData,
|
|
251
|
-
provider: 'twilio',
|
|
252
|
-
|
|
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,
|
|
253
275
|
});
|
|
254
|
-
|
|
255
|
-
return
|
|
276
|
+
}
|
|
277
|
+
return result;
|
|
256
278
|
}
|
|
257
279
|
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
if (!
|
|
261
|
-
|
|
262
|
-
...messageData,
|
|
263
|
-
provider: 'twilio',
|
|
264
|
-
fromMe: true,
|
|
265
|
-
processed: messageData.processed ?? false
|
|
266
|
-
});
|
|
267
|
-
parentId = pending?._id || null;
|
|
280
|
+
const saveFn = this._saveFn;
|
|
281
|
+
|
|
282
|
+
if (!saveFn) {
|
|
283
|
+
return this._transport(messageData);
|
|
268
284
|
}
|
|
269
285
|
|
|
270
|
-
|
|
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
|
+
}
|
|
271
293
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
294
|
+
async sendAssistantReply(messageData) {
|
|
295
|
+
if (!this.provider) throw new Error('No provider initialized');
|
|
296
|
+
|
|
297
|
+
if (messageData.parentMessageId) {
|
|
298
|
+
return this._transport(messageData);
|
|
277
299
|
}
|
|
278
300
|
|
|
279
|
-
if (this.
|
|
280
|
-
await this.
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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;
|
|
287
312
|
}
|
|
288
313
|
|
|
289
|
-
const
|
|
290
|
-
if (chatId) ensureThreadExists(chatId);
|
|
314
|
+
const saveFn = this._saveFn;
|
|
291
315
|
|
|
292
|
-
|
|
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
|
+
);
|
|
293
325
|
}
|
|
294
326
|
|
|
295
327
|
_needsTemplateRoute(messageData) {
|
|
296
|
-
if (this.
|
|
328
|
+
if (!this._providerStores) return false;
|
|
297
329
|
if (messageData.contentSid) return false;
|
|
298
330
|
if (!messageData.code) return false;
|
|
299
331
|
return true;
|
|
@@ -306,10 +338,11 @@ class NexusMessaging {
|
|
|
306
338
|
if (!scheduledMessage?.sendTime) {
|
|
307
339
|
throw new Error('sendScheduledMessage requires sendTime');
|
|
308
340
|
}
|
|
309
|
-
|
|
341
|
+
const job = await this.scheduledMessageJob.schedule({
|
|
310
342
|
scheduledMessageId: scheduledMessage._id,
|
|
311
343
|
sendTime: scheduledMessage.sendTime
|
|
312
344
|
});
|
|
345
|
+
return { ...(job || {}), delivered: false, deferred: true };
|
|
313
346
|
}
|
|
314
347
|
|
|
315
348
|
async scheduleTemplateApproval({ templateSid, messageId, originalMessageSid = null, attempt = 0 }) {
|
|
@@ -326,8 +359,6 @@ class NexusMessaging {
|
|
|
326
359
|
*/
|
|
327
360
|
|
|
328
361
|
async processIncomingMessage(messageData) {
|
|
329
|
-
if (this.messageStorage) await this.messageStorage.saveMessage(messageData);
|
|
330
|
-
|
|
331
362
|
const chatId = messageData.code;
|
|
332
363
|
|
|
333
364
|
if (chatId) {
|
|
@@ -339,41 +370,52 @@ class NexusMessaging {
|
|
|
339
370
|
}
|
|
340
371
|
}
|
|
341
372
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
+
);
|
|
356
392
|
}
|
|
357
393
|
|
|
358
394
|
async handleMessage(messageData) {
|
|
359
|
-
return await this.
|
|
360
|
-
|
|
361
|
-
});
|
|
395
|
+
if (this.handlers.onMessage) return await this.handlers.onMessage(messageData, this);
|
|
396
|
+
return await this.handleMessageWithAssistant(messageData);
|
|
362
397
|
}
|
|
363
398
|
|
|
364
399
|
async handleInteractive(messageData) {
|
|
365
|
-
return this.
|
|
400
|
+
if (this.handlers.onInteractive) return await this.handlers.onInteractive(messageData, this);
|
|
366
401
|
}
|
|
367
402
|
|
|
368
403
|
async handleMedia(messageData) {
|
|
369
|
-
return await this.
|
|
370
|
-
|
|
371
|
-
});
|
|
404
|
+
if (this.handlers.onMedia) return await this.handlers.onMedia(messageData, this);
|
|
405
|
+
return await this.handleMediaWithAssistant(messageData);
|
|
372
406
|
}
|
|
373
407
|
|
|
374
|
-
async handleCommand(messageData) {
|
|
375
|
-
|
|
376
|
-
|
|
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
|
+
}
|
|
377
419
|
|
|
378
420
|
/*
|
|
379
421
|
* ASSISTANT INTEGRATION
|
|
@@ -444,6 +486,22 @@ class NexusMessaging {
|
|
|
444
486
|
if (!resolved) return null;
|
|
445
487
|
|
|
446
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
|
+
|
|
447
505
|
if (!preProcessed.shouldProcess) return null;
|
|
448
506
|
|
|
449
507
|
const result = await this.assistantProcessor.executeLLM(
|
|
@@ -461,55 +519,69 @@ class NexusMessaging {
|
|
|
461
519
|
|
|
462
520
|
async processInstruction(code, instruction, role = 'developer', { triggeredBy } = {}) {
|
|
463
521
|
const assistantId = await this._getThreadAssistantId(code);
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
522
|
+
const messageData = {
|
|
523
|
+
pushName: 'Instruction',
|
|
524
|
+
code,
|
|
467
525
|
body: instruction,
|
|
468
|
-
|
|
469
|
-
|
|
526
|
+
messageId: `instruction_${Date.now()}_${Math.random().toString(36).substring(7)}`,
|
|
527
|
+
fromMe: true,
|
|
470
528
|
processed: true,
|
|
471
529
|
origin: 'instruction',
|
|
472
|
-
|
|
530
|
+
assistantId,
|
|
473
531
|
raw: { role },
|
|
474
|
-
triggeredBy: triggeredBy || null
|
|
475
|
-
|
|
532
|
+
triggeredBy: triggeredBy || null,
|
|
533
|
+
silent: true,
|
|
534
|
+
};
|
|
476
535
|
|
|
477
|
-
const result = await this.
|
|
478
|
-
|
|
479
|
-
|
|
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({
|
|
480
541
|
code,
|
|
481
542
|
runOptions: {
|
|
482
543
|
prePromptResult: preProcessResult,
|
|
483
|
-
additionalInstructions:
|
|
484
|
-
additionalMessages: [{ role, content:
|
|
544
|
+
additionalInstructions: body,
|
|
545
|
+
additionalMessages: [{ role, content: body }],
|
|
485
546
|
toolChoice: 'none',
|
|
486
547
|
}
|
|
487
|
-
})
|
|
488
|
-
|
|
548
|
+
}),
|
|
549
|
+
),
|
|
489
550
|
);
|
|
490
551
|
|
|
491
552
|
return result?.output || null;
|
|
492
553
|
}
|
|
493
554
|
|
|
494
|
-
async processSystemMessage(code, messages, role = 'system', { triggeredBy } = {}) {
|
|
495
|
-
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
|
+
|
|
496
559
|
const assistantId = await this._getThreadAssistantId(code);
|
|
560
|
+
const normalizedMessages = Array.isArray(messages) ? messages : [messages];
|
|
497
561
|
|
|
498
562
|
for (let i = 0; i < normalizedMessages.length; i++) {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
+
}
|
|
511
581
|
}
|
|
512
582
|
|
|
583
|
+
if (!reply) return null;
|
|
584
|
+
|
|
513
585
|
const result = await this._executeWithPipeline(code, 'system', 'queue',
|
|
514
586
|
async (preProcessResult) => {
|
|
515
587
|
return await this.assistantProcessor.process({
|
|
@@ -532,7 +604,7 @@ class NexusMessaging {
|
|
|
532
604
|
|
|
533
605
|
async _processMessages(chatId, processingFn, shouldFinalize = () => true) {
|
|
534
606
|
const query = { numero: chatId, from_me: false, processed: false };
|
|
535
|
-
const unprocessed = await
|
|
607
|
+
const unprocessed = await getMessages(query, { select: '_id' });
|
|
536
608
|
|
|
537
609
|
try {
|
|
538
610
|
const result = await processingFn();
|
|
@@ -618,6 +690,8 @@ const processSystemMessage = async (code, messages, role, options) => {
|
|
|
618
690
|
|
|
619
691
|
const getEventBus = () => getDefaultInstance()?.getEventBus();
|
|
620
692
|
|
|
693
|
+
const getPhiProcessor = () => getDefaultInstance()?.getPhiProcessor();
|
|
694
|
+
|
|
621
695
|
const _resetDefaultInstance = () => { defaultInstance = null; };
|
|
622
696
|
|
|
623
697
|
module.exports = {
|
|
@@ -631,6 +705,7 @@ module.exports = {
|
|
|
631
705
|
getDefaultInstance,
|
|
632
706
|
requireDefaultInstance,
|
|
633
707
|
getProvider,
|
|
708
|
+
getPhiProcessor,
|
|
634
709
|
requireProvider,
|
|
635
710
|
getEventBus,
|
|
636
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
|
+
};
|