@peopl-health/nexus 1.2.0 → 1.3.1
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/MIGRATION_GUIDE.md +71 -379
- package/README.md +80 -559
- package/lib/adapters/BaileysProvider.js +25 -1
- package/lib/adapters/TwilioProvider.js +107 -0
- package/lib/adapters/registry.js +29 -0
- package/lib/config/configLoader.js +38 -0
- package/lib/controllers/assistantController.js +6 -6
- package/lib/controllers/messageController.js +68 -21
- package/lib/controllers/templateController.js +5 -17
- package/lib/core/NexusMessaging.js +151 -35
- package/lib/helpers/assistantHelper.js +6 -6
- package/lib/helpers/baileysHelper.js +3 -3
- package/lib/helpers/twilioHelper.js +3 -3
- package/lib/index.d.ts +1 -1
- package/lib/index.js +84 -9
- package/lib/interactive/index.js +86 -0
- package/lib/interactive/registry.js +31 -0
- package/lib/interactive/twilioMapper.js +60 -0
- package/lib/models/agendaMessageModel.js +24 -0
- package/lib/models/messageModel.js +8 -0
- package/lib/routes/index.js +3 -1
- package/lib/services/airtableService.js +4 -0
- package/lib/services/assistantService.js +15 -2
- package/lib/storage/NoopStorage.js +19 -0
- package/lib/storage/registry.js +31 -0
- package/lib/templates/predefinedTemplates.js +1 -3
- package/lib/utils/defaultLLMProvider.js +1 -1
- package/lib/utils/index.js +3 -5
- package/package.json +2 -1
- package/lib/services/appointmentService.js +0 -55
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
const { TwilioProvider } = require('../adapters/TwilioProvider');
|
|
2
|
-
const { BaileysProvider } = require('../adapters/BaileysProvider');
|
|
3
|
-
const mongoose = require('mongoose');
|
|
4
1
|
const { airtable, getBase } = require('../config/airtableConfig');
|
|
2
|
+
const { convertTwilioToInternalFormat } = require('../helpers/twilioHelper');
|
|
3
|
+
const { replyAssistant } = require('../services/assistantService');
|
|
4
|
+
const { createProvider } = require('../adapters/registry');
|
|
5
|
+
|
|
6
|
+
const mongoose = require('mongoose');
|
|
5
7
|
const OpenAI = require('openai');
|
|
8
|
+
const EventEmitter = require('events');
|
|
6
9
|
|
|
7
10
|
/**
|
|
8
11
|
* Core messaging class that manages providers and message handling
|
|
@@ -23,6 +26,8 @@ class NexusMessaging {
|
|
|
23
26
|
onKeyword: null,
|
|
24
27
|
onFlow: null
|
|
25
28
|
};
|
|
29
|
+
this.events = new EventEmitter();
|
|
30
|
+
this.middleware = { any: [], message: [], interactive: [], media: [], command: [], keyword: [], flow: [] };
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
/**
|
|
@@ -79,6 +84,54 @@ class NexusMessaging {
|
|
|
79
84
|
return this.llmProvider;
|
|
80
85
|
}
|
|
81
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Get the underlying messaging provider instance
|
|
89
|
+
*/
|
|
90
|
+
getProvider() {
|
|
91
|
+
return this.provider;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Access the internal event bus (EventEmitter)
|
|
96
|
+
*/
|
|
97
|
+
getEventBus() {
|
|
98
|
+
return this.events;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Register middleware. Usage: use(type, fn) or use(fn) for global.
|
|
103
|
+
* Middleware signature: async (messageData, nexus, next) => {}
|
|
104
|
+
*/
|
|
105
|
+
use(typeOrFn, maybeFn) {
|
|
106
|
+
if (typeof typeOrFn === 'function') {
|
|
107
|
+
this.middleware.any.push(typeOrFn);
|
|
108
|
+
return this;
|
|
109
|
+
}
|
|
110
|
+
const type = String(typeOrFn || '').toLowerCase();
|
|
111
|
+
const fn = maybeFn;
|
|
112
|
+
if (!this.middleware[type]) this.middleware[type] = [];
|
|
113
|
+
this.middleware[type].push(fn);
|
|
114
|
+
return this;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Internal helper to run middleware pipeline and the final handler
|
|
119
|
+
*/
|
|
120
|
+
async _runPipeline(type, messageData, finalHandler) {
|
|
121
|
+
const chain = [...(this.middleware.any || []), ...(this.middleware[type] || []), async (ctx) => { return finalHandler(ctx); }];
|
|
122
|
+
let idx = -1;
|
|
123
|
+
const runner = async (i) => {
|
|
124
|
+
if (i <= idx) throw new Error('next() called multiple times');
|
|
125
|
+
idx = i;
|
|
126
|
+
const fn = chain[i];
|
|
127
|
+
if (!fn) return;
|
|
128
|
+
return await fn(messageData, this, () => runner(i+1));
|
|
129
|
+
};
|
|
130
|
+
return await runner(0);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
|
|
82
135
|
/**
|
|
83
136
|
* Initialize Nexus with all services
|
|
84
137
|
* @param {Object} options - Configuration options
|
|
@@ -122,17 +175,7 @@ class NexusMessaging {
|
|
|
122
175
|
* @param {Object} providerConfig - Provider-specific configuration
|
|
123
176
|
*/
|
|
124
177
|
async initializeProvider(providerType, providerConfig) {
|
|
125
|
-
|
|
126
|
-
case 'twilio':
|
|
127
|
-
this.provider = new TwilioProvider(providerConfig);
|
|
128
|
-
break;
|
|
129
|
-
case 'baileys':
|
|
130
|
-
this.provider = new BaileysProvider(providerConfig);
|
|
131
|
-
break;
|
|
132
|
-
default:
|
|
133
|
-
throw new Error(`Unsupported provider: ${providerType}`);
|
|
134
|
-
}
|
|
135
|
-
|
|
178
|
+
this.provider = createProvider(providerType, providerConfig);
|
|
136
179
|
await this.provider.initialize();
|
|
137
180
|
}
|
|
138
181
|
|
|
@@ -209,12 +252,18 @@ class NexusMessaging {
|
|
|
209
252
|
throw new Error('No provider initialized');
|
|
210
253
|
}
|
|
211
254
|
|
|
212
|
-
|
|
255
|
+
// Backward compatibility: accept `code` as destination
|
|
256
|
+
const normalized = { ...messageData };
|
|
257
|
+
if (!normalized.to && normalized.code) {
|
|
258
|
+
normalized.to = normalized.code;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const result = await this.provider.sendMessage(normalized);
|
|
213
262
|
|
|
214
263
|
// Store message if storage is configured
|
|
215
264
|
if (this.messageStorage) {
|
|
216
265
|
await this.messageStorage.saveMessage({
|
|
217
|
-
...
|
|
266
|
+
...normalized,
|
|
218
267
|
messageId: result.messageId,
|
|
219
268
|
provider: result.provider,
|
|
220
269
|
timestamp: new Date(),
|
|
@@ -268,44 +317,111 @@ class NexusMessaging {
|
|
|
268
317
|
}
|
|
269
318
|
|
|
270
319
|
async handleMessage(messageData) {
|
|
271
|
-
|
|
272
|
-
|
|
320
|
+
this.events.emit && this.events.emit('message:received', messageData);
|
|
321
|
+
const final = async (ctx) => {
|
|
322
|
+
if (this.handlers.onMessage) {
|
|
323
|
+
return await this.handlers.onMessage(ctx, this);
|
|
324
|
+
} else {
|
|
325
|
+
return await this.handleMessageWithAssistant(ctx);
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
const result = await this._runPipeline('message', messageData, final);
|
|
329
|
+
this.events.emit && this.events.emit('message:handled', messageData);
|
|
330
|
+
return result;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async handleMessageWithAssistant(messageData) {
|
|
334
|
+
try {
|
|
335
|
+
// Convert Twilio format to internal format if needed
|
|
336
|
+
const internalMessage = this.provider.constructor.name === 'TwilioProvider'
|
|
337
|
+
? convertTwilioToInternalFormat(messageData)
|
|
338
|
+
: messageData;
|
|
339
|
+
|
|
340
|
+
// Extract standardized data
|
|
341
|
+
const extractedData = {
|
|
342
|
+
from: internalMessage.key?.remoteJid || '',
|
|
343
|
+
message: internalMessage.message?.conversation || '',
|
|
344
|
+
messageId: internalMessage.key?.id || '',
|
|
345
|
+
fromMe: internalMessage.key?.fromMe || false
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const response = await replyAssistant(
|
|
349
|
+
extractedData.from,
|
|
350
|
+
extractedData.message
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
if (response) {
|
|
354
|
+
await this.sendMessage({
|
|
355
|
+
to: extractedData.from,
|
|
356
|
+
message: response
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
} catch (error) {
|
|
360
|
+
console.error('Error in handleMessageWithAssistant:', error);
|
|
273
361
|
}
|
|
274
362
|
}
|
|
275
363
|
|
|
276
364
|
async handleInteractive(messageData) {
|
|
277
|
-
// Store interactive message
|
|
278
365
|
if (this.messageStorage) {
|
|
279
366
|
await this.messageStorage.saveInteractive(messageData);
|
|
280
367
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
368
|
+
this.events.emit && this.events.emit('interactive:received', messageData);
|
|
369
|
+
const final = async (ctx) => {
|
|
370
|
+
if (this.handlers.onInteractive) {
|
|
371
|
+
return await this.handlers.onInteractive(ctx, this);
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
const result = await this._runPipeline('interactive', messageData, final);
|
|
375
|
+
this.events.emit && this.events.emit('interactive:handled', messageData);
|
|
376
|
+
return result;
|
|
285
377
|
}
|
|
286
378
|
|
|
287
379
|
async handleMedia(messageData) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
380
|
+
this.events.emit && this.events.emit('media:received', messageData);
|
|
381
|
+
const final = async (ctx) => {
|
|
382
|
+
if (this.handlers.onMedia) {
|
|
383
|
+
return await this.handlers.onMedia(ctx, this);
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
const result = await this._runPipeline('media', messageData, final);
|
|
387
|
+
this.events.emit && this.events.emit('media:handled', messageData);
|
|
388
|
+
return result;
|
|
291
389
|
}
|
|
292
390
|
|
|
293
391
|
async handleCommand(messageData) {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
392
|
+
this.events.emit && this.events.emit('command:received', messageData);
|
|
393
|
+
const final = async (ctx) => {
|
|
394
|
+
if (this.handlers.onCommand) {
|
|
395
|
+
return await this.handlers.onCommand(ctx, this);
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
const result = await this._runPipeline('command', messageData, final);
|
|
399
|
+
this.events.emit && this.events.emit('command:handled', messageData);
|
|
400
|
+
return result;
|
|
297
401
|
}
|
|
298
402
|
|
|
299
403
|
async handleKeyword(messageData) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
404
|
+
this.events.emit && this.events.emit('keyword:received', messageData);
|
|
405
|
+
const final = async (ctx) => {
|
|
406
|
+
if (this.handlers.onKeyword) {
|
|
407
|
+
return await this.handlers.onKeyword(ctx, this);
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
const result = await this._runPipeline('keyword', messageData, final);
|
|
411
|
+
this.events.emit && this.events.emit('keyword:handled', messageData);
|
|
412
|
+
return result;
|
|
303
413
|
}
|
|
304
414
|
|
|
305
415
|
async handleFlow(messageData) {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
416
|
+
this.events.emit && this.events.emit('flow:received', messageData);
|
|
417
|
+
const final = async (ctx) => {
|
|
418
|
+
if (this.handlers.onFlow) {
|
|
419
|
+
return await this.handlers.onFlow(ctx, this);
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
const result = await this._runPipeline('flow', messageData, final);
|
|
423
|
+
this.events.emit && this.events.emit('flow:handled', messageData);
|
|
424
|
+
return result;
|
|
309
425
|
}
|
|
310
426
|
|
|
311
427
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const { downloadFileFromS3, generatePresignedUrl } = require('../config/awsConfig.js');
|
|
2
2
|
const { openaiClient } = require('../config/llmConfig.js');
|
|
3
3
|
|
|
4
|
-
const {
|
|
4
|
+
const { Message } = require('../models/messageModel.js');
|
|
5
5
|
|
|
6
6
|
const { convertPdfToImages } = require('./filesHelper.js');
|
|
7
7
|
const { analyzeImage } = require('../helpers/llmsHelper.js');
|
|
@@ -78,7 +78,7 @@ async function getLastMessages(code) {
|
|
|
78
78
|
query.is_group = false;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
const lastMessages = await
|
|
81
|
+
const lastMessages = await Message.find(query).sort({ timestamp: -1 });
|
|
82
82
|
console.log('[getLastMessages] lastMessages', lastMessages.map(msg => msg.body).join('\n\n'));
|
|
83
83
|
|
|
84
84
|
if (lastMessages.length === 0) return [];
|
|
@@ -86,7 +86,7 @@ async function getLastMessages(code) {
|
|
|
86
86
|
let patientReply = [];
|
|
87
87
|
for (const message of lastMessages) {
|
|
88
88
|
patientReply.push(message);
|
|
89
|
-
await
|
|
89
|
+
await Message.updateOne(
|
|
90
90
|
{ message_id: message.message_id, timestamp: message.timestamp },
|
|
91
91
|
{ $set: { processed: true } }
|
|
92
92
|
);
|
|
@@ -101,7 +101,7 @@ async function getLastMessages(code) {
|
|
|
101
101
|
|
|
102
102
|
async function getLastNMessages(code, n) {
|
|
103
103
|
try {
|
|
104
|
-
const lastMessages = await
|
|
104
|
+
const lastMessages = await Message.find({ numero: code })
|
|
105
105
|
.sort({ timestamp: -1 })
|
|
106
106
|
.limit(n);
|
|
107
107
|
|
|
@@ -149,7 +149,7 @@ function formatMessage(reply) {
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
async function downloadMediaAndCreateFile(code, reply) {
|
|
152
|
-
const resultMedia = await
|
|
152
|
+
const resultMedia = await Message.findOne({
|
|
153
153
|
message_id: reply.message_id,
|
|
154
154
|
timestamp: reply.timestamp,
|
|
155
155
|
media: { $ne: null }
|
|
@@ -259,7 +259,7 @@ async function processMessage(code, reply, thread) {
|
|
|
259
259
|
|
|
260
260
|
console.log('Formatted message:', formattedMessage);
|
|
261
261
|
|
|
262
|
-
await
|
|
262
|
+
await Message.updateOne(
|
|
263
263
|
{ message_id: reply.message_id, timestamp: reply.timestamp },
|
|
264
264
|
{ $set: { assistant_id: thread.assistant_id, thread_id: thread.thread_id } }
|
|
265
265
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { Message, insertMessage, getMessageValues } = require('../models/messageModel.js');
|
|
2
2
|
const { uploadMediaToS3 } = require('./mediaHelper.js');
|
|
3
3
|
const { downloadMediaMessage } = require('baileys');
|
|
4
4
|
|
|
@@ -103,7 +103,7 @@ function extractContentTypeAndReply(message, messageType) {
|
|
|
103
103
|
async function isRecentMessage(chatId) {
|
|
104
104
|
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
|
|
105
105
|
|
|
106
|
-
const recentMessage = await
|
|
106
|
+
const recentMessage = await Message.find({
|
|
107
107
|
$or: [{ group_id: chatId }, { numero: chatId }],
|
|
108
108
|
timestamp: { $gte: fiveMinutesAgo.toISOString() }
|
|
109
109
|
}).sort({ timestamp: -1 }).limit(1);
|
|
@@ -112,7 +112,7 @@ async function isRecentMessage(chatId) {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
async function getLastMessages(chatId, n) {
|
|
115
|
-
const messages = await
|
|
115
|
+
const messages = await Message.find({ group_id: chatId })
|
|
116
116
|
.sort({ timestamp: -1 })
|
|
117
117
|
.limit(n)
|
|
118
118
|
.select('timestamp numero nombre_whatsapp body');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { Message } = require('../models/messageModel');
|
|
2
2
|
|
|
3
3
|
const axios = require('axios');
|
|
4
4
|
const { v4: uuidv4 } = require('uuid');
|
|
@@ -68,7 +68,7 @@ function extractTitle(message, mediaType) {
|
|
|
68
68
|
async function isRecentMessage(chatId) {
|
|
69
69
|
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
|
|
70
70
|
|
|
71
|
-
const recentMessage = await
|
|
71
|
+
const recentMessage = await Message.find({
|
|
72
72
|
$or: [{ group_id: chatId }, { numero: chatId }],
|
|
73
73
|
timestamp: { $gte: fiveMinutesAgo.toISOString() }
|
|
74
74
|
}).sort({ timestamp: -1 }).limit(1);
|
|
@@ -78,7 +78,7 @@ async function isRecentMessage(chatId) {
|
|
|
78
78
|
|
|
79
79
|
|
|
80
80
|
async function getLastMessages(chatId, n) {
|
|
81
|
-
const messages = await
|
|
81
|
+
const messages = await Message.find({ numero: chatId })
|
|
82
82
|
.sort({ timestamp: -1 })
|
|
83
83
|
.limit(n)
|
|
84
84
|
.select('timestamp numero nombre_whatsapp body');
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
const { NexusMessaging } = require('./core/NexusMessaging');
|
|
2
|
-
const { TwilioProvider } = require('./adapters/TwilioProvider');
|
|
3
|
-
const { BaileysProvider } = require('./adapters/BaileysProvider');
|
|
4
2
|
const { MongoStorage } = require('./storage/MongoStorage');
|
|
5
3
|
const { MessageParser } = require('./utils/messageParser');
|
|
6
4
|
const { DefaultLLMProvider } = require('./utils/defaultLLMProvider');
|
|
5
|
+
const { loadNexusConfig } = require('./config/configLoader');
|
|
6
|
+
const templateController = require('./controllers/templateController');
|
|
7
|
+
const templateFlowController = require('./controllers/templateFlowController');
|
|
8
|
+
const interactive = require('./interactive');
|
|
9
|
+
const {
|
|
10
|
+
configureLLMProvider: configureAssistantsLLM,
|
|
11
|
+
registerAssistant,
|
|
12
|
+
overrideGetAssistantById,
|
|
13
|
+
configureAssistants: setAssistantsConfig
|
|
14
|
+
} = require('./services/assistantService');
|
|
15
|
+
const { TwilioProvider } = require('./adapters/TwilioProvider');
|
|
16
|
+
const { BaileysProvider } = require('./adapters/BaileysProvider');
|
|
7
17
|
|
|
8
18
|
/**
|
|
9
19
|
* Main Nexus class that orchestrates all components
|
|
@@ -40,17 +50,50 @@ class Nexus {
|
|
|
40
50
|
parser = 'MessageParser',
|
|
41
51
|
parserConfig = {},
|
|
42
52
|
llm = 'openai',
|
|
43
|
-
llmConfig = {}
|
|
53
|
+
llmConfig = {},
|
|
54
|
+
assistants: assistantsOpt = undefined,
|
|
55
|
+
assistant: assistantOpt = undefined
|
|
44
56
|
} = options;
|
|
45
57
|
|
|
46
58
|
// Initialize messaging provider
|
|
47
59
|
await this.messaging.initializeProvider(provider, providerConfig);
|
|
48
60
|
|
|
61
|
+
// Auto-configure template controllers with active provider when Twilio is used
|
|
62
|
+
try {
|
|
63
|
+
const activeProvider = typeof this.messaging.getProvider === 'function'
|
|
64
|
+
? this.messaging.getProvider()
|
|
65
|
+
: null;
|
|
66
|
+
if (activeProvider && activeProvider.constructor && activeProvider.constructor.name === 'TwilioProvider') {
|
|
67
|
+
if (typeof templateController.configureNexusProvider === 'function') {
|
|
68
|
+
templateController.configureNexusProvider(activeProvider);
|
|
69
|
+
}
|
|
70
|
+
if (typeof templateFlowController.configureNexusProvider === 'function') {
|
|
71
|
+
templateFlowController.configureNexusProvider(activeProvider);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch (e) {
|
|
75
|
+
console.warn('Warning: failed to auto-configure template providers:', e?.message || e);
|
|
76
|
+
}
|
|
77
|
+
|
|
49
78
|
// Initialize storage if provided
|
|
50
|
-
if (storage
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
79
|
+
if (storage) {
|
|
80
|
+
try {
|
|
81
|
+
const { createStorage } = require('./storage/registry');
|
|
82
|
+
if (typeof storage === 'string') {
|
|
83
|
+
this.storage = createStorage(storage, storageConfig || {});
|
|
84
|
+
} else if (typeof storage === 'object' && (storage.saveMessage || storage.saveInteractive)) {
|
|
85
|
+
this.storage = storage; // external adapter instance
|
|
86
|
+
} else {
|
|
87
|
+
// default to mongo if truthy but not recognized
|
|
88
|
+
this.storage = createStorage('mongo', storageConfig || {});
|
|
89
|
+
}
|
|
90
|
+
if (this.storage && typeof this.storage.connect === 'function') {
|
|
91
|
+
await this.storage.connect();
|
|
92
|
+
}
|
|
93
|
+
this.messaging.setMessageStorage(this.storage);
|
|
94
|
+
} catch (e) {
|
|
95
|
+
console.warn('Warning: storage initialization failed:', e?.message || e);
|
|
96
|
+
}
|
|
54
97
|
}
|
|
55
98
|
|
|
56
99
|
// Initialize message parser if provided
|
|
@@ -63,6 +106,31 @@ class Nexus {
|
|
|
63
106
|
this.llmProvider = new DefaultLLMProvider(llmConfig);
|
|
64
107
|
}
|
|
65
108
|
|
|
109
|
+
|
|
110
|
+
// Configure Assistants (registry + overrides)
|
|
111
|
+
const assistantsConfig = assistantsOpt || assistantOpt;
|
|
112
|
+
try {
|
|
113
|
+
if (this.llmProvider && typeof configureAssistantsLLM === 'function') {
|
|
114
|
+
// Provide the raw OpenAI client to the assistant service
|
|
115
|
+
configureAssistantsLLM(this.llmProvider.getClient());
|
|
116
|
+
}
|
|
117
|
+
if (assistantsConfig) {
|
|
118
|
+
if (assistantsConfig.registry && typeof assistantsConfig.registry === 'object') {
|
|
119
|
+
for (const [id, AssistantClass] of Object.entries(assistantsConfig.registry)) {
|
|
120
|
+
registerAssistant(id, AssistantClass);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (typeof assistantsConfig.getAssistantById === 'function') {
|
|
124
|
+
overrideGetAssistantById(assistantsConfig.getAssistantById);
|
|
125
|
+
}
|
|
126
|
+
if (typeof setAssistantsConfig === 'function') {
|
|
127
|
+
setAssistantsConfig(assistantsConfig);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} catch (e) {
|
|
131
|
+
console.warn('Warning: failed to configure assistants:', e?.message || e);
|
|
132
|
+
}
|
|
133
|
+
|
|
66
134
|
this.isInitialized = true;
|
|
67
135
|
}
|
|
68
136
|
|
|
@@ -193,7 +261,14 @@ module.exports = {
|
|
|
193
261
|
MessageParser,
|
|
194
262
|
DefaultLLMProvider,
|
|
195
263
|
routes,
|
|
196
|
-
// Direct access to route utilities for convenience
|
|
197
264
|
setupDefaultRoutes: routes.setupDefaultRoutes,
|
|
198
|
-
createRouter: routes.createRouter
|
|
265
|
+
createRouter: routes.createRouter,
|
|
266
|
+
loadNexusConfig,
|
|
267
|
+
interactive,
|
|
268
|
+
registerFlow: interactive.registerFlow,
|
|
269
|
+
getFlow: interactive.getFlow,
|
|
270
|
+
listFlows: interactive.listFlows,
|
|
271
|
+
sendInteractive: interactive.sendInteractive,
|
|
272
|
+
registerInteractiveHandler: interactive.registerInteractiveHandler,
|
|
273
|
+
attachInteractiveRouter: interactive.attachInteractiveRouter
|
|
199
274
|
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
|
|
2
|
+
const { toTwilioContent } = require('./twilioMapper');
|
|
3
|
+
const { registerFlow, getFlow, listFlows, registerInteractiveHandler, listInteractiveHandlers } = require('./registry');
|
|
4
|
+
|
|
5
|
+
async function sendInteractive(nexusOrMessaging, params) {
|
|
6
|
+
const { to, spec, id, variables } = params || {};
|
|
7
|
+
if (!nexusOrMessaging) throw new Error('sendInteractive requires a Nexus or NexusMessaging instance');
|
|
8
|
+
const messaging = typeof nexusOrMessaging.getMessaging === 'function' ? nexusOrMessaging.getMessaging() : nexusOrMessaging;
|
|
9
|
+
const provider = typeof messaging.getProvider === 'function' ? messaging.getProvider() : null;
|
|
10
|
+
if (!provider) throw new Error('No active provider');
|
|
11
|
+
|
|
12
|
+
const useSpec = spec || (id ? getFlow(id) : null);
|
|
13
|
+
if (!useSpec) throw new Error('Interactive spec not found');
|
|
14
|
+
|
|
15
|
+
// If user supplied a contentSid directly in spec, just send it
|
|
16
|
+
if (useSpec.contentSid) {
|
|
17
|
+
return await provider.sendMessage({ to, contentSid: useSpec.contentSid, variables });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Twilio mapping
|
|
21
|
+
if (provider.constructor && provider.constructor.name === 'TwilioProvider') {
|
|
22
|
+
const content = toTwilioContent(useSpec);
|
|
23
|
+
const created = await provider.createTemplate(content);
|
|
24
|
+
return await provider.sendMessage({ to, contentSid: created.sid, variables });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Baileys or others: not supported yet
|
|
28
|
+
throw new Error('Interactive/flows not supported for this provider');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
registerFlow,
|
|
33
|
+
getFlow,
|
|
34
|
+
listFlows,
|
|
35
|
+
sendInteractive,
|
|
36
|
+
registerInteractiveHandler,
|
|
37
|
+
attachInteractiveRouter
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
function _matchInteractive(match, interactive) {
|
|
42
|
+
if (!match) return true;
|
|
43
|
+
if (!interactive) return false;
|
|
44
|
+
// Type match (button, list, flow)
|
|
45
|
+
if (match.type && String(match.type).toLowerCase() !== String(interactive.type || '').toLowerCase()) return false;
|
|
46
|
+
// ID match (e.g., ListId)
|
|
47
|
+
if (match.id) {
|
|
48
|
+
const targetId = interactive.id || interactive.payload || interactive.title || '';
|
|
49
|
+
if (match.id instanceof RegExp) {
|
|
50
|
+
if (!match.id.test(String(targetId))) return false;
|
|
51
|
+
} else if (String(match.id) != String(targetId)) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Payload includes
|
|
56
|
+
if (match.payloadIncludes) {
|
|
57
|
+
const blob = JSON.stringify(interactive);
|
|
58
|
+
if (!blob.includes(match.payloadIncludes)) return false;
|
|
59
|
+
}
|
|
60
|
+
// Custom predicate
|
|
61
|
+
if (typeof match.predicate === 'function' && !match.predicate(interactive)) return false;
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function attachInteractiveRouter(nexusOrMessaging) {
|
|
66
|
+
const messaging = typeof nexusOrMessaging.getMessaging === 'function' ? nexusOrMessaging.getMessaging() : nexusOrMessaging;
|
|
67
|
+
const bus = typeof messaging.getEventBus === 'function' ? messaging.getEventBus() : null;
|
|
68
|
+
if (!bus) throw new Error('Interactive router requires event bus support');
|
|
69
|
+
|
|
70
|
+
const handler = async (messageData) => {
|
|
71
|
+
const interactive = messageData && messageData.interactive ? messageData.interactive : null;
|
|
72
|
+
const items = listInteractiveHandlers();
|
|
73
|
+
for (const { match, handler } of items) {
|
|
74
|
+
try {
|
|
75
|
+
if (_matchInteractive(match, interactive)) {
|
|
76
|
+
await handler(messageData, messaging);
|
|
77
|
+
}
|
|
78
|
+
} catch (e) {
|
|
79
|
+
console.warn('Interactive handler error:', e && e.message || e);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
bus.on('interactive:received', handler);
|
|
85
|
+
return () => bus.off && bus.off('interactive:received', handler);
|
|
86
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
|
|
2
|
+
// Simple in-memory registry for interactive specs (flows, quick replies)
|
|
3
|
+
const _flows = new Map();
|
|
4
|
+
const _handlers = [];
|
|
5
|
+
|
|
6
|
+
function registerFlow(id, spec) {
|
|
7
|
+
if (!id || typeof id !== 'string') throw new Error('registerFlow requires id');
|
|
8
|
+
if (!spec || typeof spec !== 'object') throw new Error('registerFlow requires spec object');
|
|
9
|
+
_flows.set(id, spec);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getFlow(id) {
|
|
13
|
+
return _flows.get(id) || null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function listFlows() {
|
|
17
|
+
return Array.from(_flows.entries()).map(([id, spec]) => ({ id, spec }));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = { registerFlow, getFlow, listFlows, registerInteractiveHandler, listInteractiveHandlers };
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
function registerInteractiveHandler(match, handler) {
|
|
24
|
+
if (typeof handler !== 'function') throw new Error('Handler must be a function');
|
|
25
|
+
const m = match || {};
|
|
26
|
+
_handlers.push({ match: m, handler });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function listInteractiveHandlers() {
|
|
30
|
+
return _handlers.slice();
|
|
31
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
|
|
2
|
+
// Map a provider-agnostic interactive spec to Twilio Content API payload
|
|
3
|
+
|
|
4
|
+
function toTwilioContent(spec) {
|
|
5
|
+
if (!spec || typeof spec !== 'object') throw new Error('Interactive spec must be an object');
|
|
6
|
+
const { type = 'text', language = 'es', body = '', footer, variables, buttons, flow } = spec;
|
|
7
|
+
|
|
8
|
+
const content = {
|
|
9
|
+
friendly_name: spec.friendlyName || (spec.name || ('interactive_' + Date.now())),
|
|
10
|
+
language,
|
|
11
|
+
variables: {},
|
|
12
|
+
types: {}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Build variables map from array/object
|
|
16
|
+
if (Array.isArray(variables)) {
|
|
17
|
+
variables.forEach((v, idx) => {
|
|
18
|
+
content.variables[String(idx + 1)] = typeof v === 'string' ? v : (v.example || '');
|
|
19
|
+
});
|
|
20
|
+
} else if (variables && typeof variables === 'object') {
|
|
21
|
+
Object.keys(variables).forEach(k => {
|
|
22
|
+
content.variables[k] = variables[k];
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (type === 'flow') {
|
|
27
|
+
content.types['twilio/flows'] = {
|
|
28
|
+
body: flow?.body || body || '',
|
|
29
|
+
button_text: flow?.buttonText || spec.buttonText || undefined,
|
|
30
|
+
subtitle: flow?.subtitle || spec.subtitle || undefined,
|
|
31
|
+
pages: flow?.pages || spec.pages || []
|
|
32
|
+
};
|
|
33
|
+
} else if (type === 'quick-reply') {
|
|
34
|
+
const actions = (buttons || []).slice(0, 3).map((b, i) => ({
|
|
35
|
+
title: b.text || b.title || ('Button ' + (i + 1)),
|
|
36
|
+
id: b.id || ('button_' + (i + 1))
|
|
37
|
+
}));
|
|
38
|
+
content.types['twilio/quick-reply'] = {
|
|
39
|
+
body: body || '',
|
|
40
|
+
actions
|
|
41
|
+
};
|
|
42
|
+
// Also include base text body type
|
|
43
|
+
content.types['twilio/text'] = { body: body || '' };
|
|
44
|
+
} else {
|
|
45
|
+
// Plain text
|
|
46
|
+
content.types['twilio/text'] = { body: body || '' };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (footer) {
|
|
50
|
+
['twilio/text', 'twilio/quick-reply'].forEach(t => {
|
|
51
|
+
if (content.types[t]) {
|
|
52
|
+
content.types[t].footer = footer;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return content;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { toTwilioContent };
|