@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.
- package/CHANGELOG.md +0 -0
- package/LICENSE +0 -0
- package/MIGRATION_GUIDE.md +0 -0
- package/README.md +0 -0
- package/examples/.env.example +0 -0
- package/examples/assistants/BaseAssistant.js +1 -242
- package/examples/assistants/DoctorScheduleAssistant.js +83 -0
- package/examples/assistants/ExampleAssistant.js +0 -0
- package/examples/assistants/index.js +3 -1
- package/examples/basic-usage.js +8 -7
- package/examples/consumer-server.js +0 -0
- package/lib/adapters/BaileysProvider.js +0 -0
- package/lib/adapters/TwilioProvider.js +177 -5
- package/lib/adapters/index.js +0 -0
- package/lib/adapters/registry.js +0 -0
- package/lib/assistants/BaseAssistant.js +294 -0
- package/lib/assistants/index.js +5 -0
- package/lib/config/airtableConfig.js +0 -0
- package/lib/config/awsConfig.js +0 -0
- package/lib/config/configLoader.js +0 -0
- package/lib/config/llmConfig.js +0 -0
- package/lib/config/mongoAuthConfig.js +0 -0
- package/lib/config/runtimeConfig.js +0 -0
- package/lib/controllers/assistantController.js +0 -0
- package/lib/controllers/conversationController.js +0 -0
- package/lib/controllers/mediaController.js +0 -0
- package/lib/controllers/messageController.js +0 -0
- package/lib/controllers/templateController.js +0 -0
- package/lib/controllers/templateFlowController.js +0 -0
- package/lib/controllers/uploadController.js +0 -0
- package/lib/core/MessageProvider.js +0 -0
- package/lib/core/NexusMessaging.js +0 -0
- package/lib/core/index.js +0 -0
- package/lib/helpers/assistantHelper.js +0 -0
- package/lib/helpers/baileysHelper.js +0 -0
- package/lib/helpers/filesHelper.js +0 -0
- package/lib/helpers/llmsHelper.js +0 -0
- package/lib/helpers/mediaHelper.js +0 -0
- package/lib/helpers/mongoHelper.js +0 -0
- package/lib/helpers/qrHelper.js +0 -0
- package/lib/helpers/twilioHelper.js +0 -0
- package/lib/helpers/twilioMediaProcessor.js +0 -0
- package/lib/helpers/whatsappHelper.js +0 -0
- package/lib/index.d.ts +51 -0
- package/lib/index.js +6 -0
- package/lib/interactive/index.js +0 -0
- package/lib/interactive/registry.js +0 -0
- package/lib/interactive/twilioMapper.js +0 -0
- package/lib/models/agendaMessageModel.js +0 -0
- package/lib/models/index.js +0 -0
- package/lib/models/messageModel.js +2 -1
- package/lib/models/templateModel.js +0 -0
- package/lib/models/threadModel.js +0 -0
- package/lib/routes/index.js +0 -0
- package/lib/services/airtableService.js +0 -0
- package/lib/services/assistantService.js +66 -4
- package/lib/services/conversationService.js +0 -0
- package/lib/services/twilioService.js +0 -0
- package/lib/storage/MongoStorage.js +15 -4
- package/lib/storage/NoopStorage.js +0 -0
- package/lib/storage/index.js +0 -0
- package/lib/storage/registry.js +0 -0
- package/lib/templates/predefinedTemplates.js +0 -0
- package/lib/templates/templateStructure.js +0 -0
- package/lib/utils/dateUtils.js +0 -0
- package/lib/utils/defaultLLMProvider.js +0 -0
- package/lib/utils/errorHandler.js +0 -0
- package/lib/utils/index.js +0 -0
- package/lib/utils/logger.js +0 -0
- package/lib/utils/mediaValidator.js +0 -0
- package/lib/utils/messageParser.js +0 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
File without changes
|
package/LICENSE
CHANGED
|
File without changes
|
package/MIGRATION_GUIDE.md
CHANGED
|
File without changes
|
package/README.md
CHANGED
|
File without changes
|
package/examples/.env.example
CHANGED
|
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
|
};
|
package/examples/basic-usage.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
|
-
|
|
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
|
|
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
|
-
|
|
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 {
|
|
47
|
+
const { code, message, fileUrl, fileType, variables, contentSid } = messageData;
|
|
48
48
|
|
|
49
49
|
const formattedFrom = this.ensureWhatsAppFormat(this.whatsappNumber);
|
|
50
|
-
const formattedTo = this.ensureWhatsAppFormat(
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
*/
|
package/lib/adapters/index.js
CHANGED
|
File without changes
|
package/lib/adapters/registry.js
CHANGED
|
File without changes
|