@peopl-health/nexus 1.5.8 → 1.5.10

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.
Files changed (72) hide show
  1. package/CHANGELOG.md +0 -0
  2. package/LICENSE +0 -0
  3. package/MIGRATION_GUIDE.md +0 -0
  4. package/README.md +0 -0
  5. package/examples/.env.example +0 -0
  6. package/examples/assistants/BaseAssistant.js +1 -242
  7. package/examples/assistants/DoctorScheduleAssistant.js +83 -0
  8. package/examples/assistants/ExampleAssistant.js +0 -0
  9. package/examples/assistants/index.js +3 -1
  10. package/examples/basic-usage.js +8 -7
  11. package/examples/consumer-server.js +0 -0
  12. package/lib/adapters/BaileysProvider.js +0 -0
  13. package/lib/adapters/TwilioProvider.js +177 -5
  14. package/lib/adapters/index.js +0 -0
  15. package/lib/adapters/registry.js +0 -0
  16. package/lib/assistants/BaseAssistant.js +294 -0
  17. package/lib/assistants/index.js +5 -0
  18. package/lib/config/airtableConfig.js +0 -0
  19. package/lib/config/awsConfig.js +0 -0
  20. package/lib/config/configLoader.js +0 -0
  21. package/lib/config/llmConfig.js +0 -0
  22. package/lib/config/mongoAuthConfig.js +0 -0
  23. package/lib/config/runtimeConfig.js +0 -0
  24. package/lib/controllers/assistantController.js +0 -0
  25. package/lib/controllers/conversationController.js +0 -0
  26. package/lib/controllers/mediaController.js +0 -0
  27. package/lib/controllers/messageController.js +0 -0
  28. package/lib/controllers/templateController.js +0 -0
  29. package/lib/controllers/templateFlowController.js +0 -0
  30. package/lib/controllers/uploadController.js +0 -0
  31. package/lib/core/MessageProvider.js +0 -0
  32. package/lib/core/NexusMessaging.js +0 -0
  33. package/lib/core/index.js +0 -0
  34. package/lib/helpers/assistantHelper.js +0 -0
  35. package/lib/helpers/baileysHelper.js +0 -0
  36. package/lib/helpers/filesHelper.js +0 -0
  37. package/lib/helpers/llmsHelper.js +0 -0
  38. package/lib/helpers/mediaHelper.js +0 -0
  39. package/lib/helpers/mongoHelper.js +0 -0
  40. package/lib/helpers/qrHelper.js +0 -0
  41. package/lib/helpers/twilioHelper.js +0 -0
  42. package/lib/helpers/twilioMediaProcessor.js +0 -0
  43. package/lib/helpers/whatsappHelper.js +0 -0
  44. package/lib/index.d.ts +51 -0
  45. package/lib/index.js +6 -0
  46. package/lib/interactive/index.js +0 -0
  47. package/lib/interactive/registry.js +0 -0
  48. package/lib/interactive/twilioMapper.js +0 -0
  49. package/lib/models/agendaMessageModel.js +0 -0
  50. package/lib/models/index.js +0 -0
  51. package/lib/models/messageModel.js +2 -1
  52. package/lib/models/templateModel.js +0 -0
  53. package/lib/models/threadModel.js +0 -0
  54. package/lib/routes/index.js +0 -0
  55. package/lib/services/airtableService.js +0 -0
  56. package/lib/services/assistantService.js +66 -4
  57. package/lib/services/conversationService.js +0 -0
  58. package/lib/services/twilioService.js +0 -0
  59. package/lib/storage/MongoStorage.js +15 -4
  60. package/lib/storage/NoopStorage.js +0 -0
  61. package/lib/storage/index.js +0 -0
  62. package/lib/storage/registry.js +0 -0
  63. package/lib/templates/predefinedTemplates.js +0 -0
  64. package/lib/templates/templateStructure.js +0 -0
  65. package/lib/utils/dateUtils.js +0 -0
  66. package/lib/utils/defaultLLMProvider.js +0 -0
  67. package/lib/utils/errorHandler.js +0 -0
  68. package/lib/utils/index.js +0 -0
  69. package/lib/utils/logger.js +0 -0
  70. package/lib/utils/mediaValidator.js +0 -0
  71. package/lib/utils/messageParser.js +0 -0
  72. package/package.json +3 -3
package/CHANGELOG.md CHANGED
File without changes
package/LICENSE CHANGED
File without changes
File without changes
package/README.md CHANGED
File without changes
File without changes
@@ -1,242 +1 @@
1
- /**
2
- * Base Assistant class for consumer server extensions
3
- * Provides common functionality for AI assistant implementations
4
- */
5
- class BaseAssistant {
6
- constructor(config = {}) {
7
- this.assistantId = config.assistantId;
8
- this.llmClient = config.llmClient;
9
- this.storage = config.storage;
10
- this.nexus = config.nexus;
11
- this.functions = new Map();
12
- }
13
-
14
- /**
15
- * Register a function that can be called by the assistant
16
- * @param {string} name - Function name
17
- * @param {Function} handler - Function handler
18
- * @param {Object} schema - Function schema for LLM
19
- */
20
- registerFunction(name, handler, schema) {
21
- this.functions.set(name, { handler, schema });
22
- }
23
-
24
- /**
25
- * Get all registered function schemas for LLM
26
- * @returns {Array} Function schemas
27
- */
28
- getFunctionSchemas() {
29
- return Array.from(this.functions.values()).map(f => f.schema);
30
- }
31
-
32
- /**
33
- * Execute a function call from the assistant
34
- * @param {string} name - Function name
35
- * @param {Object} args - Function arguments
36
- * @returns {Promise<any>} Function result
37
- */
38
- async executeFunction(name, args) {
39
- const func = this.functions.get(name);
40
- if (!func) {
41
- throw new Error(`Unknown function: ${name}`);
42
- }
43
- return await func.handler(args);
44
- }
45
-
46
- /**
47
- * Create a new thread for this assistant
48
- * @param {string} userId - User identifier
49
- * @param {Array} initialMessages - Initial messages
50
- * @returns {Promise<Object>} Thread data
51
- */
52
- async createThread(userId, initialMessages = []) {
53
- if (!this.llmClient) {
54
- throw new Error('LLM client not configured');
55
- }
56
-
57
- const thread = await this.llmClient.beta.threads.create();
58
-
59
- // Add initial messages if provided
60
- for (const message of initialMessages) {
61
- await this.llmClient.beta.threads.messages.create(
62
- thread.id,
63
- { role: "assistant", content: message }
64
- );
65
- }
66
-
67
- const threadData = {
68
- code: userId,
69
- assistantId: this.assistantId,
70
- threadId: thread.id,
71
- active: true,
72
- createdAt: new Date()
73
- };
74
-
75
- // Save to storage if available
76
- if (this.storage) {
77
- await this.storage.createThread(threadData);
78
- }
79
-
80
- return threadData;
81
- }
82
-
83
- /**
84
- * Send message to assistant and handle response
85
- * @param {string} userId - User identifier
86
- * @param {string} message - User message
87
- * @param {Object} options - Additional options
88
- * @returns {Promise<string>} Assistant response
89
- */
90
- async sendMessage(userId, message, options = {}) {
91
- if (!this.llmClient) {
92
- throw new Error('LLM client not configured');
93
- }
94
-
95
- // Get or create thread
96
- let threadData = null;
97
- if (this.storage) {
98
- threadData = await this.storage.getThread(userId);
99
- }
100
-
101
- if (!threadData) {
102
- threadData = await this.createThread(userId);
103
- }
104
-
105
- // Add user message to thread
106
- await this.llmClient.beta.threads.messages.create(
107
- threadData.threadId,
108
- { role: "user", content: message }
109
- );
110
-
111
- // Create run with function schemas if available
112
- const runConfig = {
113
- assistant_id: this.assistantId,
114
- ...options
115
- };
116
-
117
- const functionSchemas = this.getFunctionSchemas();
118
- if (functionSchemas.length > 0) {
119
- runConfig.tools = functionSchemas;
120
- }
121
-
122
- const run = await this.llmClient.beta.threads.runs.create(
123
- threadData.threadId,
124
- runConfig
125
- );
126
-
127
- // Wait for completion and handle function calls
128
- const result = await this.waitForCompletion(threadData.threadId, run.id);
129
-
130
- if (result.status === 'completed') {
131
- const messages = await this.llmClient.beta.threads.messages.list(
132
- threadData.threadId,
133
- { run_id: run.id }
134
- );
135
- return messages.data[0]?.content[0]?.text?.value || "";
136
- }
137
-
138
- return null;
139
- }
140
-
141
- /**
142
- * Wait for run completion and handle function calls
143
- * @param {string} threadId - Thread ID
144
- * @param {string} runId - Run ID
145
- * @returns {Promise<Object>} Run result
146
- */
147
- async waitForCompletion(threadId, runId) {
148
- const maxAttempts = 30;
149
- let attempts = 0;
150
-
151
- while (attempts < maxAttempts) {
152
- const run = await this.llmClient.beta.threads.runs.retrieve(threadId, runId);
153
-
154
- if (run.status === 'completed') {
155
- return { status: 'completed', run };
156
- } else if (run.status === 'requires_action') {
157
- // Handle function calls
158
- const toolOutputs = await this.handleRequiredActions(run);
159
-
160
- if (toolOutputs.length > 0) {
161
- await this.llmClient.beta.threads.runs.submitToolOutputs(
162
- threadId,
163
- runId,
164
- { tool_outputs: toolOutputs }
165
- );
166
-
167
- // Continue waiting for completion
168
- attempts = 0; // Reset attempts after submitting outputs
169
- await this.delay(2000);
170
- continue;
171
- }
172
- } else if (run.status === 'failed' || run.status === 'cancelled' || run.status === 'expired') {
173
- return { status: 'failed', run };
174
- }
175
-
176
- await this.delay(2000);
177
- attempts++;
178
- }
179
-
180
- throw new Error('Assistant run timeout');
181
- }
182
-
183
- /**
184
- * Handle required actions (function calls)
185
- * @param {Object} run - Run object with required actions
186
- * @returns {Promise<Array>} Tool outputs
187
- */
188
- async handleRequiredActions(run) {
189
- const toolCalls = run.required_action?.submit_tool_outputs?.tool_calls || [];
190
- const toolOutputs = [];
191
-
192
- for (const toolCall of toolCalls) {
193
- try {
194
- const functionName = toolCall.function.name;
195
- const args = JSON.parse(toolCall.function.arguments);
196
-
197
- const result = await this.executeFunction(functionName, args);
198
-
199
- toolOutputs.push({
200
- tool_call_id: toolCall.id,
201
- output: JSON.stringify(result)
202
- });
203
- } catch (error) {
204
- console.error(`Error executing function ${toolCall.function.name}:`, error);
205
- toolOutputs.push({
206
- tool_call_id: toolCall.id,
207
- output: JSON.stringify({ error: error.message })
208
- });
209
- }
210
- }
211
-
212
- return toolOutputs;
213
- }
214
-
215
- /**
216
- * Send a message through Nexus
217
- * @param {string} to - Recipient
218
- * @param {string} message - Message text
219
- * @param {Object} options - Additional options
220
- */
221
- async sendNexusMessage(to, message, options = {}) {
222
- if (!this.nexus) {
223
- throw new Error('Nexus instance not provided');
224
- }
225
-
226
- return await this.nexus.sendMessage({
227
- to,
228
- message,
229
- ...options
230
- });
231
- }
232
-
233
- /**
234
- * Utility delay function
235
- * @param {number} ms - Milliseconds to delay
236
- */
237
- delay(ms) {
238
- return new Promise(resolve => setTimeout(resolve, ms));
239
- }
240
- }
241
-
242
- module.exports = { BaseAssistant };
1
+ module.exports = require('../../lib/assistants/BaseAssistant');
@@ -0,0 +1,83 @@
1
+ const { BaseAssistant } = require('../../lib/assistants/BaseAssistant');
2
+
3
+ class DoctorScheduleAssistant extends BaseAssistant {
4
+ constructor(thread) {
5
+ super({ assistantId: 'doctor_schedule', thread });
6
+
7
+ this.registerTool({
8
+ name: 'storePatientAppointment',
9
+ description: 'Stores a requested appointment so that an operator can follow up with the patient.',
10
+ parameters: {
11
+ type: 'object',
12
+ required: ['contact_number', 'appointment_type'],
13
+ properties: {
14
+ contact_number: {
15
+ type: 'string',
16
+ description: 'WhatsApp phone number of the patient in international format (e.g. +521234567890).'
17
+ },
18
+ appointment_type: {
19
+ type: 'string',
20
+ description: 'Type of medical appointment requested by the patient.'
21
+ },
22
+ patient_name: {
23
+ type: 'string',
24
+ description: 'Full name of the patient if provided.'
25
+ },
26
+ preferred_date: {
27
+ type: 'string',
28
+ description: 'Preferred appointment date in ISO format (YYYY-MM-DD).'
29
+ },
30
+ preferred_time: {
31
+ type: 'string',
32
+ description: 'Preferred appointment time in HH:mm format.'
33
+ },
34
+ notes: {
35
+ type: 'string',
36
+ description: 'Any extra notes supplied by the patient.'
37
+ }
38
+ }
39
+ },
40
+ handler: async (payload = {}) => {
41
+ console.log('[DoctorScheduleAssistant] storePatientAppointment', payload);
42
+
43
+ if (!payload.contact_number || !payload.appointment_type) {
44
+ return JSON.stringify({
45
+ success: false,
46
+ message: 'Falta el número de contacto o el tipo de cita. Pide esa información al paciente.'
47
+ });
48
+ }
49
+
50
+ const normalizedNumber = String(payload.contact_number).replace(/[^0-9+]/g, '');
51
+
52
+ const appointment = {
53
+ contact_number: normalizedNumber,
54
+ appointment_type: payload.appointment_type,
55
+ patient_name: payload.patient_name || null,
56
+ preferred_date: payload.preferred_date || null,
57
+ preferred_time: payload.preferred_time || null,
58
+ notes: payload.notes || null
59
+ };
60
+
61
+ console.log('[DoctorScheduleAssistant] Appointment captured', appointment);
62
+
63
+ return JSON.stringify({ success: true, appointment });
64
+ }
65
+ });
66
+ }
67
+
68
+ async create(code, curRow) {
69
+ await super.create(code, curRow);
70
+
71
+ const summary = this.lastMessages
72
+ ? [{ role: 'assistant', content: `Últimos mensajes:
73
+ ${this.lastMessages}` }]
74
+ : [];
75
+
76
+ const client = this.client;
77
+ if (!client) throw new Error('OpenAI client not configured');
78
+
79
+ return await client.beta.threads.create({ messages: summary });
80
+ }
81
+ }
82
+
83
+ module.exports = { DoctorScheduleAssistant };
File without changes
@@ -1,7 +1,9 @@
1
1
  const { BaseAssistant } = require('./BaseAssistant');
2
2
  const { ExampleAssistant } = require('./ExampleAssistant');
3
+ const { DoctorScheduleAssistant } = require('./DoctorScheduleAssistant');
3
4
 
4
5
  module.exports = {
5
6
  BaseAssistant,
6
- ExampleAssistant
7
+ ExampleAssistant,
8
+ DoctorScheduleAssistant
7
9
  };
@@ -1,12 +1,13 @@
1
1
  const express = require('express');
2
- const { NexusMessaging, setupDefaultRoutes } = require('@peopl-health/nexus');
2
+ require('dotenv').config();
3
+ const { Nexus, setupDefaultRoutes } = require('@peopl-health/nexus');
3
4
 
4
5
  const app = express();
5
6
  app.use(express.json());
6
7
 
7
8
  async function startServer() {
8
9
  // Initialize Nexus with all services
9
- const nexus = new NexusMessaging();
10
+ const nexus = new Nexus();
10
11
 
11
12
  await nexus.initialize({
12
13
  // MongoDB connection
@@ -17,7 +18,7 @@ async function startServer() {
17
18
  providerConfig: {
18
19
  accountSid: process.env.TWILIO_ACCOUNT_SID,
19
20
  authToken: process.env.TWILIO_AUTH_TOKEN,
20
- phoneNumber: process.env.TWILIO_PHONE_NUMBER
21
+ whatsappNumber: process.env.TWILIO_WHATSAPP_NUMBER
21
22
  }
22
23
  });
23
24
 
@@ -27,7 +28,7 @@ async function startServer() {
27
28
  // Add webhook endpoint for incoming messages
28
29
  app.post('/webhook', async (req, res) => {
29
30
  try {
30
- await nexus.processIncomingMessage(req.body);
31
+ await nexus.messaging.processIncomingMessage(req.body);
31
32
  res.status(200).send('OK');
32
33
  } catch (error) {
33
34
  console.error('Webhook error:', error);
@@ -38,10 +39,10 @@ async function startServer() {
38
39
  // Custom endpoint example
39
40
  app.get('/status', (req, res) => {
40
41
  res.json({
41
- connected: nexus.isConnected(),
42
+ connected: nexus.messaging.isConnected(),
42
43
  provider: 'twilio',
43
- mongodb: nexus.mongodb?.readyState === 1,
44
- airtable: !!nexus.airtable
44
+ mongodb: nexus.messaging.mongodb?.readyState === 1,
45
+ airtable: !!nexus.messaging.airtable
45
46
  });
46
47
  });
47
48
 
File without changes
File without changes
@@ -44,10 +44,11 @@ class TwilioProvider extends MessageProvider {
44
44
  throw new Error('Twilio provider not initialized');
45
45
  }
46
46
 
47
- const { to, message, fileUrl, fileType, variables, contentSid } = messageData;
47
+ const { code, message, fileUrl, fileType, variables, contentSid } = messageData;
48
48
 
49
49
  const formattedFrom = this.ensureWhatsAppFormat(this.whatsappNumber);
50
- const formattedTo = this.ensureWhatsAppFormat(to);
50
+ const formattedTo = this.ensureWhatsAppFormat(code);
51
+
51
52
 
52
53
  if (!formattedFrom || !formattedTo) {
53
54
  throw new Error('Invalid sender or recipient number');
@@ -60,6 +61,12 @@ class TwilioProvider extends MessageProvider {
60
61
 
61
62
  // Handle template messages
62
63
  if (contentSid) {
64
+ // Render template and add to messageData for storage
65
+ const renderedMessage = await this.renderTemplate(contentSid, variables);
66
+ if (renderedMessage) {
67
+ messageData.body = renderedMessage; // Add rendered content for storage
68
+ }
69
+
63
70
  messageParams.contentSid = contentSid;
64
71
  if (variables && Object.keys(variables).length > 0) {
65
72
  const formattedVariables = {};
@@ -140,17 +147,18 @@ class TwilioProvider extends MessageProvider {
140
147
  : async (payload) => await this.sendMessage(payload);
141
148
 
142
149
  console.log('[TwilioProvider] Scheduled message created', {
143
- to: scheduledMessage.to || scheduledMessage.code,
150
+ to: scheduledMessage.code,
144
151
  delay,
145
152
  hasContentSid: Boolean(scheduledMessage.contentSid)
146
153
  });
147
154
 
148
155
  setTimeout(async () => {
149
156
  try {
150
- const payload = { ...scheduledMessage };
157
+ // Convert Mongoose document to plain object if needed
158
+ const payload = scheduledMessage.toObject ? scheduledMessage.toObject() : { ...scheduledMessage };
151
159
  delete payload.__nexusSend;
152
160
  console.log('[TwilioProvider] Timer fired', {
153
- to: payload.to || payload.code,
161
+ to: payload.code,
154
162
  hasMessage: Boolean(payload.message || payload.body),
155
163
  hasMedia: Boolean(payload.fileUrl)
156
164
  });
@@ -311,6 +319,170 @@ class TwilioProvider extends MessageProvider {
311
319
  }
312
320
  }
313
321
 
322
+ /**
323
+ * Render template content with variables
324
+ * @param {string} contentSid - The Twilio content SID
325
+ * @param {Object} variables - The variables object with keys like "1", "2"
326
+ * @returns {Promise<string|null>} The rendered message content or null if not found
327
+ */
328
+ async renderTemplate(contentSid, variables) {
329
+ try {
330
+ if (!contentSid) return null;
331
+
332
+ const template = await this.getTemplate(contentSid);
333
+
334
+ if (!template || !template.types) {
335
+ console.warn('[TwilioProvider] Template not found or has no types:', contentSid);
336
+ return null;
337
+ }
338
+
339
+ // Extract text content from different template types
340
+ let textContent = this.extractTextFromTemplate(template);
341
+
342
+ if (!textContent) {
343
+ console.warn('[TwilioProvider] No text content found in template:', contentSid);
344
+ return null;
345
+ }
346
+
347
+ // Render variables if provided
348
+ if (variables && typeof variables === 'object' && Object.keys(variables).length > 0) {
349
+ return this.renderTemplateWithVariables(textContent, variables);
350
+ }
351
+
352
+ return textContent.trim();
353
+ } catch (error) {
354
+ console.error('[TwilioProvider] Error rendering template:', error.message);
355
+ return null;
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Extract text content from different template types
361
+ * @param {Object} template - The Twilio template object
362
+ * @returns {string} The extracted text content
363
+ */
364
+ extractTextFromTemplate(template) {
365
+ const types = template.types || {};
366
+
367
+ // Handle plain text templates
368
+ if (types['twilio/text']) {
369
+ return types['twilio/text'].body || '';
370
+ }
371
+
372
+ // Handle quick reply templates
373
+ if (types['twilio/quick-reply']) {
374
+ const quickReply = types['twilio/quick-reply'];
375
+ let text = quickReply.body || '';
376
+
377
+ // Add quick reply options
378
+ if (quickReply.actions && Array.isArray(quickReply.actions)) {
379
+ const options = quickReply.actions
380
+ .filter(action => action.title)
381
+ .map(action => `• ${action.title}`)
382
+ .join('\n');
383
+ if (options) {
384
+ text += (text ? '\n\n' : '') + options;
385
+ }
386
+ }
387
+
388
+ return text;
389
+ }
390
+
391
+ // Handle list templates
392
+ if (types['twilio/list']) {
393
+ const list = types['twilio/list'];
394
+ let text = list.body || '';
395
+
396
+ // Add list items
397
+ if (list.items && Array.isArray(list.items)) {
398
+ const items = list.items
399
+ .filter(item => item.title)
400
+ .map((item, index) => `${index + 1}. ${item.title}`)
401
+ .join('\n');
402
+ if (items) {
403
+ text += (text ? '\n\n' : '') + items;
404
+ }
405
+ }
406
+
407
+ return text;
408
+ }
409
+
410
+ // Handle button templates
411
+ if (types['twilio/button']) {
412
+ const button = types['twilio/button'];
413
+ let text = button.body || '';
414
+
415
+ // Add button options
416
+ if (button.actions && Array.isArray(button.actions)) {
417
+ const buttons = button.actions
418
+ .filter(action => action.title)
419
+ .map(action => `[${action.title}]`)
420
+ .join(' ');
421
+ if (buttons) {
422
+ text += (text ? '\n\n' : '') + buttons;
423
+ }
424
+ }
425
+
426
+ return text;
427
+ }
428
+
429
+ // Handle flow templates (fallback to body)
430
+ if (types['twilio/flows']) {
431
+ return types['twilio/flows'].body || '';
432
+ }
433
+
434
+ // Handle media templates (extract caption)
435
+ if (types['twilio/media']) {
436
+ return types['twilio/media'].caption || '';
437
+ }
438
+
439
+ // Fallback: try to find any body content
440
+ for (const typeKey of Object.keys(types)) {
441
+ const type = types[typeKey];
442
+ if (type && typeof type === 'object' && type.body) {
443
+ return type.body;
444
+ }
445
+ }
446
+
447
+ return '';
448
+ }
449
+
450
+ /**
451
+ * Render template content with variables
452
+ * @param {string} templateBody - The template body with placeholders like {{1}}, {{2}}
453
+ * @param {Object} variables - The variables object with keys like "1", "2"
454
+ * @returns {string} The rendered message content
455
+ */
456
+ renderTemplateWithVariables(templateBody, variables) {
457
+ if (!templateBody || typeof templateBody !== 'string') {
458
+ return '';
459
+ }
460
+
461
+ if (!variables || typeof variables !== 'object') {
462
+ return templateBody;
463
+ }
464
+
465
+ try {
466
+ let rendered = templateBody;
467
+
468
+ // Replace placeholders like {{1}}, {{2}}, etc. with variable values
469
+ Object.keys(variables).forEach(key => {
470
+ const placeholder = `{{${key}}}`;
471
+ const value = variables[key] || '';
472
+
473
+ // Simple string replacement - more reliable than regex for this use case
474
+ if (rendered.includes(placeholder)) {
475
+ rendered = rendered.replace(new RegExp(placeholder.replace(/[{}]/g, '\\$&'), 'g'), value);
476
+ }
477
+ });
478
+
479
+ return rendered.trim();
480
+ } catch (error) {
481
+ console.warn('[TwilioProvider] Error rendering template variables:', error.message);
482
+ return templateBody; // Return original template if rendering fails
483
+ }
484
+ }
485
+
314
486
  /**
315
487
  * Check template approval status using Twilio Content API helpers
316
488
  */
File without changes
File without changes