@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.
@@ -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 };
@@ -1,4 +1,3 @@
1
- // Temporary stub for llmConfig - should be replaced with Nexus LLM provider
2
1
  module.exports = {
3
- openaiClient: null // This will be replaced with Nexus LLM provider
2
+ openaiClient: null
4
3
  };
@@ -1,7 +1,10 @@
1
- const { Message } = require('../models/messageModel');
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
- // Check if messages exist
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
- // Stub functions for missing services
2
- const TwilioService = {
3
- createTemplate: () => Promise.resolve({ success: false, error: 'Service not available' }),
4
- deleteTemplate: () => Promise.resolve({ success: false, error: 'Service not available' }),
5
- listTemplates: () => Promise.resolve([]),
6
- getTemplate: () => Promise.resolve(null)
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
- const handleApiError = (res, error, message) => {
9
- console.error(message, error);
10
- return res.status(500).json({ success: false, error: message });
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
- // Stub imports for missing dependencies
14
- const Template = class { constructor() {} };
15
- const TemplateModel = {
16
- create: () => Promise.resolve({ success: false, error: 'Model not available' }),
17
- find: () => Promise.resolve([]),
18
- findById: () => Promise.resolve(null)
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
- const twilioRawTemplates = await TwilioService.listTemplates({ limit: parseInt(limit, 10) });
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 TwilioService.checkApprovalStatus(twilioTemplate.sid);
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
- twilioTemplate = await TwilioService.getTemplate(id);
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
- const response = await TwilioService.submitForApproval(contentSid, approvalName, approvalCategory);
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
- const status = await TwilioService.checkApprovalStatus(contentSid);
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
- await TwilioService.deleteTemplate(id);
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
- const twilioTemplate = await TwilioService.getTemplate(sid);
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
- createFlow,
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/MessageParser');
6
- const { DefaultLLMProvider } = require('./utils/DefaultLLMProvider');
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;
@@ -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: mediaController.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: templateController.createFlow,
112
- deleteFlow: templateController.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
+ };
@@ -0,0 +1,8 @@
1
+ const handleApiError = (res, error, message) => {
2
+ console.error(message, error);
3
+ return res.status(500).json({ success: false, error: message });
4
+ };
5
+
6
+ module.exports = {
7
+ handleApiError
8
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "1.1.6",
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 };