@peopl-health/nexus 1.5.1 → 1.5.3
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/core/NexusMessaging.js +73 -63
- package/lib/index.js +9 -14
- package/lib/services/conversationService.js +4 -1
- package/package.json +1 -1
|
@@ -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,30 +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
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
381
|
+
const { from, message } = this._extractAssistantInputs(messageData);
|
|
382
|
+
|
|
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);
|
|
339
389
|
|
|
340
390
|
if (response) {
|
|
341
391
|
await this.sendMessage({
|
|
342
|
-
to:
|
|
392
|
+
to: from,
|
|
343
393
|
message: response
|
|
344
394
|
});
|
|
345
395
|
}
|
|
@@ -352,63 +402,23 @@ class NexusMessaging {
|
|
|
352
402
|
if (this.messageStorage) {
|
|
353
403
|
await this.messageStorage.saveInteractive(messageData);
|
|
354
404
|
}
|
|
355
|
-
|
|
356
|
-
const final = async (ctx) => {
|
|
357
|
-
if (this.handlers.onInteractive) {
|
|
358
|
-
return await this.handlers.onInteractive(ctx, this);
|
|
359
|
-
}
|
|
360
|
-
};
|
|
361
|
-
const result = await this._runPipeline('interactive', messageData, final);
|
|
362
|
-
this.events.emit && this.events.emit('interactive:handled', messageData);
|
|
363
|
-
return result;
|
|
405
|
+
return await this._handleWithPipeline('interactive', 'onInteractive', messageData);
|
|
364
406
|
}
|
|
365
407
|
|
|
366
408
|
async handleMedia(messageData) {
|
|
367
|
-
|
|
368
|
-
const final = async (ctx) => {
|
|
369
|
-
if (this.handlers.onMedia) {
|
|
370
|
-
return await this.handlers.onMedia(ctx, this);
|
|
371
|
-
}
|
|
372
|
-
};
|
|
373
|
-
const result = await this._runPipeline('media', messageData, final);
|
|
374
|
-
this.events.emit && this.events.emit('media:handled', messageData);
|
|
375
|
-
return result;
|
|
409
|
+
return await this._handleWithPipeline('media', 'onMedia', messageData);
|
|
376
410
|
}
|
|
377
411
|
|
|
378
412
|
async handleCommand(messageData) {
|
|
379
|
-
|
|
380
|
-
const final = async (ctx) => {
|
|
381
|
-
if (this.handlers.onCommand) {
|
|
382
|
-
return await this.handlers.onCommand(ctx, this);
|
|
383
|
-
}
|
|
384
|
-
};
|
|
385
|
-
const result = await this._runPipeline('command', messageData, final);
|
|
386
|
-
this.events.emit && this.events.emit('command:handled', messageData);
|
|
387
|
-
return result;
|
|
413
|
+
return await this._handleWithPipeline('command', 'onCommand', messageData);
|
|
388
414
|
}
|
|
389
415
|
|
|
390
416
|
async handleKeyword(messageData) {
|
|
391
|
-
|
|
392
|
-
const final = async (ctx) => {
|
|
393
|
-
if (this.handlers.onKeyword) {
|
|
394
|
-
return await this.handlers.onKeyword(ctx, this);
|
|
395
|
-
}
|
|
396
|
-
};
|
|
397
|
-
const result = await this._runPipeline('keyword', messageData, final);
|
|
398
|
-
this.events.emit && this.events.emit('keyword:handled', messageData);
|
|
399
|
-
return result;
|
|
417
|
+
return await this._handleWithPipeline('keyword', 'onKeyword', messageData);
|
|
400
418
|
}
|
|
401
419
|
|
|
402
420
|
async handleFlow(messageData) {
|
|
403
|
-
|
|
404
|
-
const final = async (ctx) => {
|
|
405
|
-
if (this.handlers.onFlow) {
|
|
406
|
-
return await this.handlers.onFlow(ctx, this);
|
|
407
|
-
}
|
|
408
|
-
};
|
|
409
|
-
const result = await this._runPipeline('flow', messageData, final);
|
|
410
|
-
this.events.emit && this.events.emit('flow:handled', messageData);
|
|
411
|
-
return result;
|
|
421
|
+
return await this._handleWithPipeline('flow', 'onFlow', messageData);
|
|
412
422
|
}
|
|
413
423
|
|
|
414
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' }
|