@peopl-health/nexus 1.1.6 → 1.1.8
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 +19 -0
- package/lib/config/llmConfig.js +1 -2
- package/lib/controllers/conversationController.js +13 -2
- package/lib/controllers/templateController.js +69 -34
- package/lib/controllers/templateFlowController.js +108 -0
- package/lib/controllers/uploadController.js +82 -0
- package/lib/index.js +2 -9
- package/lib/models/templateModel.js +72 -0
- package/lib/routes/index.js +5 -3
- package/lib/services/twilioService.js +56 -0
- package/lib/templates/predefinedTemplates.js +81 -0
- package/lib/templates/templateStructure.js +204 -0
- package/lib/utils/dateUtils.js +26 -0
- package/lib/utils/errorHandler.js +8 -0
- package/package.json +2 -1
- package/lib/utils/AssistantManager.js +0 -218
- /package/lib/utils/{DefaultLLMProvider.js → defaultLLMProvider.js} +0 -0
- /package/lib/utils/{MessageParser.js → messageParser.js} +0 -0
|
@@ -113,6 +113,25 @@ class TwilioProvider extends MessageProvider {
|
|
|
113
113
|
const targetTime = new Date(sendTime);
|
|
114
114
|
return Math.max(0, targetTime.getTime() - now.getTime());
|
|
115
115
|
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* List templates from Twilio Content API
|
|
119
|
+
* @param {Object} options - Query options
|
|
120
|
+
* @returns {Promise<Array>} Array of templates
|
|
121
|
+
*/
|
|
122
|
+
async listTemplates(options = {}) {
|
|
123
|
+
if (!this.isConnected || !this.twilioClient) {
|
|
124
|
+
throw new Error('Twilio provider not initialized');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const { limit = 50 } = options;
|
|
129
|
+
const templates = await this.twilioClient.content.v1.contents.list({ limit });
|
|
130
|
+
return templates;
|
|
131
|
+
} catch (error) {
|
|
132
|
+
throw new Error(`Failed to list templates: ${error.message}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
116
135
|
}
|
|
117
136
|
|
|
118
137
|
module.exports = { TwilioProvider };
|
package/lib/config/llmConfig.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
// Use mongoose models directly to avoid conflicts
|
|
2
|
+
const mongoose = require('mongoose');
|
|
2
3
|
const { fetchConversationData, processConversations } = require('../services/conversationService');
|
|
3
4
|
const { sendMessage } = require('../core/NexusMessaging');
|
|
4
5
|
|
|
6
|
+
const Message = mongoose.models.Message;
|
|
7
|
+
|
|
5
8
|
const getConversationController = async (req, res) => {
|
|
6
9
|
const startTime = Date.now();
|
|
7
10
|
console.log('Starting getConversationController at', new Date().toISOString());
|
|
@@ -15,7 +18,15 @@ const getConversationController = async (req, res) => {
|
|
|
15
18
|
|
|
16
19
|
console.log(`Pagination: page ${page}, limit ${limit}, skip ${skip}, filter: ${filter}`);
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
if (!Message) {
|
|
22
|
+
console.log('Message model not found, returning empty conversations list');
|
|
23
|
+
return res.status(200).json({
|
|
24
|
+
success: true,
|
|
25
|
+
conversations: [],
|
|
26
|
+
emptyDatabase: true
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
19
30
|
const messageCount = await Message.countDocuments({});
|
|
20
31
|
console.log('Total message count:', messageCount);
|
|
21
32
|
|
|
@@ -1,23 +1,51 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
// Nexus provider will be injected - templates only work with Twilio
|
|
2
|
+
let nexusProvider = null;
|
|
3
|
+
|
|
4
|
+
// Configure Nexus provider
|
|
5
|
+
const configureNexusProvider = (provider) => {
|
|
6
|
+
nexusProvider = provider;
|
|
7
7
|
};
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
|
|
9
|
+
// Check if provider supports templates
|
|
10
|
+
const checkTemplateSupport = () => {
|
|
11
|
+
if (!nexusProvider) {
|
|
12
|
+
throw new Error('Nexus provider not configured');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Only Twilio provider supports template operations
|
|
16
|
+
if (!nexusProvider.listTemplates || typeof nexusProvider.listTemplates !== 'function') {
|
|
17
|
+
throw new Error('Template operations are only supported with Twilio provider');
|
|
18
|
+
}
|
|
11
19
|
};
|
|
20
|
+
const mongoose = require('mongoose');
|
|
21
|
+
const { handleApiError } = require('../utils/errorHandler');
|
|
12
22
|
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
23
|
+
const { Template } = require('../templates/templateStructure');
|
|
24
|
+
const predefinedTemplates = require('../templates/predefinedTemplates');
|
|
25
|
+
|
|
26
|
+
// Use mongoose models to avoid conflicts
|
|
27
|
+
const getTemplateModel = () => {
|
|
28
|
+
if (mongoose.models.Template) {
|
|
29
|
+
return mongoose.models.Template;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// If not in mongoose.models, require and return the model
|
|
33
|
+
try {
|
|
34
|
+
const TemplateModel = require('../models/templateModel');
|
|
35
|
+
return TemplateModel;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error('Failed to load Template model:', error);
|
|
38
|
+
// Return a stub model with required methods
|
|
39
|
+
return {
|
|
40
|
+
deleteMany: () => Promise.resolve({ deletedCount: 0 }),
|
|
41
|
+
find: () => ({ sort: () => ({ limit: () => ({ lean: () => Promise.resolve([]) }) }) }),
|
|
42
|
+
findOne: () => Promise.resolve(null),
|
|
43
|
+
create: () => Promise.resolve({}),
|
|
44
|
+
updateOne: () => Promise.resolve({}),
|
|
45
|
+
deleteOne: () => Promise.resolve({})
|
|
46
|
+
};
|
|
47
|
+
}
|
|
19
48
|
};
|
|
20
|
-
const predefinedTemplates = [];
|
|
21
49
|
|
|
22
50
|
/**
|
|
23
51
|
* Create a new template and store it in both Twilio and our database
|
|
@@ -109,6 +137,7 @@ const createTemplate = async (req, res) => {
|
|
|
109
137
|
dateCreated = currentDate;
|
|
110
138
|
}
|
|
111
139
|
|
|
140
|
+
const TemplateModel = getTemplateModel();
|
|
112
141
|
const newDbTemplate = await TemplateModel.create({
|
|
113
142
|
sid: twilioContent.sid,
|
|
114
143
|
name: (twilioContent.friendlyName || `template_${twilioContent.sid}`).replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase(),
|
|
@@ -152,8 +181,13 @@ const createTemplate = async (req, res) => {
|
|
|
152
181
|
const listTemplates = async (req, res) => {
|
|
153
182
|
try {
|
|
154
183
|
const { status: queryStatus, type, limit = 50, showFlows: queryShowFlows } = req.query;
|
|
184
|
+
const TemplateModel = getTemplateModel();
|
|
155
185
|
|
|
156
|
-
|
|
186
|
+
console.log('nexusProvider:', nexusProvider ? 'configured' : 'not configured');
|
|
187
|
+
console.log('nexusProvider methods:', nexusProvider ? Object.keys(nexusProvider) : 'none');
|
|
188
|
+
|
|
189
|
+
checkTemplateSupport();
|
|
190
|
+
const twilioRawTemplates = await nexusProvider.listTemplates({ limit: parseInt(limit, 10) });
|
|
157
191
|
|
|
158
192
|
const showFlows = type === 'flow' || queryShowFlows === 'true';
|
|
159
193
|
const filteredTwilioTemplates = twilioRawTemplates.filter(template => {
|
|
@@ -176,7 +210,7 @@ const listTemplates = async (req, res) => {
|
|
|
176
210
|
};
|
|
177
211
|
|
|
178
212
|
try {
|
|
179
|
-
const approvalInfo = await
|
|
213
|
+
const approvalInfo = await nexusProvider.checkApprovalStatus(twilioTemplate.sid);
|
|
180
214
|
if (approvalInfo && approvalInfo.approvalRequest) {
|
|
181
215
|
const reqData = approvalInfo.approvalRequest;
|
|
182
216
|
updateFields.approvalRequest = {
|
|
@@ -260,10 +294,12 @@ const getTemplate = async (req, res) => {
|
|
|
260
294
|
try {
|
|
261
295
|
const { id } = req.params;
|
|
262
296
|
|
|
297
|
+
const TemplateModel = getTemplateModel();
|
|
263
298
|
let template = await TemplateModel.findOne({ sid: id });
|
|
264
299
|
let twilioTemplate;
|
|
265
300
|
|
|
266
|
-
|
|
301
|
+
checkTemplateSupport();
|
|
302
|
+
twilioTemplate = await nexusProvider.getTemplate(id);
|
|
267
303
|
|
|
268
304
|
if (!template) {
|
|
269
305
|
// If template wasn't in our database, create it based on Twilio data
|
|
@@ -345,7 +381,8 @@ const submitForApproval = async (req, res) => {
|
|
|
345
381
|
const approvalName = `${name || 'template'}_${timestamp}_${randomSuffix}`.toLowerCase().replace(/[^a-z0-9_]/g, '_');
|
|
346
382
|
const approvalCategory = category || 'UTILITY';
|
|
347
383
|
|
|
348
|
-
|
|
384
|
+
checkTemplateSupport();
|
|
385
|
+
const response = await nexusProvider.submitForApproval(contentSid, approvalName, approvalCategory);
|
|
349
386
|
|
|
350
387
|
const dateCreated = response.date_created || response.dateCreated;
|
|
351
388
|
const dateUpdated = response.date_updated || response.dateUpdated || dateCreated;
|
|
@@ -356,6 +393,7 @@ const submitForApproval = async (req, res) => {
|
|
|
356
393
|
const validSubmittedDate = !isNaN(submittedDate.getTime()) ? submittedDate : new Date();
|
|
357
394
|
const validUpdatedDate = !isNaN(updatedDate.getTime()) ? updatedDate : new Date();
|
|
358
395
|
|
|
396
|
+
const TemplateModel = getTemplateModel();
|
|
359
397
|
await TemplateModel.updateOne(
|
|
360
398
|
{ sid: contentSid },
|
|
361
399
|
{
|
|
@@ -397,10 +435,12 @@ const checkApprovalStatus = async (req, res) => {
|
|
|
397
435
|
return res.status(400).json({ success: false, error: 'Content SID is required' });
|
|
398
436
|
}
|
|
399
437
|
|
|
438
|
+
const TemplateModel = getTemplateModel();
|
|
400
439
|
const dbTemplate = await TemplateModel.findOne({ sid: contentSid });
|
|
401
440
|
|
|
402
441
|
try {
|
|
403
|
-
|
|
442
|
+
checkTemplateSupport();
|
|
443
|
+
const status = await nexusProvider.checkApprovalStatus(contentSid);
|
|
404
444
|
|
|
405
445
|
if (dbTemplate) {
|
|
406
446
|
// Use approval status as the authoritative source if available
|
|
@@ -475,8 +515,10 @@ const deleteTemplate = async (req, res) => {
|
|
|
475
515
|
try {
|
|
476
516
|
const { id } = req.params;
|
|
477
517
|
|
|
478
|
-
|
|
518
|
+
checkTemplateSupport();
|
|
519
|
+
await nexusProvider.deleteTemplate(id);
|
|
479
520
|
|
|
521
|
+
const TemplateModel = getTemplateModel();
|
|
480
522
|
await TemplateModel.deleteOne({ sid: id });
|
|
481
523
|
|
|
482
524
|
return res.json({
|
|
@@ -529,6 +571,7 @@ const getCompleteTemplate = async (req, res) => {
|
|
|
529
571
|
try {
|
|
530
572
|
const { sid } = req.params;
|
|
531
573
|
|
|
574
|
+
const TemplateModel = getTemplateModel();
|
|
532
575
|
let template = await TemplateModel.findOne({ sid }).lean();
|
|
533
576
|
|
|
534
577
|
if (!template) {
|
|
@@ -540,7 +583,8 @@ const getCompleteTemplate = async (req, res) => {
|
|
|
540
583
|
|
|
541
584
|
if (!template.body || !template.variables || template.variables.length === 0) {
|
|
542
585
|
try {
|
|
543
|
-
|
|
586
|
+
checkTemplateSupport();
|
|
587
|
+
const twilioTemplate = await nexusProvider.getTemplate(sid);
|
|
544
588
|
console.log('Fetched template from Twilio:', twilioTemplate);
|
|
545
589
|
|
|
546
590
|
let body = '';
|
|
@@ -603,6 +647,7 @@ const getCompleteTemplate = async (req, res) => {
|
|
|
603
647
|
updateData.actions = template.actions;
|
|
604
648
|
}
|
|
605
649
|
|
|
650
|
+
const TemplateModel = getTemplateModel();
|
|
606
651
|
await TemplateModel.updateOne(
|
|
607
652
|
{ sid },
|
|
608
653
|
{ $set: updateData }
|
|
@@ -628,15 +673,6 @@ const getCompleteTemplate = async (req, res) => {
|
|
|
628
673
|
}
|
|
629
674
|
};
|
|
630
675
|
|
|
631
|
-
// Flow functions (inline since templateFlowController was removed)
|
|
632
|
-
const createFlow = async (req, res) => {
|
|
633
|
-
return res.status(501).json({ success: false, error: 'Flow creation not available' });
|
|
634
|
-
};
|
|
635
|
-
|
|
636
|
-
const deleteFlow = async (req, res) => {
|
|
637
|
-
return res.status(501).json({ success: false, error: 'Flow deletion not available' });
|
|
638
|
-
};
|
|
639
|
-
|
|
640
676
|
module.exports = {
|
|
641
677
|
createTemplate,
|
|
642
678
|
listTemplates,
|
|
@@ -646,6 +682,5 @@ module.exports = {
|
|
|
646
682
|
checkApprovalStatus,
|
|
647
683
|
deleteTemplate,
|
|
648
684
|
getCompleteTemplate,
|
|
649
|
-
|
|
650
|
-
deleteFlow
|
|
685
|
+
configureNexusProvider
|
|
651
686
|
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Nexus provider will be injected
|
|
2
|
+
let nexusProvider = null;
|
|
3
|
+
|
|
4
|
+
// Configure Nexus provider
|
|
5
|
+
const configureNexusProvider = (provider) => {
|
|
6
|
+
nexusProvider = provider;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// Check if provider supports templates
|
|
10
|
+
const checkTemplateSupport = () => {
|
|
11
|
+
if (!nexusProvider) {
|
|
12
|
+
throw new Error('Nexus provider not configured. Call configureNexusProvider() first.');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (!nexusProvider.createTemplate || typeof nexusProvider.createTemplate !== 'function') {
|
|
16
|
+
throw new Error('Template operations are only supported with Twilio provider');
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const { handleApiError } = require('../utils/errorHandler');
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
const createFlow = async (req, res) => {
|
|
23
|
+
try {
|
|
24
|
+
const { friendlyName, language, flowType, body, buttonText, subtitle, pages } = req.body;
|
|
25
|
+
|
|
26
|
+
const missingParams = [];
|
|
27
|
+
if (!friendlyName) missingParams.push('friendlyName');
|
|
28
|
+
if (!language) missingParams.push('language');
|
|
29
|
+
if (!pages || !Array.isArray(pages)) missingParams.push('pages');
|
|
30
|
+
|
|
31
|
+
if (missingParams.length > 0) {
|
|
32
|
+
return res.status(400).json({
|
|
33
|
+
success: false,
|
|
34
|
+
error: `Missing required parameters: ${missingParams.join(', ')}`
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Generate unique identifiers for anti-rejection
|
|
39
|
+
const timestamp = Date.now().toString();
|
|
40
|
+
const uniqueId = require('uuid').v4().substring(0, 6);
|
|
41
|
+
const uniqueFriendlyName = `${friendlyName}_${timestamp}_${uniqueId}`;
|
|
42
|
+
|
|
43
|
+
// Match Twilio's expected structure for flex templates
|
|
44
|
+
checkTemplateSupport();
|
|
45
|
+
const content = await nexusProvider.createTemplate({
|
|
46
|
+
friendly_name: uniqueFriendlyName,
|
|
47
|
+
language,
|
|
48
|
+
categories: ['UTILITY'],
|
|
49
|
+
types: {
|
|
50
|
+
'twilio/flows': {
|
|
51
|
+
body: body,
|
|
52
|
+
button_text: buttonText,
|
|
53
|
+
subtitle: subtitle || null,
|
|
54
|
+
pages: pages,
|
|
55
|
+
type: flowType
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return res.status(201).json({
|
|
61
|
+
success: true,
|
|
62
|
+
message: 'Flow template created successfully',
|
|
63
|
+
contentSid: content.sid,
|
|
64
|
+
friendlyName: content.friendly_name,
|
|
65
|
+
approvalStatus: 'Pending review',
|
|
66
|
+
approvalLinks: content.links
|
|
67
|
+
});
|
|
68
|
+
} catch (error) {
|
|
69
|
+
return handleApiError(res, error, 'Error creating WhatsApp flow template');
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
const deleteFlow = async (req, res) => {
|
|
75
|
+
try {
|
|
76
|
+
const { sid: contentSid } = req.params;
|
|
77
|
+
|
|
78
|
+
if (!contentSid) {
|
|
79
|
+
return res.status(400).json({
|
|
80
|
+
success: false,
|
|
81
|
+
error: 'Content SID is required'
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
checkTemplateSupport();
|
|
86
|
+
await nexusProvider.deleteTemplate(contentSid);
|
|
87
|
+
|
|
88
|
+
return res.status(200).json({
|
|
89
|
+
success: true,
|
|
90
|
+
message: 'Template deleted successfully'
|
|
91
|
+
});
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (error.code === 20404) {
|
|
94
|
+
return res.status(404).json({
|
|
95
|
+
success: false,
|
|
96
|
+
error: 'Template not found'
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return handleApiError(res, error, 'Error deleting template');
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
createFlow,
|
|
106
|
+
deleteFlow,
|
|
107
|
+
configureNexusProvider
|
|
108
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const multer = require('multer');
|
|
2
|
+
const { v4: uuidv4 } = require('uuid');
|
|
3
|
+
const { generatePresignedUrl, uploadBufferToS3 } = require('../config/awsConfig');
|
|
4
|
+
const bucketName = process.env.AWS_S3_BUCKET_NAME;
|
|
5
|
+
|
|
6
|
+
const storage = multer.memoryStorage();
|
|
7
|
+
|
|
8
|
+
const fileFilter = (req, file, cb) => {
|
|
9
|
+
if (file.mimetype.startsWith('image/') ||
|
|
10
|
+
file.mimetype.startsWith('application/') ||
|
|
11
|
+
file.mimetype.startsWith('audio/') ||
|
|
12
|
+
file.mimetype.startsWith('video/')) {
|
|
13
|
+
cb(null, true);
|
|
14
|
+
} else {
|
|
15
|
+
cb(new Error('Unsupported file type'), false);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const upload = multer({
|
|
20
|
+
storage: storage,
|
|
21
|
+
fileFilter: fileFilter,
|
|
22
|
+
limits: {
|
|
23
|
+
fileSize: 50 * 1024 * 1024, // Increased to 50MB
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const handleFileUpload = async (req, res) => {
|
|
28
|
+
try {
|
|
29
|
+
if (!req.file) {
|
|
30
|
+
return res.status(400).json({
|
|
31
|
+
success: false,
|
|
32
|
+
error: 'No file provided'
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const file = req.file;
|
|
37
|
+
|
|
38
|
+
let filePrefix = 'document';
|
|
39
|
+
if (file.mimetype.startsWith('image/')) {
|
|
40
|
+
filePrefix = 'image';
|
|
41
|
+
} else if (file.mimetype.startsWith('video/')) {
|
|
42
|
+
filePrefix = 'video';
|
|
43
|
+
} else if (file.mimetype.startsWith('audio/')) {
|
|
44
|
+
filePrefix = 'audio';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const uniqueId = uuidv4();
|
|
48
|
+
const originalFileName = file.originalname;
|
|
49
|
+
const sanitizedFileName = file.originalname.replace(/[^a-zA-Z0-9.]/g, '_').replace('.pdf', '');
|
|
50
|
+
|
|
51
|
+
const key = `${filePrefix}/${sanitizedFileName}_${uniqueId}`;
|
|
52
|
+
|
|
53
|
+
await uploadBufferToS3(file.buffer, bucketName, key, file.mimetype, true);
|
|
54
|
+
const whatsappExpirationSeconds = 7 * 24 * 60 * 60;
|
|
55
|
+
const presignedUrl = await generatePresignedUrl(bucketName, key, whatsappExpirationSeconds);
|
|
56
|
+
|
|
57
|
+
res.status(200).json({
|
|
58
|
+
success: true,
|
|
59
|
+
message: 'File uploaded successfully',
|
|
60
|
+
fileUrl: presignedUrl,
|
|
61
|
+
key: key,
|
|
62
|
+
fileType: filePrefix,
|
|
63
|
+
fileName: originalFileName,
|
|
64
|
+
sanitizedFileName: sanitizedFileName,
|
|
65
|
+
fileSize: file.size,
|
|
66
|
+
contentType: file.mimetype
|
|
67
|
+
});
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error('Error uploading file:', error);
|
|
70
|
+
res.status(500).json({
|
|
71
|
+
success: false,
|
|
72
|
+
error: error.message || 'Failed to upload file'
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const uploadSingleFile = upload.single('file');
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
uploadSingleFile,
|
|
81
|
+
handleFileUpload
|
|
82
|
+
};
|
package/lib/index.js
CHANGED
|
@@ -2,15 +2,8 @@ const { NexusMessaging } = require('./core/NexusMessaging');
|
|
|
2
2
|
const { TwilioProvider } = require('./adapters/TwilioProvider');
|
|
3
3
|
const { BaileysProvider } = require('./adapters/BaileysProvider');
|
|
4
4
|
const { MongoStorage } = require('./storage/MongoStorage');
|
|
5
|
-
const { MessageParser } = require('./utils/
|
|
6
|
-
const { DefaultLLMProvider } = require('./utils/
|
|
7
|
-
|
|
8
|
-
// Export individual components
|
|
9
|
-
const adapters = require('./adapters');
|
|
10
|
-
const core = require('./core');
|
|
11
|
-
const storage = require('./storage');
|
|
12
|
-
const utils = require('./utils');
|
|
13
|
-
const models = require('./models');
|
|
5
|
+
const { MessageParser } = require('./utils/messageParser');
|
|
6
|
+
const { DefaultLLMProvider } = require('./utils/defaultLLMProvider');
|
|
14
7
|
|
|
15
8
|
/**
|
|
16
9
|
* Main Nexus class that orchestrates all components
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const mongoose = require('mongoose');
|
|
2
|
+
|
|
3
|
+
const TemplateSchema = new mongoose.Schema({
|
|
4
|
+
sid: {
|
|
5
|
+
type: String,
|
|
6
|
+
required: true,
|
|
7
|
+
unique: true
|
|
8
|
+
},
|
|
9
|
+
name: {
|
|
10
|
+
type: String,
|
|
11
|
+
required: true
|
|
12
|
+
},
|
|
13
|
+
friendlyName: String,
|
|
14
|
+
category: {
|
|
15
|
+
type: String,
|
|
16
|
+
default: 'UTILITY',
|
|
17
|
+
enum: ['UTILITY', 'MARKETING', 'AUTHENTICATION']
|
|
18
|
+
},
|
|
19
|
+
language: {
|
|
20
|
+
type: String,
|
|
21
|
+
default: 'es'
|
|
22
|
+
},
|
|
23
|
+
status: {
|
|
24
|
+
type: String,
|
|
25
|
+
default: 'DRAFT',
|
|
26
|
+
enum: ['DRAFT', 'PENDING', 'APPROVED', 'REJECTED', 'UNKNOWN']
|
|
27
|
+
},
|
|
28
|
+
body: String,
|
|
29
|
+
footer: String,
|
|
30
|
+
variables: [
|
|
31
|
+
{
|
|
32
|
+
name: String,
|
|
33
|
+
description: String,
|
|
34
|
+
example: String
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
buttons: [
|
|
38
|
+
{
|
|
39
|
+
type: {
|
|
40
|
+
type: String,
|
|
41
|
+
enum: ['quick_reply', 'url']
|
|
42
|
+
},
|
|
43
|
+
text: String,
|
|
44
|
+
url: String
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
approvalRequest: {
|
|
48
|
+
sid: String,
|
|
49
|
+
status: String,
|
|
50
|
+
dateSubmitted: Date,
|
|
51
|
+
dateUpdated: Date,
|
|
52
|
+
rejectionReason: String
|
|
53
|
+
},
|
|
54
|
+
dateCreated: {
|
|
55
|
+
type: Date,
|
|
56
|
+
default: Date.now
|
|
57
|
+
},
|
|
58
|
+
lastUpdated: {
|
|
59
|
+
type: Date,
|
|
60
|
+
default: Date.now
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Pre-save middleware to update lastUpdated
|
|
65
|
+
TemplateSchema.pre('save', function(next) {
|
|
66
|
+
this.lastUpdated = new Date();
|
|
67
|
+
next();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const Template = mongoose.model('Template', TemplateSchema);
|
|
71
|
+
|
|
72
|
+
module.exports = Template;
|
package/lib/routes/index.js
CHANGED
|
@@ -70,6 +70,8 @@ const conversationController = require('../controllers/conversationController');
|
|
|
70
70
|
const mediaController = require('../controllers/mediaController');
|
|
71
71
|
const messageController = require('../controllers/messageController');
|
|
72
72
|
const templateController = require('../controllers/templateController');
|
|
73
|
+
const templateFlowController = require('../controllers/templateFlowController');
|
|
74
|
+
const uploadController = require('../controllers/uploadController');
|
|
73
75
|
|
|
74
76
|
// Built-in controllers mapping
|
|
75
77
|
const builtInControllers = {
|
|
@@ -95,7 +97,7 @@ const builtInControllers = {
|
|
|
95
97
|
|
|
96
98
|
// Media controllers
|
|
97
99
|
getMediaController: mediaController.getMediaController,
|
|
98
|
-
handleFileUpload:
|
|
100
|
+
handleFileUpload: uploadController.handleFileUpload,
|
|
99
101
|
|
|
100
102
|
// Message controllers
|
|
101
103
|
sendMessageController: messageController.sendMessageController,
|
|
@@ -108,8 +110,8 @@ const builtInControllers = {
|
|
|
108
110
|
getPredefinedTemplates: templateController.getPredefinedTemplates,
|
|
109
111
|
getTemplate: templateController.getTemplate,
|
|
110
112
|
getCompleteTemplate: templateController.getCompleteTemplate,
|
|
111
|
-
createFlow:
|
|
112
|
-
deleteFlow:
|
|
113
|
+
createFlow: templateFlowController.createFlow,
|
|
114
|
+
deleteFlow: templateFlowController.deleteFlow,
|
|
113
115
|
submitForApproval: templateController.submitForApproval,
|
|
114
116
|
checkApprovalStatus: templateController.checkApprovalStatus,
|
|
115
117
|
deleteTemplate: templateController.deleteTemplate
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// TwilioService that connects to Nexus Twilio provider
|
|
2
|
+
let nexusProvider = null;
|
|
3
|
+
|
|
4
|
+
// Configure the Nexus provider
|
|
5
|
+
const configureNexusProvider = (provider) => {
|
|
6
|
+
nexusProvider = provider;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// Check if provider is configured and supports Twilio operations
|
|
10
|
+
const checkTwilioSupport = () => {
|
|
11
|
+
if (!nexusProvider) {
|
|
12
|
+
throw new Error('Nexus provider not configured. Call configureNexusProvider() first.');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (!nexusProvider.listTemplates || typeof nexusProvider.listTemplates !== 'function') {
|
|
16
|
+
throw new Error('Twilio operations are only supported with Twilio provider');
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// TwilioService wrapper that delegates to Nexus provider
|
|
21
|
+
const TwilioService = {
|
|
22
|
+
async listTemplates(options = {}) {
|
|
23
|
+
checkTwilioSupport();
|
|
24
|
+
return await nexusProvider.listTemplates(options);
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
async getTemplate(sid) {
|
|
28
|
+
checkTwilioSupport();
|
|
29
|
+
return await nexusProvider.getTemplate(sid);
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
async checkApprovalStatus(sid) {
|
|
33
|
+
checkTwilioSupport();
|
|
34
|
+
return await nexusProvider.checkApprovalStatus(sid);
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
async submitForApproval(contentSid, name, category) {
|
|
38
|
+
checkTwilioSupport();
|
|
39
|
+
return await nexusProvider.submitForApproval(contentSid, name, category);
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
async deleteTemplate(sid) {
|
|
43
|
+
checkTwilioSupport();
|
|
44
|
+
return await nexusProvider.deleteTemplate(sid);
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
async createTemplate(templateData) {
|
|
48
|
+
checkTwilioSupport();
|
|
49
|
+
return await nexusProvider.createTemplate(templateData);
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
// Add any other Twilio operations as needed
|
|
53
|
+
configureNexusProvider
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
module.exports = TwilioService;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const { Template } = require('./templateStructure');
|
|
2
|
+
|
|
3
|
+
const predefinedTemplates = {
|
|
4
|
+
citaDrLimon: () => {
|
|
5
|
+
const template = new Template('cita_dr_limon', 'UTILITY', 'es');
|
|
6
|
+
|
|
7
|
+
const patientVars = [
|
|
8
|
+
{
|
|
9
|
+
name: 'Nombre del paciente',
|
|
10
|
+
description: 'Nombre completo del paciente',
|
|
11
|
+
example: 'Juan Pérez'
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: 'Fecha de la cita',
|
|
15
|
+
description: 'Fecha en formato DD/MM',
|
|
16
|
+
example: '18/05'
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: 'Hora de la cita',
|
|
20
|
+
description: 'Hora en formato HH:MM am/pm',
|
|
21
|
+
example: '9:30 am'
|
|
22
|
+
}
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
template.addBodyVariation(
|
|
26
|
+
'Hola buenos días {{1}}, \n\nSoy Briggite del equipo del Dr Limón \n\n📆 Tienes programado una cita con el dr Limón para mañana {{2}} a las {{3}}\n\n✅ Confirma tu asistencia por favor',
|
|
27
|
+
patientVars
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
template.addBodyVariation(
|
|
31
|
+
'Hola {{1}}, buenos días\n\nLe saluda Briggite del equipo médico del Dr Limón\n\n📆 Su cita con el Dr Limón está programada para mañana {{2}} a las {{3}}\n\n✅ Por favor confirme su asistencia',
|
|
32
|
+
patientVars
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
template.addBodyVariation(
|
|
36
|
+
'Buenos días {{1}}\n\nSoy Briggite, asistente del Dr Limón\n\n📆 Le recordamos su cita para mañana {{2}} a las {{3}} con el Dr Limón\n\n✅ Agradecemos su confirmación',
|
|
37
|
+
patientVars
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
template.setBody('Hola buenos días {{1}}, \n\nSoy Briggite del equipo del Dr Limón \n\n📆 Tienes programado una cita con el dr Limón para mañana {{2}} a las {{3}}\n\n✅ Confirma tu asistencia por favor',
|
|
41
|
+
patientVars);
|
|
42
|
+
|
|
43
|
+
return template;
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
seguimientoDrLimon: () => {
|
|
47
|
+
const template = new Template('seguimiento_dr_limon', 'UTILITY', 'es');
|
|
48
|
+
|
|
49
|
+
const followUpVars = [
|
|
50
|
+
{
|
|
51
|
+
name: 'Nombre del paciente',
|
|
52
|
+
description: 'Nombre completo del paciente',
|
|
53
|
+
example: 'Carmen Rodríguez'
|
|
54
|
+
}
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
template.addBodyVariation(
|
|
58
|
+
'Hola {{1}} buenos días ¿cómo han amanecido? ¿cómo les fue con las recomendaciones?',
|
|
59
|
+
followUpVars
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
template.addBodyVariation(
|
|
63
|
+
'Buenos días {{1}}, ¿cómo se encuentra hoy? ¿ha podido seguir las recomendaciones médicas?',
|
|
64
|
+
followUpVars
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
template.addBodyVariation(
|
|
68
|
+
'Hola {{1}}, buenos días. Seguimiento médico: ¿cómo se ha sentido? ¿pudo implementar las recomendaciones indicadas?',
|
|
69
|
+
followUpVars
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
template.setBody('Hola {{1}} buenos días ¿cómo han amanecido? ¿cómo les fue con las recomendaciones?',
|
|
73
|
+
followUpVars);
|
|
74
|
+
|
|
75
|
+
return template;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
predefinedTemplates
|
|
81
|
+
};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
const { v4: uuidv4 } = require('uuid');
|
|
2
|
+
|
|
3
|
+
// Nexus provider will be injected
|
|
4
|
+
let nexusProvider = null;
|
|
5
|
+
|
|
6
|
+
// Configure Nexus provider
|
|
7
|
+
const configureNexusProvider = (provider) => {
|
|
8
|
+
nexusProvider = provider;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// Check if provider supports templates
|
|
12
|
+
const checkTemplateSupport = () => {
|
|
13
|
+
if (!nexusProvider) {
|
|
14
|
+
throw new Error('Nexus provider not configured. Call configureNexusProvider() first.');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!nexusProvider.createTemplate || typeof nexusProvider.createTemplate !== 'function') {
|
|
18
|
+
throw new Error('Template operations are only supported with Twilio provider');
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
class Template {
|
|
23
|
+
constructor(name, category = 'UTILITY', language = 'es') {
|
|
24
|
+
this.name = name;
|
|
25
|
+
this.category = category.toUpperCase();
|
|
26
|
+
this.language = language;
|
|
27
|
+
this.components = {
|
|
28
|
+
header: [],
|
|
29
|
+
body: [],
|
|
30
|
+
footer: [],
|
|
31
|
+
buttons: []
|
|
32
|
+
};
|
|
33
|
+
this.variables = [];
|
|
34
|
+
|
|
35
|
+
const timestamp = Date.now().toString();
|
|
36
|
+
const uniqueId = uuidv4().substring(0, 6);
|
|
37
|
+
this.friendlyName = `${name}_${timestamp}_${uniqueId}`;
|
|
38
|
+
this.templateName = `${name}_${Date.now().toString().substring(0, 10)}`;
|
|
39
|
+
this.approvalName = this.name || `template_${name}_${Math.floor(Math.random() * 1000)}`;
|
|
40
|
+
this.status = 'DRAFT';
|
|
41
|
+
|
|
42
|
+
this.variations = [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
addBodyVariation(text, variableDescriptions = []) {
|
|
46
|
+
this.variations.push({
|
|
47
|
+
text: text,
|
|
48
|
+
variables: variableDescriptions
|
|
49
|
+
});
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
setBody(text, variableDescriptions = []) {
|
|
54
|
+
this.addBodyVariation(text, variableDescriptions);
|
|
55
|
+
|
|
56
|
+
const selectedVariation = this.variations.length > 1 ?
|
|
57
|
+
this.variations[Math.floor(Math.random() * this.variations.length)] :
|
|
58
|
+
{ text, variables: variableDescriptions };
|
|
59
|
+
|
|
60
|
+
const enhancedText = `${selectedVariation.text}`;
|
|
61
|
+
|
|
62
|
+
const processedVariables = selectedVariation.variables.map((desc, i) => {
|
|
63
|
+
if (typeof desc === 'string') {
|
|
64
|
+
return {
|
|
65
|
+
name: `var_${i + 1}`,
|
|
66
|
+
description: desc,
|
|
67
|
+
example: `Ejemplo ${i + 1}`
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
name: desc.name || `var_${i + 1}`,
|
|
72
|
+
description: desc.description || `Variable ${i + 1}`,
|
|
73
|
+
example: desc.example || `Ejemplo ${i + 1}`
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
this.components.body = [{
|
|
78
|
+
type: 'BODY',
|
|
79
|
+
text: enhancedText,
|
|
80
|
+
example: {
|
|
81
|
+
body_text: processedVariables.map(v => v.example)
|
|
82
|
+
}
|
|
83
|
+
}];
|
|
84
|
+
|
|
85
|
+
this.variables = processedVariables;
|
|
86
|
+
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
addQuickReply(text) {
|
|
91
|
+
if (this.components.buttons.length >= 3) {
|
|
92
|
+
throw new Error('Maximum of 3 buttons allowed');
|
|
93
|
+
}
|
|
94
|
+
this.components.buttons.push({
|
|
95
|
+
type: 'QUICK_REPLY',
|
|
96
|
+
text: text
|
|
97
|
+
});
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
addCallToAction(text, url) {
|
|
102
|
+
if (this.components.buttons.length >= 3) {
|
|
103
|
+
throw new Error('Maximum of 3 buttons allowed');
|
|
104
|
+
}
|
|
105
|
+
this.components.buttons.push({
|
|
106
|
+
type: 'URL',
|
|
107
|
+
text: text,
|
|
108
|
+
url: url
|
|
109
|
+
});
|
|
110
|
+
return this;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
setFooter(text) {
|
|
114
|
+
if (text.length > 60) {
|
|
115
|
+
throw new Error('Footer text must be 60 characters or less');
|
|
116
|
+
}
|
|
117
|
+
this.components.footer = [{
|
|
118
|
+
type: 'FOOTER',
|
|
119
|
+
text: text
|
|
120
|
+
}];
|
|
121
|
+
return this;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
generateTemplateName(baseName) {
|
|
125
|
+
const timestamp = new Date().toISOString().replace(/[^0-9]/g, '').slice(-8);
|
|
126
|
+
return `${baseName}_${timestamp}`.toUpperCase();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
validate() {
|
|
130
|
+
if (!this.components.body || !this.components.body[0]?.text) {
|
|
131
|
+
throw new Error('Template body is required');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const varMatches = this.components.body[0].text.match(/\{\{\d+\}\}/g) || [];
|
|
135
|
+
const varCount = new Set(varMatches.map(m => m.match(/\d+/)[0])).size;
|
|
136
|
+
|
|
137
|
+
if (varCount !== this.variables.length) {
|
|
138
|
+
throw new Error(`Mismatch between variable placeholders (${varCount}) and variable definitions (${this.variables.length})`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
toTwilioFormat() {
|
|
145
|
+
this.validate();
|
|
146
|
+
|
|
147
|
+
const variables = {};
|
|
148
|
+
this.variables.forEach((variable, i) => {
|
|
149
|
+
variables[`${i + 1}`] = variable.example;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const template = {
|
|
153
|
+
friendly_name: this.friendlyName,
|
|
154
|
+
language: this.language,
|
|
155
|
+
variables: variables,
|
|
156
|
+
types: {
|
|
157
|
+
'twilio/text': {
|
|
158
|
+
body: this.components.body[0].text
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
if (this.category) {
|
|
164
|
+
template.categories = [this.category];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (this.components.buttons && this.components.buttons.length > 0) {
|
|
168
|
+
const actions = this.components.buttons.map((button, index) => {
|
|
169
|
+
return {
|
|
170
|
+
title: button.text,
|
|
171
|
+
id: `button_${index + 1}`
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
template.types['twilio/quick-reply'] = {
|
|
176
|
+
body: this.components.body[0].text,
|
|
177
|
+
actions: actions
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (this.components.footer && this.components.footer.length > 0) {
|
|
182
|
+
const textTypes = ['twilio/text', 'twilio/quick-reply'];
|
|
183
|
+
textTypes.forEach(type => {
|
|
184
|
+
if (template.types[type]) {
|
|
185
|
+
template.types[type].footer = this.components.footer[0].text;
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return template;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async save() {
|
|
194
|
+
checkTemplateSupport();
|
|
195
|
+
const twilioFormat = this.toTwilioFormat();
|
|
196
|
+
const createdTemplate = await nexusProvider.createTemplate(twilioFormat);
|
|
197
|
+
return createdTemplate;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = {
|
|
202
|
+
Template,
|
|
203
|
+
configureNexusProvider
|
|
204
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const moment = require('moment-timezone');
|
|
2
|
+
|
|
3
|
+
const ISO_DATE = 'YYYY-MM-DD';
|
|
4
|
+
|
|
5
|
+
const parseStartTime = (startTime) => {
|
|
6
|
+
return moment(startTime);
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const addDays = (date, days) => {
|
|
10
|
+
return moment(date).add(days, 'days');
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const dateAndTimeFromStart = (startTime) => {
|
|
14
|
+
const momentTime = moment(startTime);
|
|
15
|
+
return {
|
|
16
|
+
date: momentTime.format(ISO_DATE),
|
|
17
|
+
time: momentTime.format('HH:mm')
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
module.exports = {
|
|
22
|
+
ISO_DATE,
|
|
23
|
+
parseStartTime,
|
|
24
|
+
addDays,
|
|
25
|
+
dateAndTimeFromStart
|
|
26
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peopl-health/nexus",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.8",
|
|
4
4
|
"description": "Core messaging and assistant library for WhatsApp communication platforms",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"dotenv": "^16.4.7",
|
|
57
57
|
"moment-timezone": "^0.5.43",
|
|
58
58
|
"mongoose": "^7.5.0",
|
|
59
|
+
"multer": "1.4.5-lts.1",
|
|
59
60
|
"pdf-lib": "1.17.1",
|
|
60
61
|
"pino": "^8.15.0",
|
|
61
62
|
"pino-pretty": "^10.2.0",
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Configurable assistant manager for handling AI interactions
|
|
3
|
-
*/
|
|
4
|
-
class AssistantManager {
|
|
5
|
-
constructor(config = {}) {
|
|
6
|
-
this.config = config;
|
|
7
|
-
this.assistants = new Map();
|
|
8
|
-
this.llmClient = null;
|
|
9
|
-
this.handlers = {
|
|
10
|
-
onRequiresAction: null,
|
|
11
|
-
onCompleted: null,
|
|
12
|
-
onFailed: null
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Initialize with LLM client (OpenAI, etc.)
|
|
18
|
-
* @param {Object} llmClient - LLM client instance
|
|
19
|
-
*/
|
|
20
|
-
setLLMClient(llmClient) {
|
|
21
|
-
this.llmClient = llmClient;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Register assistant configurations
|
|
26
|
-
* @param {Object} assistantConfigs - Map of assistant IDs to configurations
|
|
27
|
-
*/
|
|
28
|
-
registerAssistants(assistantConfigs) {
|
|
29
|
-
Object.entries(assistantConfigs).forEach(([key, config]) => {
|
|
30
|
-
this.assistants.set(key, config);
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Set event handlers for assistant interactions
|
|
36
|
-
* @param {Object} handlers - Handler functions
|
|
37
|
-
*/
|
|
38
|
-
setHandlers(handlers) {
|
|
39
|
-
this.handlers = { ...this.handlers, ...handlers };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Create a new thread for a conversation
|
|
44
|
-
* @param {string} code - User/conversation identifier
|
|
45
|
-
* @param {string} assistantId - Assistant ID to use
|
|
46
|
-
* @param {Array} initialMessages - Initial messages for context
|
|
47
|
-
*/
|
|
48
|
-
async createThread(code, assistantId, initialMessages = []) {
|
|
49
|
-
if (!this.llmClient) {
|
|
50
|
-
throw new Error('LLM client not configured');
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
const thread = await this.llmClient.beta.threads.create();
|
|
55
|
-
|
|
56
|
-
// Add initial messages if provided
|
|
57
|
-
for (const message of initialMessages) {
|
|
58
|
-
await this.llmClient.beta.threads.messages.create(
|
|
59
|
-
thread.id,
|
|
60
|
-
{ role: 'assistant', content: message }
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return {
|
|
65
|
-
code,
|
|
66
|
-
assistantId,
|
|
67
|
-
threadId: thread.id,
|
|
68
|
-
active: true,
|
|
69
|
-
createdAt: new Date()
|
|
70
|
-
};
|
|
71
|
-
} catch (error) {
|
|
72
|
-
throw new Error(`Failed to create thread: ${error.message}`);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Send message to assistant and get response
|
|
78
|
-
* @param {Object} threadData - Thread information
|
|
79
|
-
* @param {string} message - User message
|
|
80
|
-
* @param {Object} runOptions - Additional run options
|
|
81
|
-
*/
|
|
82
|
-
async sendMessage(threadData, message, runOptions = {}) {
|
|
83
|
-
if (!this.llmClient) {
|
|
84
|
-
throw new Error('LLM client not configured');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
try {
|
|
88
|
-
// Add user message to thread
|
|
89
|
-
await this.llmClient.beta.threads.messages.create(
|
|
90
|
-
threadData.threadId,
|
|
91
|
-
{ role: 'user', content: message }
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
// Create run
|
|
95
|
-
const run = await this.llmClient.beta.threads.runs.create(
|
|
96
|
-
threadData.threadId,
|
|
97
|
-
{
|
|
98
|
-
assistant_id: threadData.assistantId,
|
|
99
|
-
...runOptions
|
|
100
|
-
}
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
// Wait for completion and handle actions
|
|
104
|
-
const result = await this.waitForCompletion(threadData.threadId, run.id);
|
|
105
|
-
|
|
106
|
-
if (result.status === 'completed') {
|
|
107
|
-
const messages = await this.llmClient.beta.threads.messages.list(
|
|
108
|
-
threadData.threadId,
|
|
109
|
-
{ run_id: run.id }
|
|
110
|
-
);
|
|
111
|
-
return messages.data[0]?.content[0]?.text?.value || '';
|
|
112
|
-
} else if (result.status === 'requires_action') {
|
|
113
|
-
if (this.handlers.onRequiresAction) {
|
|
114
|
-
return await this.handlers.onRequiresAction(result, threadData);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return null;
|
|
119
|
-
} catch (error) {
|
|
120
|
-
throw new Error(`Assistant interaction failed: ${error.message}`);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Wait for run completion and handle different statuses
|
|
126
|
-
* @param {string} threadId - Thread ID
|
|
127
|
-
* @param {string} runId - Run ID
|
|
128
|
-
*/
|
|
129
|
-
async waitForCompletion(threadId, runId) {
|
|
130
|
-
const maxAttempts = 30;
|
|
131
|
-
let attempts = 0;
|
|
132
|
-
|
|
133
|
-
while (attempts < maxAttempts) {
|
|
134
|
-
const run = await this.llmClient.beta.threads.runs.retrieve(threadId, runId);
|
|
135
|
-
|
|
136
|
-
if (run.status === 'completed') {
|
|
137
|
-
return { status: 'completed', run };
|
|
138
|
-
} else if (run.status === 'requires_action') {
|
|
139
|
-
return { status: 'requires_action', run };
|
|
140
|
-
} else if (run.status === 'failed' || run.status === 'cancelled' || run.status === 'expired') {
|
|
141
|
-
if (this.handlers.onFailed) {
|
|
142
|
-
await this.handlers.onFailed(run);
|
|
143
|
-
}
|
|
144
|
-
return { status: 'failed', run };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Wait before next check
|
|
148
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
149
|
-
attempts++;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
throw new Error('Assistant run timeout');
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Submit tool outputs for function calls
|
|
157
|
-
* @param {string} threadId - Thread ID
|
|
158
|
-
* @param {string} runId - Run ID
|
|
159
|
-
* @param {Array} toolOutputs - Tool outputs array
|
|
160
|
-
*/
|
|
161
|
-
async submitToolOutputs(threadId, runId, toolOutputs) {
|
|
162
|
-
if (!this.llmClient) {
|
|
163
|
-
throw new Error('LLM client not configured');
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
try {
|
|
167
|
-
const run = await this.llmClient.beta.threads.runs.submitToolOutputs(
|
|
168
|
-
threadId,
|
|
169
|
-
runId,
|
|
170
|
-
{ tool_outputs: toolOutputs }
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
return await this.waitForCompletion(threadId, run.id);
|
|
174
|
-
} catch (error) {
|
|
175
|
-
throw new Error(`Failed to submit tool outputs: ${error.message}`);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Add instruction to existing thread
|
|
181
|
-
* @param {Object} threadData - Thread information
|
|
182
|
-
* @param {string} instruction - Additional instruction
|
|
183
|
-
*/
|
|
184
|
-
async addInstruction(threadData, instruction) {
|
|
185
|
-
if (!this.llmClient) {
|
|
186
|
-
throw new Error('LLM client not configured');
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
try {
|
|
190
|
-
const run = await this.llmClient.beta.threads.runs.create(
|
|
191
|
-
threadData.threadId,
|
|
192
|
-
{
|
|
193
|
-
assistant_id: threadData.assistantId,
|
|
194
|
-
additional_instructions: instruction,
|
|
195
|
-
additional_messages: [
|
|
196
|
-
{ role: 'user', content: instruction }
|
|
197
|
-
]
|
|
198
|
-
}
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
const result = await this.waitForCompletion(threadData.threadId, run.id);
|
|
202
|
-
|
|
203
|
-
if (result.status === 'completed') {
|
|
204
|
-
const messages = await this.llmClient.beta.threads.messages.list(
|
|
205
|
-
threadData.threadId,
|
|
206
|
-
{ run_id: run.id }
|
|
207
|
-
);
|
|
208
|
-
return messages.data[0]?.content[0]?.text?.value || '';
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return null;
|
|
212
|
-
} catch (error) {
|
|
213
|
-
throw new Error(`Failed to add instruction: ${error.message}`);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
module.exports = { AssistantManager };
|
|
File without changes
|
|
File without changes
|