@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
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
|
|
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
|
-
##
|
|
111
|
+
## Events
|
|
112
112
|
|
|
113
|
-
|
|
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
|
|
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>}
|
|
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<
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
92
|
-
const messages = await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
400
|
-
|
|
401
|
-
|
|
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
|
|
563
|
-
const messages = await
|
|
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
|
|
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
|
|
219
|
-
$or: [{ numero: normalizedCode }, { group_id: normalizedCode }]
|
|
220
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
119
|
-
numero: chatId, from_me: false, processed: false,
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|