@peopl-health/nexus 1.5.2 → 1.5.4
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/lib/adapters/TwilioProvider.js +55 -18
- package/lib/core/NexusMessaging.js +72 -64
- package/lib/index.js +9 -14
- package/lib/services/conversationService.js +4 -1
- package/package.json +2 -2
|
@@ -151,30 +151,67 @@ class TwilioProvider extends MessageProvider {
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
/**
|
|
154
|
-
*
|
|
155
|
-
* Twilio SDK may not expose approval requests directly; return content and null approval by default.
|
|
154
|
+
* Check template approval status using Twilio Content API helpers
|
|
156
155
|
*/
|
|
157
156
|
async checkApprovalStatus(sid) {
|
|
157
|
+
if (!this.isConnected || !this.twilioClient) {
|
|
158
|
+
throw new Error('Twilio provider not initialized');
|
|
159
|
+
}
|
|
158
160
|
if (!sid) throw new Error('Content SID is required');
|
|
159
|
-
|
|
161
|
+
|
|
160
162
|
try {
|
|
161
|
-
const
|
|
162
|
-
let
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
163
|
+
const content = await this.twilioClient.content.v1.contents(sid).fetch();
|
|
164
|
+
let processedApprovalRequest = null;
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const approvalRequest = await this.twilioClient.content.v1
|
|
168
|
+
.contents(sid)
|
|
169
|
+
.approvalFetch()
|
|
170
|
+
.fetch();
|
|
171
|
+
|
|
172
|
+
if (approvalRequest) {
|
|
173
|
+
processedApprovalRequest = {
|
|
174
|
+
sid: approvalRequest.sid,
|
|
175
|
+
status: 'UNKNOWN'
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
if (approvalRequest.whatsapp) {
|
|
179
|
+
processedApprovalRequest.status = approvalRequest.whatsapp.status?.toUpperCase() || 'UNKNOWN';
|
|
180
|
+
processedApprovalRequest.category = approvalRequest.whatsapp.category;
|
|
181
|
+
processedApprovalRequest.name = approvalRequest.whatsapp.name;
|
|
182
|
+
processedApprovalRequest.rejectionReason = approvalRequest.whatsapp.rejection_reason;
|
|
183
|
+
processedApprovalRequest.contentType = approvalRequest.whatsapp.content_type;
|
|
184
|
+
} else {
|
|
185
|
+
processedApprovalRequest.status = approvalRequest.status || 'PENDING';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (processedApprovalRequest.status === 'approved') {
|
|
189
|
+
processedApprovalRequest.status = 'APPROVED';
|
|
190
|
+
} else if (processedApprovalRequest.status === 'rejected') {
|
|
191
|
+
processedApprovalRequest.status = 'REJECTED';
|
|
192
|
+
} else if (processedApprovalRequest.status === 'pending') {
|
|
193
|
+
processedApprovalRequest.status = 'PENDING';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (approvalRequest.dateCreated || approvalRequest.date_created) {
|
|
197
|
+
processedApprovalRequest.dateCreated = approvalRequest.dateCreated || approvalRequest.date_created;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (approvalRequest.dateUpdated || approvalRequest.date_updated) {
|
|
201
|
+
processedApprovalRequest.dateUpdated = approvalRequest.dateUpdated || approvalRequest.date_updated;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
} catch (approvalError) {
|
|
205
|
+
console.warn('Approval request fetch failed:', approvalError?.message || approvalError);
|
|
172
206
|
}
|
|
173
|
-
|
|
174
|
-
return {
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
content,
|
|
210
|
+
approvalRequest: processedApprovalRequest
|
|
211
|
+
};
|
|
175
212
|
} catch (error) {
|
|
176
|
-
|
|
177
|
-
|
|
213
|
+
console.error('Error checking approval status:', error);
|
|
214
|
+
throw error;
|
|
178
215
|
}
|
|
179
216
|
}
|
|
180
217
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const { airtable, getBase } = require('../config/airtableConfig');
|
|
2
|
-
const { convertTwilioToInternalFormat } = require('../helpers/twilioHelper');
|
|
3
2
|
const { replyAssistant } = require('../services/assistantService');
|
|
4
3
|
const { createProvider } = require('../adapters/registry');
|
|
5
4
|
|
|
@@ -27,7 +26,15 @@ class NexusMessaging {
|
|
|
27
26
|
onFlow: null
|
|
28
27
|
};
|
|
29
28
|
this.events = new EventEmitter();
|
|
30
|
-
this.middleware = {
|
|
29
|
+
this.middleware = {
|
|
30
|
+
any: [],
|
|
31
|
+
message: [],
|
|
32
|
+
interactive: [],
|
|
33
|
+
media: [],
|
|
34
|
+
command: [],
|
|
35
|
+
keyword: [],
|
|
36
|
+
flow: []
|
|
37
|
+
};
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
/**
|
|
@@ -118,7 +125,11 @@ class NexusMessaging {
|
|
|
118
125
|
* Internal helper to run middleware pipeline and the final handler
|
|
119
126
|
*/
|
|
120
127
|
async _runPipeline(type, messageData, finalHandler) {
|
|
121
|
-
const chain = [
|
|
128
|
+
const chain = [
|
|
129
|
+
...(this.middleware.any || []),
|
|
130
|
+
...(this.middleware[type] || []),
|
|
131
|
+
async (ctx) => await finalHandler(ctx)
|
|
132
|
+
];
|
|
122
133
|
let idx = -1;
|
|
123
134
|
const runner = async (i) => {
|
|
124
135
|
if (i <= idx) throw new Error('next() called multiple times');
|
|
@@ -316,32 +327,69 @@ class NexusMessaging {
|
|
|
316
327
|
}
|
|
317
328
|
}
|
|
318
329
|
|
|
319
|
-
async
|
|
320
|
-
this.events.emit
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
return await this.handleMessageWithAssistant(ctx);
|
|
330
|
+
async _handleWithPipeline(type, handlerKey, messageData, fallback) {
|
|
331
|
+
this.events.emit(`${type}:received`, messageData);
|
|
332
|
+
const result = await this._runPipeline(type, messageData, async (ctx) => {
|
|
333
|
+
const handler = this.handlers[handlerKey];
|
|
334
|
+
if (handler) {
|
|
335
|
+
return await handler(ctx, this);
|
|
326
336
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
337
|
+
if (fallback) {
|
|
338
|
+
return await fallback(ctx);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
this.events.emit(`${type}:handled`, messageData);
|
|
330
342
|
return result;
|
|
331
343
|
}
|
|
332
344
|
|
|
345
|
+
_extractAssistantInputs(messageData) {
|
|
346
|
+
if (!messageData || typeof messageData !== 'object') {
|
|
347
|
+
return { from: null, message: null };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const from = [
|
|
351
|
+
messageData.from,
|
|
352
|
+
messageData.sender,
|
|
353
|
+
messageData.numero,
|
|
354
|
+
messageData.code,
|
|
355
|
+
messageData.From,
|
|
356
|
+
messageData.key?.remoteJid,
|
|
357
|
+
messageData.raw?.from,
|
|
358
|
+
messageData.raw?.From,
|
|
359
|
+
messageData.raw?.key?.remoteJid
|
|
360
|
+
].find((value) => typeof value === 'string' && value.trim().length > 0) || null;
|
|
361
|
+
|
|
362
|
+
const message = [
|
|
363
|
+
typeof messageData.message === 'string' ? messageData.message : null,
|
|
364
|
+
typeof messageData.message?.conversation === 'string' ? messageData.message.conversation : null,
|
|
365
|
+
typeof messageData.Body === 'string' ? messageData.Body : null,
|
|
366
|
+
typeof messageData.raw?.message?.conversation === 'string' ? messageData.raw.message.conversation : null,
|
|
367
|
+
typeof messageData.raw?.Body === 'string' ? messageData.raw.Body : null
|
|
368
|
+
].find((value) => typeof value === 'string' && value.trim().length > 0) || null;
|
|
369
|
+
|
|
370
|
+
return { from, message };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async handleMessage(messageData) {
|
|
374
|
+
return await this._handleWithPipeline('message', 'onMessage', messageData, async (ctx) => {
|
|
375
|
+
return await this.handleMessageWithAssistant(ctx);
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
333
379
|
async handleMessageWithAssistant(messageData) {
|
|
334
380
|
try {
|
|
335
|
-
|
|
381
|
+
const { from, message } = this._extractAssistantInputs(messageData);
|
|
336
382
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
383
|
+
if (!from || !message) {
|
|
384
|
+
console.warn('Unable to resolve assistant inputs from message, skipping automatic reply.');
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const response = await replyAssistant(from, message);
|
|
341
389
|
|
|
342
390
|
if (response) {
|
|
343
391
|
await this.sendMessage({
|
|
344
|
-
to:
|
|
392
|
+
to: from,
|
|
345
393
|
message: response
|
|
346
394
|
});
|
|
347
395
|
}
|
|
@@ -354,63 +402,23 @@ class NexusMessaging {
|
|
|
354
402
|
if (this.messageStorage) {
|
|
355
403
|
await this.messageStorage.saveInteractive(messageData);
|
|
356
404
|
}
|
|
357
|
-
|
|
358
|
-
const final = async (ctx) => {
|
|
359
|
-
if (this.handlers.onInteractive) {
|
|
360
|
-
return await this.handlers.onInteractive(ctx, this);
|
|
361
|
-
}
|
|
362
|
-
};
|
|
363
|
-
const result = await this._runPipeline('interactive', messageData, final);
|
|
364
|
-
this.events.emit && this.events.emit('interactive:handled', messageData);
|
|
365
|
-
return result;
|
|
405
|
+
return await this._handleWithPipeline('interactive', 'onInteractive', messageData);
|
|
366
406
|
}
|
|
367
407
|
|
|
368
408
|
async handleMedia(messageData) {
|
|
369
|
-
|
|
370
|
-
const final = async (ctx) => {
|
|
371
|
-
if (this.handlers.onMedia) {
|
|
372
|
-
return await this.handlers.onMedia(ctx, this);
|
|
373
|
-
}
|
|
374
|
-
};
|
|
375
|
-
const result = await this._runPipeline('media', messageData, final);
|
|
376
|
-
this.events.emit && this.events.emit('media:handled', messageData);
|
|
377
|
-
return result;
|
|
409
|
+
return await this._handleWithPipeline('media', 'onMedia', messageData);
|
|
378
410
|
}
|
|
379
411
|
|
|
380
412
|
async handleCommand(messageData) {
|
|
381
|
-
|
|
382
|
-
const final = async (ctx) => {
|
|
383
|
-
if (this.handlers.onCommand) {
|
|
384
|
-
return await this.handlers.onCommand(ctx, this);
|
|
385
|
-
}
|
|
386
|
-
};
|
|
387
|
-
const result = await this._runPipeline('command', messageData, final);
|
|
388
|
-
this.events.emit && this.events.emit('command:handled', messageData);
|
|
389
|
-
return result;
|
|
413
|
+
return await this._handleWithPipeline('command', 'onCommand', messageData);
|
|
390
414
|
}
|
|
391
415
|
|
|
392
416
|
async handleKeyword(messageData) {
|
|
393
|
-
|
|
394
|
-
const final = async (ctx) => {
|
|
395
|
-
if (this.handlers.onKeyword) {
|
|
396
|
-
return await this.handlers.onKeyword(ctx, this);
|
|
397
|
-
}
|
|
398
|
-
};
|
|
399
|
-
const result = await this._runPipeline('keyword', messageData, final);
|
|
400
|
-
this.events.emit && this.events.emit('keyword:handled', messageData);
|
|
401
|
-
return result;
|
|
417
|
+
return await this._handleWithPipeline('keyword', 'onKeyword', messageData);
|
|
402
418
|
}
|
|
403
419
|
|
|
404
420
|
async handleFlow(messageData) {
|
|
405
|
-
|
|
406
|
-
const final = async (ctx) => {
|
|
407
|
-
if (this.handlers.onFlow) {
|
|
408
|
-
return await this.handlers.onFlow(ctx, this);
|
|
409
|
-
}
|
|
410
|
-
};
|
|
411
|
-
const result = await this._runPipeline('flow', messageData, final);
|
|
412
|
-
this.events.emit && this.events.emit('flow:handled', messageData);
|
|
413
|
-
return result;
|
|
421
|
+
return await this._handleWithPipeline('flow', 'onFlow', messageData);
|
|
414
422
|
}
|
|
415
423
|
|
|
416
424
|
/**
|
package/lib/index.js
CHANGED
|
@@ -67,10 +67,11 @@ class Nexus {
|
|
|
67
67
|
|
|
68
68
|
// Auto-configure template controllers with active provider when Twilio is used
|
|
69
69
|
try {
|
|
70
|
-
const activeProvider = typeof this.messaging.getProvider === 'function'
|
|
71
|
-
? this.messaging.getProvider()
|
|
70
|
+
const activeProvider = typeof this.messaging.getProvider === 'function'
|
|
71
|
+
? this.messaging.getProvider()
|
|
72
72
|
: null;
|
|
73
|
-
|
|
73
|
+
|
|
74
|
+
if (activeProvider instanceof TwilioProvider) {
|
|
74
75
|
if (typeof templateController.configureNexusProvider === 'function') {
|
|
75
76
|
templateController.configureNexusProvider(activeProvider);
|
|
76
77
|
}
|
|
@@ -129,28 +130,24 @@ class Nexus {
|
|
|
129
130
|
if (llm === 'openai') {
|
|
130
131
|
this.llmProvider = new DefaultLLMProvider(llmConfig);
|
|
131
132
|
try {
|
|
132
|
-
if (
|
|
133
|
+
if (typeof this.llmProvider.getClient === 'function') {
|
|
133
134
|
llmConfigModule.openaiClient = this.llmProvider.getClient();
|
|
134
135
|
}
|
|
135
136
|
} catch (err) {
|
|
136
137
|
console.warn('[Nexus] Failed to expose OpenAI client:', err?.message || err);
|
|
137
138
|
}
|
|
139
|
+
}
|
|
138
140
|
|
|
139
141
|
// Convenience: handle common top-level config for mongo, media bucket, airtable
|
|
140
142
|
try {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (typeof storageConfig === 'object') {
|
|
144
|
-
storageConfig.mongoUri = storageConfig.mongoUri || options.mongoUri;
|
|
145
|
-
}
|
|
143
|
+
if (options.mongoUri && storage === 'mongo' && typeof storageConfig === 'object') {
|
|
144
|
+
storageConfig.mongoUri = storageConfig.mongoUri || options.mongoUri;
|
|
146
145
|
}
|
|
147
146
|
|
|
148
|
-
// Media bucket (overrides storage setting)
|
|
149
147
|
if (options.media && options.media.bucketName) {
|
|
150
148
|
runtimeConfig.set('AWS_S3_BUCKET_NAME', options.media.bucketName);
|
|
151
149
|
}
|
|
152
150
|
|
|
153
|
-
// Airtable base default (accepts alias like 'calendar' or an ID)
|
|
154
151
|
if (options.airtable && options.airtable.base) {
|
|
155
152
|
runtimeConfig.set('AIRTABLE_BASE_ID', options.airtable.base);
|
|
156
153
|
}
|
|
@@ -161,13 +158,11 @@ class Nexus {
|
|
|
161
158
|
console.warn('[Nexus] convenience config warning:', cfgErr?.message || cfgErr);
|
|
162
159
|
}
|
|
163
160
|
|
|
164
|
-
}
|
|
165
|
-
|
|
166
161
|
|
|
167
162
|
// Configure Assistants (registry + overrides)
|
|
168
163
|
const assistantsConfig = assistantsOpt || assistantOpt;
|
|
169
164
|
try {
|
|
170
|
-
if (this.llmProvider && typeof configureAssistantsLLM === 'function') {
|
|
165
|
+
if (this.llmProvider && typeof configureAssistantsLLM === 'function' && typeof this.llmProvider.getClient === 'function') {
|
|
171
166
|
// Provide the raw OpenAI client to the assistant service
|
|
172
167
|
configureAssistantsLLM(this.llmProvider.getClient());
|
|
173
168
|
}
|
|
@@ -56,6 +56,7 @@ const fetchConversationData = async (filter, skip, limit) => {
|
|
|
56
56
|
nombre_whatsapp: 1,
|
|
57
57
|
from_me: 1
|
|
58
58
|
}},
|
|
59
|
+
{ $sort: { createdAt: 1, timestamp: 1 } },
|
|
59
60
|
{ $group: {
|
|
60
61
|
_id: '$numero',
|
|
61
62
|
latestMessage: { $last: '$$ROOT' },
|
|
@@ -169,8 +170,10 @@ const fetchConversationData = async (filter, skip, limit) => {
|
|
|
169
170
|
{ $project: {
|
|
170
171
|
numero: 1,
|
|
171
172
|
from_me: 1,
|
|
172
|
-
createdAt: 1
|
|
173
|
+
createdAt: 1,
|
|
174
|
+
timestamp: 1
|
|
173
175
|
}},
|
|
176
|
+
{ $sort: { createdAt: -1, timestamp: -1 } },
|
|
174
177
|
{ $group: {
|
|
175
178
|
_id: '$numero',
|
|
176
179
|
latestMessage: { $first: '$$ROOT' }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peopl-health/nexus",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.4",
|
|
4
4
|
"description": "Core messaging and assistant library for WhatsApp communication platforms",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"baileys": "^6.4.0",
|
|
68
68
|
"express": "4.21.2",
|
|
69
69
|
"openai": "^4.0.0",
|
|
70
|
-
"twilio": "
|
|
70
|
+
"twilio": "5.6.0"
|
|
71
71
|
},
|
|
72
72
|
"devDependencies": {
|
|
73
73
|
"@types/node": "^20.5.0",
|