@peopl-health/nexus 1.3.2 → 1.4.0
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/README.md +78 -2
- package/lib/adapters/TwilioProvider.js +11 -3
- package/lib/config/airtableConfig.js +29 -13
- package/lib/config/runtimeConfig.js +15 -0
- package/lib/controllers/messageController.js +172 -70
- package/lib/controllers/templateController.js +20 -9
- package/lib/core/NexusMessaging.js +22 -2
- package/lib/helpers/assistantHelper.js +19 -7
- package/lib/index.js +58 -1
- package/lib/interactive/twilioMapper.js +1 -0
- package/lib/models/messageModel.js +54 -6
- package/lib/routes/index.js +1 -1
- package/lib/services/assistantService.js +24 -1
- package/lib/storage/MongoStorage.js +43 -32
- package/lib/templates/templateStructure.js +48 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,8 +24,23 @@ await nexus.initialize({
|
|
|
24
24
|
authToken: process.env.TWILIO_AUTH_TOKEN,
|
|
25
25
|
phoneNumber: process.env.TWILIO_PHONE_NUMBER
|
|
26
26
|
},
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
// Storage (MongoStorage) and Mongo convenience
|
|
28
|
+
storage: 'mongo',
|
|
29
|
+
storageConfig: { dbName: 'nexus' }, // other options
|
|
30
|
+
mongoUri: process.env.MONGODB_URI, // convenience: passed into storageConfig.mongoUri
|
|
31
|
+
|
|
32
|
+
// Media convenience (inject only bucket name)
|
|
33
|
+
media: { bucketName: process.env.AWS_S3_BUCKET_NAME },
|
|
34
|
+
|
|
35
|
+
// Airtable convenience (pick default base or pass a Base ID)
|
|
36
|
+
airtable: {
|
|
37
|
+
base: 'calendar', // friendly key or base ID
|
|
38
|
+
apiKey: process.env.AIRTABLE_API_KEY
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
// Optional LLM (OpenAI)
|
|
42
|
+
llm: 'openai',
|
|
43
|
+
llmConfig: { apiKey: process.env.OPENAI_API_KEY }
|
|
29
44
|
});
|
|
30
45
|
|
|
31
46
|
// Built‑in routes (assistant, conversation, media, message, template)
|
|
@@ -130,3 +145,64 @@ See:
|
|
|
130
145
|
- examples/basic-usage.js
|
|
131
146
|
- examples/assistants/
|
|
132
147
|
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
## Configuration
|
|
151
|
+
|
|
152
|
+
You can configure Nexus via environment variables or at runtime using simple injection helpers. For production apps, prefer passing options or DI, and use envs as a fallback.
|
|
153
|
+
|
|
154
|
+
- Providers/AI
|
|
155
|
+
- TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_PHONE_NUMBER
|
|
156
|
+
- OPENAI_API_KEY
|
|
157
|
+
|
|
158
|
+
- Mongo
|
|
159
|
+
- MONGODB_URI (or pass `mongoUri` when calling `initializeMongoDB()`)
|
|
160
|
+
|
|
161
|
+
- Airtable
|
|
162
|
+
- AIRTABLE_API_KEY
|
|
163
|
+
- AIRTABLE_BASE_ID (or specific IDs below)
|
|
164
|
+
- AIRTABLE_CALENDAR_ID, AIRTABLE_CONFIG_ID, AIRTABLE_HISTORIAL_CLINICO_ID
|
|
165
|
+
- AIRTABLE_LOGGING_ID, AIRTABLE_MONITOREO_ID, AIRTABLE_PROGRAMA_JUNTAS_ID
|
|
166
|
+
- AIRTABLE_SYMPTOMS_ID, AIRTABLE_WEBINARS_LEADS_ID
|
|
167
|
+
|
|
168
|
+
- AWS (S3)
|
|
169
|
+
- AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION (default: us-east-1)
|
|
170
|
+
- AWS_S3_BUCKET_NAME (or inject via `configureMediaController`)
|
|
171
|
+
|
|
172
|
+
- Misc
|
|
173
|
+
- NODE_ENV (affects logging and helpers)
|
|
174
|
+
- USER_DB_MONGO (used as author in message controller)
|
|
175
|
+
|
|
176
|
+
Injection points (dependency injection)
|
|
177
|
+
|
|
178
|
+
- Message scheduling (use your Agenda/Bull model + scheduler):
|
|
179
|
+
```js
|
|
180
|
+
const { configureMessageController } = require('@peopl-health/nexus/lib/controllers/messageController');
|
|
181
|
+
const { AgendaMessage } = require('./src/models/agendaMessageModel');
|
|
182
|
+
const { sendScheduledMessage: appSchedule } = require('./src/messaging/scheduledMessageService');
|
|
183
|
+
|
|
184
|
+
const provider = nexus.getMessaging().getProvider(); // e.g., TwilioProvider
|
|
185
|
+
configureMessageController({
|
|
186
|
+
ScheduledMessage: AgendaMessage, // must expose create/find/findById/deleteOne
|
|
187
|
+
sendScheduledMessage: (saved) => appSchedule(provider.twilioClient, saved),
|
|
188
|
+
// Optional: only if you use bulk‑airtable
|
|
189
|
+
getRecordByFilter: require('@peopl-health/nexus/lib/services/airtableService').getRecordByFilter
|
|
190
|
+
});
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
- Media (inject only your bucket name; AWS SDK is loaded by the lib):
|
|
194
|
+
```js
|
|
195
|
+
const { configureMediaController } = require('@peopl-health/nexus/lib/controllers/mediaController');
|
|
196
|
+
configureMediaController({ bucketName: process.env.AWS_S3_BUCKET_NAME });
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
- Storage settings (MongoStorage only):
|
|
200
|
+
```js
|
|
201
|
+
// Store once; Nexus auto-injects media bucket from storage at startup
|
|
202
|
+
await nexus.getStorage().setConfig('media.bucketName', process.env.AWS_S3_BUCKET_NAME);
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Tips
|
|
206
|
+
- Connect Mongo before `app.listen()` to avoid Mongoose buffering timeouts.
|
|
207
|
+
- If you initialize OpenAI, pass `llm: 'openai'` and `llmConfig: { apiKey }` to `nexus.initialize`.
|
|
208
|
+
- Use the event bus + middleware for custom routing and transformations without forking the default handlers.
|
|
@@ -161,7 +161,7 @@ class TwilioProvider extends MessageProvider {
|
|
|
161
161
|
const links = (content && content.links) || {};
|
|
162
162
|
let approvalsUrl = links.approvals || links.approvalRequests || links.approval_requests;
|
|
163
163
|
if (!approvalsUrl) {
|
|
164
|
-
|
|
164
|
+
return { content, approvalRequest: null, warning: 'Approval endpoint not provided by Twilio' };
|
|
165
165
|
}
|
|
166
166
|
const resp = await axios.get(approvalsUrl, { auth: { username: this.accountSid, password: this.authToken } });
|
|
167
167
|
const data = resp && resp.data ? resp.data : null;
|
|
@@ -188,7 +188,11 @@ class TwilioProvider extends MessageProvider {
|
|
|
188
188
|
const links = (content && content.links) || {};
|
|
189
189
|
let approvalsUrl = links.approvals || links.approvalRequests || links.approval_requests;
|
|
190
190
|
if (!approvalsUrl) {
|
|
191
|
-
|
|
191
|
+
return {
|
|
192
|
+
success: false,
|
|
193
|
+
contentSid,
|
|
194
|
+
warning: 'Twilio account does not expose an approvals endpoint. Approval must be handled manually in the Console.'
|
|
195
|
+
};
|
|
192
196
|
}
|
|
193
197
|
const payload = {
|
|
194
198
|
name,
|
|
@@ -200,7 +204,11 @@ class TwilioProvider extends MessageProvider {
|
|
|
200
204
|
const resp = await axios.post(approvalsUrl, payload, { auth: { username: this.accountSid, password: this.authToken } });
|
|
201
205
|
return { success: true, contentSid, approvalRequest: resp.data || null };
|
|
202
206
|
} catch (error) {
|
|
203
|
-
|
|
207
|
+
return {
|
|
208
|
+
success: false,
|
|
209
|
+
contentSid,
|
|
210
|
+
warning: `Failed to submit for approval via API: ${error.message}`
|
|
211
|
+
};
|
|
204
212
|
}
|
|
205
213
|
}
|
|
206
214
|
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
const Airtable = require('airtable');
|
|
2
|
+
const runtimeConfig = require('./runtimeConfig');
|
|
2
3
|
|
|
3
4
|
const airtableConfig = {
|
|
4
|
-
apiKey:
|
|
5
|
+
apiKey: runtimeConfig.get('AIRTABLE_API_KEY'),
|
|
5
6
|
};
|
|
6
7
|
|
|
7
8
|
// Configurable base IDs - users can override via environment variables
|
|
8
|
-
const Calendar_ID =
|
|
9
|
-
const Config_ID =
|
|
10
|
-
const Historial_Clinico_ID =
|
|
11
|
-
const Logging_ID =
|
|
12
|
-
const Monitoreo_ID =
|
|
13
|
-
const Programa_Juntas_ID =
|
|
14
|
-
const Symptoms_ID =
|
|
15
|
-
const Webinars_Leads_ID =
|
|
9
|
+
const Calendar_ID = require('./runtimeConfig').get('AIRTABLE_CALENDAR_ID') || 'appIjEstWR6972tbF';
|
|
10
|
+
const Config_ID = require('./runtimeConfig').get('AIRTABLE_CONFIG_ID') || 'app9K4EvGI8McC8jF';
|
|
11
|
+
const Historial_Clinico_ID = require('./runtimeConfig').get('AIRTABLE_HISTORIAL_CLINICO_ID') || 'appdUpGUS06XIzVnY';
|
|
12
|
+
const Logging_ID = require('./runtimeConfig').get('AIRTABLE_LOGGING_ID') || 'appQ7YhzfebRDbSPJ';
|
|
13
|
+
const Monitoreo_ID = require('./runtimeConfig').get('AIRTABLE_MONITOREO_ID') || 'appdvraKSdp0XVn5n';
|
|
14
|
+
const Programa_Juntas_ID = require('./runtimeConfig').get('AIRTABLE_PROGRAMA_JUNTAS_ID') || 'appKFWzkcDEWlrXBE';
|
|
15
|
+
const Symptoms_ID = require('./runtimeConfig').get('AIRTABLE_SYMPTOMS_ID') || 'appQRhZlQ9tMfYZWJ';
|
|
16
|
+
const Webinars_Leads_ID = require('./runtimeConfig').get('AIRTABLE_WEBINARS_LEADS_ID') || 'appzjpVXTI0TgqGPq';
|
|
16
17
|
|
|
17
18
|
// Initialize Airtable only if API key is provided
|
|
18
19
|
let airtable = null;
|
|
@@ -20,6 +21,17 @@ if (airtableConfig.apiKey) {
|
|
|
20
21
|
airtable = new Airtable({ apiKey: airtableConfig.apiKey });
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
const BASE_MAP = {
|
|
25
|
+
calendar: Calendar_ID,
|
|
26
|
+
config: Config_ID,
|
|
27
|
+
historial: Historial_Clinico_ID,
|
|
28
|
+
logging: Logging_ID,
|
|
29
|
+
monitoreo: Monitoreo_ID,
|
|
30
|
+
programa: Programa_Juntas_ID,
|
|
31
|
+
symptoms: Symptoms_ID,
|
|
32
|
+
webinars: Webinars_Leads_ID
|
|
33
|
+
};
|
|
34
|
+
|
|
23
35
|
module.exports = {
|
|
24
36
|
airtable,
|
|
25
37
|
config: airtableConfig,
|
|
@@ -33,13 +45,17 @@ module.exports = {
|
|
|
33
45
|
Webinars_Leads_ID,
|
|
34
46
|
|
|
35
47
|
// Helper function to get base by ID
|
|
36
|
-
getBase: (
|
|
48
|
+
getBase: (baseKeyOrId = require('./runtimeConfig').get('AIRTABLE_BASE_ID')) => {
|
|
37
49
|
if (!airtable) {
|
|
38
50
|
throw new Error('Airtable not configured. Please set AIRTABLE_API_KEY environment variable.');
|
|
39
51
|
}
|
|
40
|
-
|
|
41
|
-
|
|
52
|
+
let resolved = baseKeyOrId;
|
|
53
|
+
if (resolved && BASE_MAP[String(resolved).toLowerCase()]) {
|
|
54
|
+
resolved = BASE_MAP[String(resolved).toLowerCase()];
|
|
55
|
+
}
|
|
56
|
+
if (!resolved) {
|
|
57
|
+
throw new Error('Airtable base identifier not provided. Pass a base ID or one of: ' + Object.keys(BASE_MAP).join(', ') + '.');
|
|
42
58
|
}
|
|
43
|
-
return airtable.base(
|
|
59
|
+
return airtable.base(resolved);
|
|
44
60
|
}
|
|
45
61
|
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const overrides = new Map();
|
|
2
|
+
|
|
3
|
+
function set(key, value) {
|
|
4
|
+
if (!key) return;
|
|
5
|
+
overrides.set(String(key), value);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function get(key, fallback = undefined) {
|
|
9
|
+
const k = String(key);
|
|
10
|
+
if (overrides.has(k)) return overrides.get(k);
|
|
11
|
+
return process.env[k] !== undefined ? process.env[k] : fallback;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = { set, get };
|
|
15
|
+
|
|
@@ -1,10 +1,84 @@
|
|
|
1
1
|
const { Message } = require('../models/messageModel.js');
|
|
2
|
-
const { ScheduledMessage } = require('../models/agendaMessageModel.js');
|
|
3
|
-
const {
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
const { ScheduledMessage: DefaultScheduledMessage } = require('../models/agendaMessageModel.js');
|
|
3
|
+
const {
|
|
4
|
+
sendMessage: defaultSendMessage,
|
|
5
|
+
sendScheduledMessage: defaultSendScheduledMessage
|
|
6
|
+
} = require('../core/NexusMessaging');
|
|
7
|
+
const { getRecordByFilter: defaultGetRecordByFilter } = require('../services/airtableService');
|
|
8
|
+
const runtimeConfig = require('../config/runtimeConfig');
|
|
6
9
|
const moment = require('moment-timezone');
|
|
7
10
|
|
|
11
|
+
const dependencies = {
|
|
12
|
+
ScheduledMessage: DefaultScheduledMessage,
|
|
13
|
+
getRecordByFilter: defaultGetRecordByFilter,
|
|
14
|
+
sendScheduledMessage: defaultSendScheduledMessage,
|
|
15
|
+
sendMessage: defaultSendMessage
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const configureMessageController = (overrides = {}) => {
|
|
19
|
+
if (overrides.ScheduledMessage) dependencies.ScheduledMessage = overrides.ScheduledMessage;
|
|
20
|
+
if (overrides.getRecordByFilter) dependencies.getRecordByFilter = overrides.getRecordByFilter;
|
|
21
|
+
if (overrides.sendScheduledMessage) dependencies.sendScheduledMessage = overrides.sendScheduledMessage;
|
|
22
|
+
if (overrides.sendMessage) dependencies.sendMessage = overrides.sendMessage;
|
|
23
|
+
return { ...dependencies };
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const ensureDependency = (res, condition, errorMessage) => {
|
|
27
|
+
if (condition) return true;
|
|
28
|
+
res.status(500).json({ success: false, error: errorMessage });
|
|
29
|
+
return false;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const persistScheduledMessage = async (Model, payload) => {
|
|
33
|
+
if (Model && typeof Model.create === 'function') {
|
|
34
|
+
return await Model.create(payload);
|
|
35
|
+
}
|
|
36
|
+
if (typeof Model === 'function') {
|
|
37
|
+
const instance = new Model(payload);
|
|
38
|
+
if (typeof instance.save === 'function') {
|
|
39
|
+
await instance.save();
|
|
40
|
+
return instance;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
throw new Error('ScheduledMessage model must expose create() or constructor+save()');
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const pickMessageId = (result, fallbackDoc) => {
|
|
47
|
+
if (result) {
|
|
48
|
+
const id = result.sid || result.messageId || result.id || result._id;
|
|
49
|
+
if (id) return id;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (fallbackDoc) {
|
|
53
|
+
const docId = fallbackDoc.wa_id || fallbackDoc.sid || fallbackDoc.id || fallbackDoc._id;
|
|
54
|
+
if (docId) return typeof docId === 'object' && docId.toString ? docId.toString() : docId;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return null;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const normalizeCode = (code) => {
|
|
61
|
+
if (!code || typeof code !== 'string') return code;
|
|
62
|
+
|
|
63
|
+
const trimmed = code.trim();
|
|
64
|
+
if (trimmed.startsWith('whatsapp:')) {
|
|
65
|
+
return trimmed;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (trimmed.includes('@')) {
|
|
69
|
+
return trimmed;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (trimmed.startsWith('+')) {
|
|
73
|
+
return `whatsapp:${trimmed}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (/^\d+$/.test(trimmed)) {
|
|
77
|
+
return `whatsapp:+${trimmed}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return trimmed;
|
|
81
|
+
};
|
|
8
82
|
|
|
9
83
|
const sendMessageController = async (req, res) => {
|
|
10
84
|
const {
|
|
@@ -18,35 +92,43 @@ const sendMessageController = async (req, res) => {
|
|
|
18
92
|
contentSid = null,
|
|
19
93
|
variables = null
|
|
20
94
|
} = req.body || {};
|
|
21
|
-
const author =
|
|
95
|
+
const author = runtimeConfig.get('USER_DB_MONGO');
|
|
22
96
|
const sendMoment = sendTime ? moment.tz(sendTime, timeZone) + 2500 : new Date();
|
|
23
97
|
|
|
98
|
+
const ScheduledMessageModel = dependencies.ScheduledMessage;
|
|
99
|
+
if (!ensureDependency(res, ScheduledMessageModel, 'ScheduledMessage model not configured. Call configureMessageController() to inject it.')) return;
|
|
100
|
+
|
|
101
|
+
const hasScheduler = typeof dependencies.sendScheduledMessage === 'function';
|
|
102
|
+
const hasDirectSend = typeof dependencies.sendMessage === 'function';
|
|
103
|
+
if (!ensureDependency(res, hasScheduler || hasDirectSend, 'No messaging provider configured. Ensure Nexus.initialize() completed before using the message controllers.')) return;
|
|
104
|
+
|
|
24
105
|
try {
|
|
25
|
-
const
|
|
106
|
+
const payload = {
|
|
26
107
|
fileUrl,
|
|
27
108
|
message,
|
|
28
109
|
fileType,
|
|
29
110
|
timeZone: timeZone === '' ? null : timeZone,
|
|
30
111
|
sendTime: sendMoment,
|
|
31
|
-
contentSid
|
|
112
|
+
contentSid,
|
|
32
113
|
hidePreview,
|
|
33
|
-
code,
|
|
114
|
+
code: normalizeCode(code),
|
|
34
115
|
author,
|
|
35
116
|
extraDelay: 0,
|
|
36
117
|
variables
|
|
37
118
|
};
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
119
|
+
|
|
120
|
+
const scheduledRecord = await persistScheduledMessage(ScheduledMessageModel, payload);
|
|
121
|
+
const result = hasScheduler
|
|
122
|
+
? await dependencies.sendScheduledMessage(scheduledRecord)
|
|
123
|
+
: await dependencies.sendMessage(payload);
|
|
124
|
+
|
|
43
125
|
res.status(200).json({
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
messageId: result
|
|
126
|
+
status: 200,
|
|
127
|
+
response: hasScheduler ? 'Message scheduled to be sent once!' : 'Message sent successfully',
|
|
128
|
+
messageId: pickMessageId(result, scheduledRecord)
|
|
47
129
|
});
|
|
48
130
|
} catch (err) {
|
|
49
|
-
console.error('Error scheduling individual message:', err
|
|
131
|
+
console.error('Error scheduling individual message:', err);
|
|
50
132
|
res.status(500).json({ status: false, error: err.message });
|
|
51
133
|
}
|
|
52
134
|
};
|
|
@@ -56,64 +138,71 @@ const sendBulkMessageController = async (req, res) => {
|
|
|
56
138
|
fileUrl,
|
|
57
139
|
message,
|
|
58
140
|
fileType,
|
|
59
|
-
codes,
|
|
141
|
+
codes = [],
|
|
60
142
|
sendTime = new Date(),
|
|
61
143
|
timeZone = 'Etc/GMT',
|
|
62
144
|
hidePreview = false,
|
|
63
145
|
contentSid = null,
|
|
64
146
|
variables = null
|
|
65
147
|
} = req.body || {};
|
|
66
|
-
const author =
|
|
67
|
-
const sendMoment = sendTime ? moment.tz(sendTime, timeZone) + 20*1000 : new Date();
|
|
148
|
+
const author = runtimeConfig.get('USER_DB_MONGO');
|
|
149
|
+
const sendMoment = sendTime ? moment.tz(sendTime, timeZone) + 20 * 1000 : new Date();
|
|
150
|
+
|
|
151
|
+
const ScheduledMessageModel = dependencies.ScheduledMessage;
|
|
152
|
+
if (!ensureDependency(res, ScheduledMessageModel, 'ScheduledMessage model not configured. Call configureMessageController() to inject it.')) return;
|
|
153
|
+
|
|
154
|
+
const hasScheduler = typeof dependencies.sendScheduledMessage === 'function';
|
|
155
|
+
const hasDirectSend = typeof dependencies.sendMessage === 'function';
|
|
156
|
+
if (!ensureDependency(res, hasScheduler || hasDirectSend, 'No messaging provider configured. Ensure Nexus.initialize() completed before using the message controllers.')) return;
|
|
68
157
|
|
|
69
158
|
try {
|
|
70
159
|
let numSend = 0;
|
|
71
160
|
let extraDelay = 0;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
for (const
|
|
75
|
-
const
|
|
161
|
+
const scheduledPayloads = [];
|
|
162
|
+
|
|
163
|
+
for (const recipient of codes) {
|
|
164
|
+
const payload = {
|
|
76
165
|
fileUrl,
|
|
77
|
-
message
|
|
166
|
+
message,
|
|
78
167
|
fileType,
|
|
79
168
|
timeZone: timeZone === '' ? null : timeZone,
|
|
80
169
|
sendTime: new Date(sendMoment + extraDelay),
|
|
81
|
-
contentSid
|
|
170
|
+
contentSid,
|
|
82
171
|
hidePreview,
|
|
83
|
-
code,
|
|
172
|
+
code: normalizeCode(recipient),
|
|
84
173
|
author,
|
|
85
174
|
extraDelay,
|
|
86
175
|
variables
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Add to scheduledMessages for both saving and scheduling
|
|
90
|
-
scheduledMessages.push(scheduledMessage);
|
|
176
|
+
};
|
|
91
177
|
|
|
92
|
-
|
|
178
|
+
scheduledPayloads.push(payload);
|
|
93
179
|
extraDelay += Math.floor(Math.random() * 5001) + 5000;
|
|
94
180
|
numSend += 1;
|
|
95
181
|
}
|
|
96
182
|
|
|
97
|
-
// Schedule all messages with Agenda in parallel
|
|
98
183
|
const sentMessages = await Promise.all(
|
|
99
|
-
|
|
100
|
-
const savedMessage = await
|
|
101
|
-
|
|
184
|
+
scheduledPayloads.map(async (payload) => {
|
|
185
|
+
const savedMessage = await persistScheduledMessage(ScheduledMessageModel, payload);
|
|
186
|
+
const result = hasScheduler
|
|
187
|
+
? await dependencies.sendScheduledMessage(savedMessage)
|
|
188
|
+
: await dependencies.sendMessage(payload);
|
|
189
|
+
return { result, scheduled: savedMessage };
|
|
102
190
|
})
|
|
103
191
|
);
|
|
104
192
|
|
|
105
193
|
console.log(`Send bulk of ${numSend} messages`);
|
|
106
194
|
|
|
107
|
-
|
|
108
|
-
|
|
195
|
+
const messageIds = sentMessages
|
|
196
|
+
.map(({ result, scheduled }) => pickMessageId(result, scheduled))
|
|
197
|
+
.filter((id) => id !== null);
|
|
109
198
|
|
|
110
|
-
res.status(200).json({
|
|
111
|
-
status: true,
|
|
199
|
+
res.status(200).json({
|
|
200
|
+
status: true,
|
|
112
201
|
response: 'Bulk message sent',
|
|
113
|
-
messageIds
|
|
202
|
+
messageIds
|
|
114
203
|
});
|
|
115
204
|
} catch (err) {
|
|
116
|
-
console.
|
|
205
|
+
console.error('Error sending bulk messages:', err);
|
|
117
206
|
res.status(500).send(err.message);
|
|
118
207
|
}
|
|
119
208
|
};
|
|
@@ -133,8 +222,18 @@ const sendBulkMessageAirtableController = async (req, res) => {
|
|
|
133
222
|
condition = '1',
|
|
134
223
|
variables = null
|
|
135
224
|
} = req.body || {};
|
|
136
|
-
const author =
|
|
137
|
-
const sendMoment = sendTime ? moment.tz(sendTime, timeZone) + 20*1000 : new Date();
|
|
225
|
+
const author = runtimeConfig.get('USER_DB_MONGO');
|
|
226
|
+
const sendMoment = sendTime ? moment.tz(sendTime, timeZone) + 20 * 1000 : new Date();
|
|
227
|
+
|
|
228
|
+
const ScheduledMessageModel = dependencies.ScheduledMessage;
|
|
229
|
+
if (!ensureDependency(res, ScheduledMessageModel, 'ScheduledMessage model not configured. Call configureMessageController() to inject it.')) return;
|
|
230
|
+
|
|
231
|
+
const hasScheduler = typeof dependencies.sendScheduledMessage === 'function';
|
|
232
|
+
const hasDirectSend = typeof dependencies.sendMessage === 'function';
|
|
233
|
+
if (!ensureDependency(res, hasScheduler || hasDirectSend, 'No messaging provider configured. Ensure Nexus.initialize() completed before using the message controllers.')) return;
|
|
234
|
+
|
|
235
|
+
const airtableFetcher = dependencies.getRecordByFilter;
|
|
236
|
+
if (!ensureDependency(res, typeof airtableFetcher === 'function', 'Airtable getRecordByFilter not configured. Call configureMessageController() to inject it.')) return;
|
|
138
237
|
|
|
139
238
|
const regex = /\[(.*?)\]/g;
|
|
140
239
|
const envVariables = [];
|
|
@@ -144,15 +243,14 @@ const sendBulkMessageAirtableController = async (req, res) => {
|
|
|
144
243
|
}
|
|
145
244
|
|
|
146
245
|
try {
|
|
147
|
-
const rows = await
|
|
246
|
+
const rows = await airtableFetcher(baseId, tableName, condition);
|
|
148
247
|
let extraDelay = 0;
|
|
149
|
-
let curMessage = message;
|
|
150
248
|
const sentPhones = new Set();
|
|
151
|
-
const
|
|
249
|
+
const scheduledPayloads = [];
|
|
250
|
+
|
|
251
|
+
for (const row of rows || []) {
|
|
252
|
+
let customMessage = message;
|
|
152
253
|
|
|
153
|
-
for (const row of rows) {
|
|
154
|
-
let customMessage = curMessage;
|
|
155
|
-
|
|
156
254
|
for (const envVar of envVariables) {
|
|
157
255
|
let value = row[envVar];
|
|
158
256
|
if (Array.isArray(value)) value = value[0];
|
|
@@ -164,54 +262,57 @@ const sendBulkMessageAirtableController = async (req, res) => {
|
|
|
164
262
|
let code = row[columnPhone];
|
|
165
263
|
if (Array.isArray(code)) code = code[0];
|
|
166
264
|
if (!code) continue;
|
|
167
|
-
|
|
265
|
+
code = normalizeCode(code);
|
|
168
266
|
|
|
169
267
|
if (sentPhones.has(code)) continue;
|
|
170
268
|
sentPhones.add(code);
|
|
171
269
|
|
|
172
|
-
|
|
173
|
-
const scheduledMessage = {
|
|
270
|
+
const payload = {
|
|
174
271
|
fileUrl,
|
|
175
272
|
message: customMessage,
|
|
176
273
|
fileType,
|
|
177
274
|
timeZone: timeZone === '' ? null : timeZone,
|
|
178
275
|
sendTime: new Date(sendMoment + extraDelay),
|
|
179
|
-
contentSid
|
|
276
|
+
contentSid,
|
|
180
277
|
hidePreview,
|
|
181
278
|
code,
|
|
182
279
|
author,
|
|
183
280
|
variables
|
|
184
281
|
};
|
|
185
|
-
|
|
186
|
-
|
|
282
|
+
|
|
283
|
+
scheduledPayloads.push(payload);
|
|
187
284
|
extraDelay += Math.floor(Math.random() * 5001) + 5000;
|
|
188
285
|
}
|
|
189
286
|
|
|
190
287
|
const sentMessages = await Promise.all(
|
|
191
|
-
|
|
192
|
-
const savedMessage = await
|
|
193
|
-
|
|
288
|
+
scheduledPayloads.map(async (payload) => {
|
|
289
|
+
const savedMessage = await persistScheduledMessage(ScheduledMessageModel, payload);
|
|
290
|
+
const result = hasScheduler
|
|
291
|
+
? await dependencies.sendScheduledMessage(savedMessage)
|
|
292
|
+
: await dependencies.sendMessage(payload);
|
|
293
|
+
return { result, scheduled: savedMessage };
|
|
194
294
|
})
|
|
195
295
|
);
|
|
196
296
|
|
|
197
|
-
|
|
198
|
-
|
|
297
|
+
const messageIds = sentMessages
|
|
298
|
+
.map(({ result, scheduled }) => pickMessageId(result, scheduled))
|
|
299
|
+
.filter((id) => id !== null);
|
|
199
300
|
|
|
200
|
-
console.log(`Iterate over ${rows.length} rows`);
|
|
301
|
+
console.log(`Iterate over ${(rows || []).length} rows`);
|
|
201
302
|
|
|
202
|
-
res.status(200).json({
|
|
203
|
-
status: true,
|
|
303
|
+
res.status(200).json({
|
|
304
|
+
status: true,
|
|
204
305
|
response: 'Airtable message sent',
|
|
205
|
-
messageIds
|
|
306
|
+
messageIds
|
|
206
307
|
});
|
|
207
308
|
} catch (err) {
|
|
208
|
-
console.
|
|
309
|
+
console.error('Error sending Airtable bulk messages:', err);
|
|
209
310
|
res.status(500).send(err.message);
|
|
210
311
|
}
|
|
211
312
|
};
|
|
212
313
|
|
|
213
314
|
const getLastInteractionController = async (req, res) => {
|
|
214
|
-
const { code } = req.
|
|
315
|
+
const { code } = req.query;
|
|
215
316
|
|
|
216
317
|
try {
|
|
217
318
|
const lastMessage = await Message.findOne({
|
|
@@ -224,7 +325,7 @@ const getLastInteractionController = async (req, res) => {
|
|
|
224
325
|
if (!lastMessage) {
|
|
225
326
|
return res.status(404).send({ message: 'No messages found for the provided code.' });
|
|
226
327
|
}
|
|
227
|
-
|
|
328
|
+
|
|
228
329
|
const createdAt = new Date(lastMessage.createdAt);
|
|
229
330
|
const now = new Date();
|
|
230
331
|
const timeDiffMs = now - createdAt;
|
|
@@ -241,5 +342,6 @@ module.exports = {
|
|
|
241
342
|
sendMessageController,
|
|
242
343
|
sendBulkMessageController,
|
|
243
344
|
sendBulkMessageAirtableController,
|
|
244
|
-
getLastInteractionController
|
|
245
|
-
|
|
345
|
+
getLastInteractionController,
|
|
346
|
+
configureMessageController
|
|
347
|
+
};
|
|
@@ -1,11 +1,3 @@
|
|
|
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
|
-
};
|
|
8
|
-
|
|
9
1
|
// Check if provider supports templates
|
|
10
2
|
const checkTemplateSupport = () => {
|
|
11
3
|
if (!nexusProvider) {
|
|
@@ -20,10 +12,29 @@ const checkTemplateSupport = () => {
|
|
|
20
12
|
|
|
21
13
|
const { handleApiError } = require('../utils/errorHandler');
|
|
22
14
|
|
|
23
|
-
const {
|
|
15
|
+
const {
|
|
16
|
+
Template,
|
|
17
|
+
configureNexusProvider: configureTemplateProvider
|
|
18
|
+
} = require('../templates/templateStructure');
|
|
24
19
|
const predefinedTemplates = require('../templates/predefinedTemplates');
|
|
25
20
|
|
|
26
21
|
|
|
22
|
+
// Nexus provider will be injected - templates only work with Twilio
|
|
23
|
+
let nexusProvider = null;
|
|
24
|
+
|
|
25
|
+
// Configure Nexus provider
|
|
26
|
+
const configureNexusProvider = (provider) => {
|
|
27
|
+
nexusProvider = provider;
|
|
28
|
+
if (typeof configureTemplateProvider === 'function') {
|
|
29
|
+
try {
|
|
30
|
+
configureTemplateProvider(provider);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
console.warn('[templateController] Failed to propagate provider to template structure:', err?.message || err);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
|
|
27
38
|
const getTemplateModel = () => {
|
|
28
39
|
// Require the concrete model; enforce presence instead of stubbing
|
|
29
40
|
try {
|
|
@@ -441,7 +441,25 @@ class NexusMessaging {
|
|
|
441
441
|
}
|
|
442
442
|
}
|
|
443
443
|
|
|
444
|
-
|
|
444
|
+
let defaultInstance = new NexusMessaging();
|
|
445
|
+
|
|
446
|
+
const setDefaultInstance = (instance) => {
|
|
447
|
+
if (!instance) {
|
|
448
|
+
throw new Error('setDefaultInstance requires a NexusMessaging instance');
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const isCompatible = typeof instance.sendMessage === 'function' &&
|
|
452
|
+
typeof instance.sendScheduledMessage === 'function' &&
|
|
453
|
+
typeof instance.processIncomingMessage === 'function';
|
|
454
|
+
|
|
455
|
+
if (!isCompatible) {
|
|
456
|
+
throw new Error('setDefaultInstance received an incompatible object');
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
defaultInstance = instance;
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const getDefaultInstance = () => defaultInstance;
|
|
445
463
|
|
|
446
464
|
const sendMessage = async (messageData) => {
|
|
447
465
|
return await defaultInstance.sendMessage(messageData);
|
|
@@ -454,5 +472,7 @@ const sendScheduledMessage = async (scheduledMessage) => {
|
|
|
454
472
|
module.exports = {
|
|
455
473
|
NexusMessaging,
|
|
456
474
|
sendMessage,
|
|
457
|
-
sendScheduledMessage
|
|
475
|
+
sendScheduledMessage,
|
|
476
|
+
setDefaultInstance,
|
|
477
|
+
getDefaultInstance
|
|
458
478
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const { downloadFileFromS3, generatePresignedUrl } = require('../config/awsConfig.js');
|
|
2
|
-
const
|
|
2
|
+
const llmConfig = require('../config/llmConfig.js');
|
|
3
3
|
|
|
4
4
|
const { Message } = require('../models/messageModel.js');
|
|
5
5
|
|
|
@@ -16,7 +16,9 @@ const mode = process.env.NODE_ENV || 'dev';
|
|
|
16
16
|
|
|
17
17
|
async function checkRunStatus(assistant, thread_id, run_id, retryCount = 0, maxRetries = 30) {
|
|
18
18
|
try {
|
|
19
|
-
const
|
|
19
|
+
const client = llmConfig.openaiClient;
|
|
20
|
+
if (!client) throw new Error('OpenAI client not configured');
|
|
21
|
+
const run = await client.beta.threads.runs.retrieve(thread_id, run_id);
|
|
20
22
|
console.log(`Status: ${run.status} ${thread_id} ${run_id} (attempt ${retryCount + 1})`);
|
|
21
23
|
|
|
22
24
|
if (run.status === 'failed' || run.status === 'expired' || run.status === 'incomplete') {
|
|
@@ -47,7 +49,9 @@ async function checkRunStatus(assistant, thread_id, run_id, retryCount = 0, maxR
|
|
|
47
49
|
|
|
48
50
|
async function checkIfFinished(text) {
|
|
49
51
|
try {
|
|
50
|
-
const
|
|
52
|
+
const client = llmConfig.openaiClient;
|
|
53
|
+
if (!client) throw new Error('OpenAI client not configured');
|
|
54
|
+
const completion = await client.chat.completions.create({
|
|
51
55
|
model: 'gpt-4o-mini',
|
|
52
56
|
messages: [
|
|
53
57
|
{
|
|
@@ -210,7 +214,9 @@ async function processMessage(code, reply, thread) {
|
|
|
210
214
|
const imageAnalysis = await analyzeImage(fileName);
|
|
211
215
|
console.log(imageAnalysis);
|
|
212
216
|
const invalidAnalysis = ['NOT_MEDICAL', 'QUALITY_INSUFFICIENT'];
|
|
213
|
-
|
|
217
|
+
if (imageAnalysis.medical_relevance) {
|
|
218
|
+
url = await generatePresignedUrl(reply.media.bucketName, reply.media.key);
|
|
219
|
+
}
|
|
214
220
|
if (imageAnalysis.has_table) {
|
|
215
221
|
messagesChat.push({
|
|
216
222
|
type: 'text',
|
|
@@ -223,7 +229,9 @@ async function processMessage(code, reply, thread) {
|
|
|
223
229
|
});
|
|
224
230
|
} else {
|
|
225
231
|
console.log('Add attachment');
|
|
226
|
-
const
|
|
232
|
+
const client = llmConfig.openaiClient;
|
|
233
|
+
if (!client) throw new Error('OpenAI client not configured');
|
|
234
|
+
const file = await client.files.create({
|
|
227
235
|
file: fs.createReadStream(fileName),
|
|
228
236
|
purpose: 'vision',
|
|
229
237
|
});
|
|
@@ -233,7 +241,9 @@ async function processMessage(code, reply, thread) {
|
|
|
233
241
|
});
|
|
234
242
|
}
|
|
235
243
|
} else if (fileName.includes('audio')) {
|
|
236
|
-
const
|
|
244
|
+
const client = llmConfig.openaiClient;
|
|
245
|
+
if (!client) throw new Error('OpenAI client not configured');
|
|
246
|
+
const audioTranscript = await client.audio.transcriptions.create({
|
|
237
247
|
model: 'whisper-1',
|
|
238
248
|
file: fs.createReadStream(fileName),
|
|
239
249
|
response_format: 'text',
|
|
@@ -251,7 +261,9 @@ async function processMessage(code, reply, thread) {
|
|
|
251
261
|
console.log('messagesChat', messagesChat);
|
|
252
262
|
console.log('attachments', attachments);
|
|
253
263
|
|
|
254
|
-
|
|
264
|
+
const client = llmConfig.openaiClient;
|
|
265
|
+
if (!client) throw new Error('OpenAI client not configured');
|
|
266
|
+
await client.beta.threads.messages.create(thread.thread_id, {
|
|
255
267
|
role: 'user',
|
|
256
268
|
content: messagesChat,
|
|
257
269
|
attachments: attachments
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { NexusMessaging } = require('./core/NexusMessaging');
|
|
1
|
+
const { NexusMessaging, setDefaultInstance } = require('./core/NexusMessaging');
|
|
2
2
|
const { MongoStorage } = require('./storage/MongoStorage');
|
|
3
3
|
const { MessageParser } = require('./utils/messageParser');
|
|
4
4
|
const { DefaultLLMProvider } = require('./utils/defaultLLMProvider');
|
|
@@ -6,6 +6,8 @@ const { loadNexusConfig } = require('./config/configLoader');
|
|
|
6
6
|
const templateController = require('./controllers/templateController');
|
|
7
7
|
const templateFlowController = require('./controllers/templateFlowController');
|
|
8
8
|
const interactive = require('./interactive');
|
|
9
|
+
const runtimeConfig = require('./config/runtimeConfig');
|
|
10
|
+
const llmConfigModule = require('./config/llmConfig');
|
|
9
11
|
const {
|
|
10
12
|
configureLLMProvider: configureAssistantsLLM,
|
|
11
13
|
registerAssistant,
|
|
@@ -22,6 +24,11 @@ class Nexus {
|
|
|
22
24
|
constructor(config = {}) {
|
|
23
25
|
this.config = config;
|
|
24
26
|
this.messaging = new NexusMessaging(config.messaging || {});
|
|
27
|
+
try {
|
|
28
|
+
setDefaultInstance(this.messaging);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
console.warn('[Nexus] Failed to set default messaging instance:', err?.message || err);
|
|
31
|
+
}
|
|
25
32
|
this.storage = null;
|
|
26
33
|
this.messageParser = null;
|
|
27
34
|
this.llmProvider = null;
|
|
@@ -75,6 +82,23 @@ class Nexus {
|
|
|
75
82
|
console.warn('Warning: failed to auto-configure template providers:', e?.message || e);
|
|
76
83
|
}
|
|
77
84
|
|
|
85
|
+
|
|
86
|
+
// Convenience: handle mongoUri early (before storage connect)
|
|
87
|
+
try {
|
|
88
|
+
if (options.mongoUri) {
|
|
89
|
+
if (storage === 'mongo') {
|
|
90
|
+
if (typeof storageConfig === 'object' && !storageConfig.mongoUri) {
|
|
91
|
+
storageConfig.mongoUri = options.mongoUri;
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
// If not using MongoStorage but a URI is provided, initialize default mongoose connection
|
|
95
|
+
await this.messaging.initializeMongoDB(options.mongoUri);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch (dbErr) {
|
|
99
|
+
console.warn('[Nexus] mongo convenience warning:', dbErr?.message || dbErr);
|
|
100
|
+
}
|
|
101
|
+
|
|
78
102
|
// Initialize storage if provided
|
|
79
103
|
if (storage) {
|
|
80
104
|
try {
|
|
@@ -104,6 +128,39 @@ class Nexus {
|
|
|
104
128
|
// Initialize default LLM provider if requested
|
|
105
129
|
if (llm === 'openai') {
|
|
106
130
|
this.llmProvider = new DefaultLLMProvider(llmConfig);
|
|
131
|
+
try {
|
|
132
|
+
if (this.llmProvider && typeof this.llmProvider.getClient === 'function') {
|
|
133
|
+
llmConfigModule.openaiClient = this.llmProvider.getClient();
|
|
134
|
+
}
|
|
135
|
+
} catch (err) {
|
|
136
|
+
console.warn('[Nexus] Failed to expose OpenAI client:', err?.message || err);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Convenience: handle common top-level config for mongo, media bucket, airtable
|
|
140
|
+
try {
|
|
141
|
+
// Mongo URI passthrough for storage=mongo
|
|
142
|
+
if (options.mongoUri && storage === 'mongo') {
|
|
143
|
+
if (typeof storageConfig === 'object') {
|
|
144
|
+
storageConfig.mongoUri = storageConfig.mongoUri || options.mongoUri;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Media bucket (overrides storage setting)
|
|
149
|
+
if (options.media && options.media.bucketName) {
|
|
150
|
+
runtimeConfig.set('AWS_S3_BUCKET_NAME', options.media.bucketName);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Airtable base default (accepts alias like 'calendar' or an ID)
|
|
154
|
+
if (options.airtable && options.airtable.base) {
|
|
155
|
+
runtimeConfig.set('AIRTABLE_BASE_ID', options.airtable.base);
|
|
156
|
+
}
|
|
157
|
+
if (options.airtable && options.airtable.apiKey) {
|
|
158
|
+
runtimeConfig.set('AIRTABLE_API_KEY', options.airtable.apiKey);
|
|
159
|
+
}
|
|
160
|
+
} catch (cfgErr) {
|
|
161
|
+
console.warn('[Nexus] convenience config warning:', cfgErr?.message || cfgErr);
|
|
162
|
+
}
|
|
163
|
+
|
|
107
164
|
}
|
|
108
165
|
|
|
109
166
|
|
|
@@ -25,6 +25,7 @@ function toTwilioContent(spec) {
|
|
|
25
25
|
|
|
26
26
|
if (type === 'flow') {
|
|
27
27
|
content.types['twilio/flows'] = {
|
|
28
|
+
type: flow?.type || spec.flowType || 'FLOW',
|
|
28
29
|
body: flow?.body || body || '',
|
|
29
30
|
button_text: flow?.buttonText || spec.buttonText || undefined,
|
|
30
31
|
subtitle: flow?.subtitle || spec.subtitle || undefined,
|
|
@@ -52,9 +52,40 @@ messageSchema.pre('save', function (next) {
|
|
|
52
52
|
const Message = mongoose.model('Message', messageSchema);
|
|
53
53
|
|
|
54
54
|
async function insertMessage(values) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
try {
|
|
56
|
+
const skipNumbers = [
|
|
57
|
+
'5215592261426@s.whatsapp.net',
|
|
58
|
+
'5215547411345@s.whatsapp.net',
|
|
59
|
+
'51985959446@s.whatsapp.net'
|
|
60
|
+
];
|
|
61
|
+
const messageData = {
|
|
62
|
+
nombre_whatsapp: values.nombre_whatsapp,
|
|
63
|
+
numero: values.numero,
|
|
64
|
+
body: values.body,
|
|
65
|
+
timestamp: values.timestamp,
|
|
66
|
+
message_id: values.message_id,
|
|
67
|
+
is_group: values.is_group,
|
|
68
|
+
is_media: values.is_media,
|
|
69
|
+
group_id: values.group_id,
|
|
70
|
+
reply_id: values.reply_id,
|
|
71
|
+
from_me: values.from_me,
|
|
72
|
+
processed: skipNumbers.includes(values.numero),
|
|
73
|
+
media: values.media ? values.media : null,
|
|
74
|
+
content_sid: values.content_sid || null,
|
|
75
|
+
template_variables: values.template_variables || null
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
await Message.findOneAndUpdate(
|
|
79
|
+
{ message_id: values.message_id, body: values.body },
|
|
80
|
+
{ $setOnInsert: messageData },
|
|
81
|
+
{ upsert: true, new: true }
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
console.log('Message inserted or updated successfully');
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.error('Error inserting message:', err);
|
|
87
|
+
throw err;
|
|
88
|
+
}
|
|
58
89
|
}
|
|
59
90
|
|
|
60
91
|
function formatTimestamp(unixTimestamp) {
|
|
@@ -62,7 +93,7 @@ function formatTimestamp(unixTimestamp) {
|
|
|
62
93
|
return date.toLocaleString('sv-MX', {
|
|
63
94
|
timeZone: 'America/Mexico_City',
|
|
64
95
|
hour12: false,
|
|
65
|
-
}).replace(' ', 'T').slice(0, 19);
|
|
96
|
+
}).replace(' ', 'T').slice(0, 19);
|
|
66
97
|
}
|
|
67
98
|
|
|
68
99
|
function getMessageValues(message, content, reply, is_media) {
|
|
@@ -90,10 +121,27 @@ function getMessageValues(message, content, reply, is_media) {
|
|
|
90
121
|
};
|
|
91
122
|
}
|
|
92
123
|
|
|
124
|
+
async function getContactDisplayName(contactNumber) {
|
|
125
|
+
try {
|
|
126
|
+
const latestMessage = await Message.findOne({ numero: contactNumber })
|
|
127
|
+
.sort({ timestamp: -1 })
|
|
128
|
+
.select('nombre_whatsapp');
|
|
129
|
+
|
|
130
|
+
if (latestMessage && latestMessage.nombre_whatsapp && latestMessage.nombre_whatsapp.trim() !== '') {
|
|
131
|
+
return latestMessage.nombre_whatsapp;
|
|
132
|
+
} else {
|
|
133
|
+
return contactNumber;
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error(`Error fetching display name for ${contactNumber}:`, error);
|
|
137
|
+
return contactNumber;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
93
141
|
module.exports = {
|
|
94
142
|
Message,
|
|
95
|
-
// Backward-compatible helper used by helpers
|
|
96
143
|
insertMessage,
|
|
97
144
|
getMessageValues,
|
|
98
|
-
formatTimestamp
|
|
145
|
+
formatTimestamp,
|
|
146
|
+
getContactDisplayName
|
|
99
147
|
};
|
package/lib/routes/index.js
CHANGED
|
@@ -32,7 +32,7 @@ const messageRouteDefinitions = {
|
|
|
32
32
|
'POST /send': 'sendMessageController',
|
|
33
33
|
'POST /send-bulk': 'sendBulkMessageController',
|
|
34
34
|
'POST /send-bulk-airtable': 'sendBulkMessageAirtableController',
|
|
35
|
-
'
|
|
35
|
+
'GET /last': 'getLastInteractionController'
|
|
36
36
|
};
|
|
37
37
|
|
|
38
38
|
const templateRouteDefinitions = {
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
const { Historial_Clinico_ID } = require('../config/airtableConfig.js');
|
|
1
|
+
const { Historial_Clinico_ID, Monitoreo_ID } = require('../config/airtableConfig.js');
|
|
2
|
+
const AWS = require('../config/awsConfig.js');
|
|
3
|
+
const { combineImagesToPDF, cleanupFiles } = require('../helpers/filesHelper.js');
|
|
4
|
+
const { addRecord } = require('../services/airtableService.js');
|
|
5
|
+
const runtimeConfig = require('../config/runtimeConfig');
|
|
2
6
|
|
|
3
7
|
let llmProvider = null;
|
|
4
8
|
const configureLLMProvider = (provider) => {
|
|
@@ -243,6 +247,25 @@ const replyAssistant = async function (code, message_ = null, thread_ = null, ru
|
|
|
243
247
|
|
|
244
248
|
if (urls.length > 0) {
|
|
245
249
|
console.log('urls', urls);
|
|
250
|
+
const { pdfBuffer, processedFiles } = await combineImagesToPDF({ code });
|
|
251
|
+
console.log('AFTER COMBINED IN BUFFER', processedFiles);
|
|
252
|
+
const key = `${code}-${Date.now()}-combined.pdf`;
|
|
253
|
+
const bucket = runtimeConfig.get('AWS_S3_BUCKET_NAME');
|
|
254
|
+
if (bucket && pdfBuffer) {
|
|
255
|
+
await AWS.uploadBufferToS3(pdfBuffer, bucket, key, 'application/pdf');
|
|
256
|
+
const url = await AWS.generatePresignedUrl(bucket, key);
|
|
257
|
+
const curRow = await getCurRow(Monitoreo_ID, code);
|
|
258
|
+
const customer_id = curRow?.[0]?.recordID || curRow?.[0]?.record_id || curRow?.[0]?.id || null;
|
|
259
|
+
console.log('customer_id:', customer_id);
|
|
260
|
+
try {
|
|
261
|
+
await addRecord(Monitoreo_ID, 'estudios', [{ fields: { estudios: urls, combined_estudios: [{ url }], patient_id: customer_id ? [customer_id] : [] } }]);
|
|
262
|
+
} catch (e) {
|
|
263
|
+
console.warn('Failed to add Airtable estudios record:', e?.message || e);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (processedFiles && processedFiles.length) {
|
|
267
|
+
await cleanupFiles(processedFiles);
|
|
268
|
+
}
|
|
246
269
|
}
|
|
247
270
|
|
|
248
271
|
thread = await getThread(code);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const mongoose = require('mongoose');
|
|
2
|
+
const runtimeConfig = require('../config/runtimeConfig');
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* MongoDB storage interface for messages and interactions
|
|
@@ -16,21 +17,7 @@ class MongoStorage {
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
createSchemas() {
|
|
19
|
-
const
|
|
20
|
-
messageId: String,
|
|
21
|
-
numero: String,
|
|
22
|
-
body: String,
|
|
23
|
-
timestamp: String,
|
|
24
|
-
isGroup: { type: Boolean, default: false },
|
|
25
|
-
isMedia: { type: Boolean, default: false },
|
|
26
|
-
fromMe: { type: Boolean, default: false },
|
|
27
|
-
contentSid: String,
|
|
28
|
-
isTemplate: { type: Boolean, default: false },
|
|
29
|
-
templateVariables: String,
|
|
30
|
-
provider: String,
|
|
31
|
-
createdAt: { type: Date, default: Date.now }
|
|
32
|
-
});
|
|
33
|
-
|
|
20
|
+
const { Message } = require('../models/messageModel');
|
|
34
21
|
const interactionSchema = new mongoose.Schema({
|
|
35
22
|
messageId: String,
|
|
36
23
|
numero: String,
|
|
@@ -54,7 +41,7 @@ class MongoStorage {
|
|
|
54
41
|
});
|
|
55
42
|
|
|
56
43
|
return {
|
|
57
|
-
Message
|
|
44
|
+
Message,
|
|
58
45
|
Interaction: mongoose.models.Interaction || mongoose.model('Interaction', interactionSchema),
|
|
59
46
|
Thread: mongoose.models.Thread || mongoose.model('Thread', threadSchema)
|
|
60
47
|
};
|
|
@@ -74,28 +61,52 @@ class MongoStorage {
|
|
|
74
61
|
|
|
75
62
|
async saveMessage(messageData) {
|
|
76
63
|
try {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
timestamp: this.formatTimestamp(messageData.timestamp),
|
|
82
|
-
isGroup: messageData.isGroup || false,
|
|
83
|
-
isMedia: messageData.fileType && messageData.fileType !== 'text',
|
|
84
|
-
fromMe: messageData.fromMe || false,
|
|
85
|
-
contentSid: messageData.contentSid,
|
|
86
|
-
isTemplate: !!messageData.contentSid,
|
|
87
|
-
templateVariables: messageData.variables ? JSON.stringify(messageData.variables) : null,
|
|
88
|
-
provider: messageData.provider
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
await message.save();
|
|
92
|
-
return message;
|
|
64
|
+
const values = this.buildLegacyMessageValues(messageData);
|
|
65
|
+
const { insertMessage } = require('../models/messageModel');
|
|
66
|
+
await insertMessage(values);
|
|
67
|
+
return values;
|
|
93
68
|
} catch (error) {
|
|
94
69
|
console.error('Error saving message:', error);
|
|
95
70
|
throw error;
|
|
96
71
|
}
|
|
97
72
|
}
|
|
98
73
|
|
|
74
|
+
buildLegacyMessageValues(messageData = {}) {
|
|
75
|
+
const numero = messageData.to || messageData.code || messageData.numero || messageData.from;
|
|
76
|
+
const normalizedNumero = typeof numero === 'string' ? numero : '';
|
|
77
|
+
const isGroup = normalizedNumero.includes('@g.us');
|
|
78
|
+
const isMedia = messageData.isMedia === true || (messageData.fileType && messageData.fileType !== 'text');
|
|
79
|
+
const now = new Date();
|
|
80
|
+
const timestamp = now.toISOString();
|
|
81
|
+
const nombre = messageData.nombre_whatsapp || messageData.author || messageData.fromName || runtimeConfig.get('USER_DB_MONGO') || process.env.USER_DB_MONGO || 'Nexus';
|
|
82
|
+
const textBody = messageData.message || messageData.body || (messageData.contentSid ? `[Template:${messageData.contentSid}]` : isMedia ? `[Media:${messageData.fileType || 'attachment'}]` : '');
|
|
83
|
+
const providerId = messageData.messageId || messageData.sid || messageData.id || messageData._id || `pending-${now.getTime()}-${Math.floor(Math.random()*1000)}`;
|
|
84
|
+
|
|
85
|
+
const media = messageData.media || (messageData.fileUrl ? {
|
|
86
|
+
url: messageData.fileUrl,
|
|
87
|
+
mediaType: messageData.fileType === 'text' ? null : (messageData.fileType || 'document'),
|
|
88
|
+
fileName: messageData.fileName || null,
|
|
89
|
+
contentType: messageData.contentType || null,
|
|
90
|
+
metadata: messageData.mediaMetadata || null
|
|
91
|
+
} : null);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
nombre_whatsapp: nombre,
|
|
95
|
+
numero: normalizedNumero,
|
|
96
|
+
body: textBody,
|
|
97
|
+
timestamp,
|
|
98
|
+
message_id: providerId,
|
|
99
|
+
is_group: isGroup,
|
|
100
|
+
is_media: isMedia,
|
|
101
|
+
group_id: isGroup ? normalizedNumero : null,
|
|
102
|
+
reply_id: messageData.reply_id || messageData.replyId || null,
|
|
103
|
+
from_me: messageData.fromMe !== undefined ? messageData.fromMe : true,
|
|
104
|
+
media,
|
|
105
|
+
content_sid: messageData.contentSid || null,
|
|
106
|
+
template_variables: messageData.variables ? JSON.stringify(messageData.variables) : null
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
99
110
|
async saveInteractive(interactionData) {
|
|
100
111
|
try {
|
|
101
112
|
const interaction = new this.schemas.Interaction({
|
|
@@ -43,19 +43,21 @@ class Template {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
addBodyVariation(text, variableDescriptions = []) {
|
|
46
|
+
const normalizedVariables = Template.normalizeVariables(variableDescriptions);
|
|
46
47
|
this.variations.push({
|
|
47
48
|
text: text,
|
|
48
|
-
variables:
|
|
49
|
+
variables: normalizedVariables
|
|
49
50
|
});
|
|
50
51
|
return this;
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
setBody(text, variableDescriptions = []) {
|
|
54
|
-
|
|
55
|
+
const normalizedVariables = Template.normalizeVariables(variableDescriptions);
|
|
56
|
+
this.addBodyVariation(text, normalizedVariables);
|
|
55
57
|
|
|
56
58
|
const selectedVariation = this.variations.length > 1 ?
|
|
57
59
|
this.variations[Math.floor(Math.random() * this.variations.length)] :
|
|
58
|
-
{ text, variables:
|
|
60
|
+
{ text, variables: normalizedVariables };
|
|
59
61
|
|
|
60
62
|
const enhancedText = `${selectedVariation.text}`;
|
|
61
63
|
|
|
@@ -196,6 +198,49 @@ class Template {
|
|
|
196
198
|
const createdTemplate = await nexusProvider.createTemplate(twilioFormat);
|
|
197
199
|
return createdTemplate;
|
|
198
200
|
}
|
|
201
|
+
|
|
202
|
+
static normalizeVariables(variableDescriptions = []) {
|
|
203
|
+
if (!variableDescriptions) return [];
|
|
204
|
+
|
|
205
|
+
if (Array.isArray(variableDescriptions)) {
|
|
206
|
+
return variableDescriptions;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (typeof variableDescriptions === 'object') {
|
|
210
|
+
return Object.entries(variableDescriptions)
|
|
211
|
+
.sort(([a], [b]) => {
|
|
212
|
+
const na = Number(a);
|
|
213
|
+
const nb = Number(b);
|
|
214
|
+
if (!Number.isNaN(na) && !Number.isNaN(nb)) {
|
|
215
|
+
return na - nb;
|
|
216
|
+
}
|
|
217
|
+
return a.localeCompare(b);
|
|
218
|
+
})
|
|
219
|
+
.map(([key, value], index) => {
|
|
220
|
+
if (typeof value === 'string') {
|
|
221
|
+
return {
|
|
222
|
+
name: `var_${key}`,
|
|
223
|
+
description: value,
|
|
224
|
+
example: `Ejemplo ${key}`
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
if (value && typeof value === 'object') {
|
|
228
|
+
return {
|
|
229
|
+
name: value.name || `var_${key}`,
|
|
230
|
+
description: value.description || `Variable ${key}`,
|
|
231
|
+
example: value.example || value.default || `Ejemplo ${key}`
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
name: `var_${key}`,
|
|
236
|
+
description: `Variable ${key}`,
|
|
237
|
+
example: `Ejemplo ${key}`
|
|
238
|
+
};
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return [];
|
|
243
|
+
}
|
|
199
244
|
}
|
|
200
245
|
|
|
201
246
|
module.exports = {
|