@peopl-health/nexus 1.2.0 → 1.3.1
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/MIGRATION_GUIDE.md +71 -379
- package/README.md +80 -559
- package/lib/adapters/BaileysProvider.js +25 -1
- package/lib/adapters/TwilioProvider.js +107 -0
- package/lib/adapters/registry.js +29 -0
- package/lib/config/configLoader.js +38 -0
- package/lib/controllers/assistantController.js +6 -6
- package/lib/controllers/messageController.js +68 -21
- package/lib/controllers/templateController.js +5 -17
- package/lib/core/NexusMessaging.js +151 -35
- package/lib/helpers/assistantHelper.js +6 -6
- package/lib/helpers/baileysHelper.js +3 -3
- package/lib/helpers/twilioHelper.js +3 -3
- package/lib/index.d.ts +1 -1
- package/lib/index.js +84 -9
- package/lib/interactive/index.js +86 -0
- package/lib/interactive/registry.js +31 -0
- package/lib/interactive/twilioMapper.js +60 -0
- package/lib/models/agendaMessageModel.js +24 -0
- package/lib/models/messageModel.js +8 -0
- package/lib/routes/index.js +3 -1
- package/lib/services/airtableService.js +4 -0
- package/lib/services/assistantService.js +15 -2
- package/lib/storage/NoopStorage.js +19 -0
- package/lib/storage/registry.js +31 -0
- package/lib/templates/predefinedTemplates.js +1 -3
- package/lib/utils/defaultLLMProvider.js +1 -1
- package/lib/utils/index.js +3 -5
- package/package.json +2 -1
- package/lib/services/appointmentService.js +0 -55
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const mongoose = require('mongoose');
|
|
2
|
+
|
|
3
|
+
const scheduledMessageSchema = new mongoose.Schema({
|
|
4
|
+
fileUrl: String,
|
|
5
|
+
message: String,
|
|
6
|
+
fileType: String,
|
|
7
|
+
sendTime: Date,
|
|
8
|
+
hidePreview: Boolean,
|
|
9
|
+
contentSid: String,
|
|
10
|
+
code: String,
|
|
11
|
+
author: String,
|
|
12
|
+
variables: Object,
|
|
13
|
+
extraDelay: Number,
|
|
14
|
+
timeZone: { type: String, default: 'Etc/GMT'},
|
|
15
|
+
status: { type: String, default: 'pending' },
|
|
16
|
+
wa_id: { type: String, default: null },
|
|
17
|
+
createdAt: { type: Date, default: Date.now }
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const ScheduledMessage = mongoose.model('ScheduledMessage', scheduledMessageSchema);
|
|
21
|
+
|
|
22
|
+
module.exports = {
|
|
23
|
+
ScheduledMessage
|
|
24
|
+
};
|
|
@@ -51,6 +51,12 @@ messageSchema.pre('save', function (next) {
|
|
|
51
51
|
|
|
52
52
|
const Message = mongoose.model('Message', messageSchema);
|
|
53
53
|
|
|
54
|
+
async function insertMessage(values) {
|
|
55
|
+
const msg = new Message(values);
|
|
56
|
+
await msg.save();
|
|
57
|
+
return msg;
|
|
58
|
+
}
|
|
59
|
+
|
|
54
60
|
function formatTimestamp(unixTimestamp) {
|
|
55
61
|
const date = new Date(unixTimestamp * 1000);
|
|
56
62
|
return date.toLocaleString('sv-MX', {
|
|
@@ -86,6 +92,8 @@ function getMessageValues(message, content, reply, is_media) {
|
|
|
86
92
|
|
|
87
93
|
module.exports = {
|
|
88
94
|
Message,
|
|
95
|
+
// Backward-compatible helper used by helpers
|
|
96
|
+
insertMessage,
|
|
89
97
|
getMessageValues,
|
|
90
98
|
formatTimestamp
|
|
91
99
|
};
|
package/lib/routes/index.js
CHANGED
|
@@ -31,7 +31,8 @@ const mediaRouteDefinitions = {
|
|
|
31
31
|
const messageRouteDefinitions = {
|
|
32
32
|
'POST /send': 'sendMessageController',
|
|
33
33
|
'POST /send-bulk': 'sendBulkMessageController',
|
|
34
|
-
'POST /send-bulk-airtable': 'sendBulkMessageAirtableController'
|
|
34
|
+
'POST /send-bulk-airtable': 'sendBulkMessageAirtableController',
|
|
35
|
+
'POST /get-last': 'getLastInteractionController'
|
|
35
36
|
};
|
|
36
37
|
|
|
37
38
|
const templateRouteDefinitions = {
|
|
@@ -103,6 +104,7 @@ const builtInControllers = {
|
|
|
103
104
|
sendMessageController: messageController.sendMessageController,
|
|
104
105
|
sendBulkMessageController: messageController.sendBulkMessageController,
|
|
105
106
|
sendBulkMessageAirtableController: messageController.sendBulkMessageAirtableController,
|
|
107
|
+
getLastInteractionController: messageController.getLastInteractionController,
|
|
106
108
|
|
|
107
109
|
// Template controllers
|
|
108
110
|
createTemplate: templateController.createTemplate,
|
|
@@ -3,6 +3,7 @@ const { airtable } = require('../config/airtableConfig');
|
|
|
3
3
|
|
|
4
4
|
async function addRecord(baseID, tableName, fields) {
|
|
5
5
|
try {
|
|
6
|
+
if (!airtable) throw new Error('Airtable not configured. Set AIRTABLE_API_KEY');
|
|
6
7
|
const base = airtable.base(baseID);
|
|
7
8
|
const record = await base(tableName).create(fields);
|
|
8
9
|
console.log('Record added at', tableName);
|
|
@@ -17,6 +18,7 @@ async function addRecord(baseID, tableName, fields) {
|
|
|
17
18
|
async function getRecords(baseID, tableName) {
|
|
18
19
|
try {
|
|
19
20
|
const records = [];
|
|
21
|
+
if (!airtable) throw new Error('Airtable not configured. Set AIRTABLE_API_KEY');
|
|
20
22
|
const base = airtable.base(baseID);
|
|
21
23
|
await base(tableName).select({
|
|
22
24
|
maxRecords: 3
|
|
@@ -36,6 +38,7 @@ async function getRecords(baseID, tableName) {
|
|
|
36
38
|
async function getRecordByFilter(baseID, tableName, filter, view = 'Grid view') {
|
|
37
39
|
try {
|
|
38
40
|
const records = [];
|
|
41
|
+
if (!airtable) throw new Error('Airtable not configured. Set AIRTABLE_API_KEY');
|
|
39
42
|
const base = airtable.base(baseID);
|
|
40
43
|
await base(tableName).select({
|
|
41
44
|
filterByFormula: `${filter}`,
|
|
@@ -55,6 +58,7 @@ async function getRecordByFilter(baseID, tableName, filter, view = 'Grid view')
|
|
|
55
58
|
|
|
56
59
|
async function updateRecordByFilter(baseID, tableName, filter, updateFields) {
|
|
57
60
|
try {
|
|
61
|
+
if (!airtable) throw new Error('Airtable not configured. Set AIRTABLE_API_KEY');
|
|
58
62
|
const base = airtable.base(baseID);
|
|
59
63
|
const updatedRecords = [];
|
|
60
64
|
|
|
@@ -7,6 +7,7 @@ const configureLLMProvider = (provider) => {
|
|
|
7
7
|
|
|
8
8
|
let assistantConfig = null;
|
|
9
9
|
let assistantRegistry = {};
|
|
10
|
+
let customGetAssistantById = null;
|
|
10
11
|
|
|
11
12
|
const { Message, formatTimestamp } = require('../models/messageModel.js');
|
|
12
13
|
const { Thread } = require('../models/threadModel.js');
|
|
@@ -26,9 +27,20 @@ const registerAssistant = (assistantId, AssistantClass) => {
|
|
|
26
27
|
assistantRegistry[assistantId] = AssistantClass;
|
|
27
28
|
};
|
|
28
29
|
|
|
30
|
+
const overrideGetAssistantById = (resolverFn) => {
|
|
31
|
+
if (typeof resolverFn === 'function') {
|
|
32
|
+
customGetAssistantById = resolverFn;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
29
36
|
const getAssistantById = (assistant_id, thread) => {
|
|
37
|
+
if (customGetAssistantById) {
|
|
38
|
+
const inst = customGetAssistantById(assistant_id, thread);
|
|
39
|
+
if (inst) return inst;
|
|
40
|
+
}
|
|
41
|
+
|
|
30
42
|
if (!assistantConfig) {
|
|
31
|
-
|
|
43
|
+
assistantConfig = {};
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
const AssistantClass = assistantRegistry[assistant_id];
|
|
@@ -288,5 +300,6 @@ module.exports = {
|
|
|
288
300
|
switchAssistant,
|
|
289
301
|
configureAssistants,
|
|
290
302
|
registerAssistant,
|
|
291
|
-
configureLLMProvider
|
|
303
|
+
configureLLMProvider,
|
|
304
|
+
overrideGetAssistantById
|
|
292
305
|
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* Noop storage adapter - implements the storage interface but does nothing.
|
|
4
|
+
*/
|
|
5
|
+
class NoopStorage {
|
|
6
|
+
constructor(config = {}) {
|
|
7
|
+
this.config = config;
|
|
8
|
+
}
|
|
9
|
+
async connect() {}
|
|
10
|
+
async saveMessage() { return null; }
|
|
11
|
+
async saveInteractive() { return null; }
|
|
12
|
+
async getMessages() { return []; }
|
|
13
|
+
async getThread() { return null; }
|
|
14
|
+
async createThread() { return null; }
|
|
15
|
+
async updateThread() { return null; }
|
|
16
|
+
async disconnect() {}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = { NoopStorage };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
|
|
2
|
+
const { MongoStorage } = require('./MongoStorage');
|
|
3
|
+
const { NoopStorage } = require('./NoopStorage');
|
|
4
|
+
|
|
5
|
+
const _storages = new Map();
|
|
6
|
+
|
|
7
|
+
function registerStorage(name, StorageClassOrFactory) {
|
|
8
|
+
if (!name || !StorageClassOrFactory) throw new Error('registerStorage requires a name and class/factory');
|
|
9
|
+
_storages.set(String(name).toLowerCase(), StorageClassOrFactory);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getStorage(name) {
|
|
13
|
+
return _storages.get(String(name || '').toLowerCase());
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function createStorage(name, config) {
|
|
17
|
+
const Entry = getStorage(name);
|
|
18
|
+
if (!Entry) throw new Error(`Unsupported storage: ${name}`);
|
|
19
|
+
if (typeof Entry === 'function' && /^class\s/.test(Function.prototype.toString.call(Entry))) {
|
|
20
|
+
return new Entry(config || {});
|
|
21
|
+
}
|
|
22
|
+
// factory function
|
|
23
|
+
if (typeof Entry === 'function') return Entry(config || {});
|
|
24
|
+
throw new Error('Invalid storage registry entry');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Register built-ins
|
|
28
|
+
registerStorage('mongo', MongoStorage);
|
|
29
|
+
registerStorage('noop', NoopStorage);
|
|
30
|
+
|
|
31
|
+
module.exports = { registerStorage, getStorage, createStorage };
|
package/lib/utils/index.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
const logger = require('./logger');
|
|
1
|
+
const { DefaultLLMProvider } = require('./defaultLLMProvider');
|
|
2
|
+
const { MessageParser } = require('./messageParser');
|
|
3
|
+
const { logger } = require('./logger');
|
|
5
4
|
|
|
6
5
|
module.exports = {
|
|
7
|
-
AssistantManager,
|
|
8
6
|
DefaultLLMProvider,
|
|
9
7
|
MessageParser,
|
|
10
8
|
logger
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peopl-health/nexus",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Core messaging and assistant library for WhatsApp communication platforms",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"build": "tsc",
|
|
35
35
|
"dev": "tsc --watch",
|
|
36
36
|
"test": "jest",
|
|
37
|
+
"test:inband": "jest --runInBand",
|
|
37
38
|
"lint": "eslint lib/**/*.js",
|
|
38
39
|
"prepublishOnly": "npm test && npm run lint",
|
|
39
40
|
"version": "npm run prepublishOnly && git add -A lib",
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
const { Calendar_ID } = require('../config/airtableConfig.js');
|
|
2
|
-
|
|
3
|
-
const { ISO_DATE, parseStartTime, addDays } = require('../utils/dateUtils.js');
|
|
4
|
-
const { dateAndTimeFromStart } = require('../utils/dateUtils.js');
|
|
5
|
-
|
|
6
|
-
const { getRecordByFilter, updateRecordByFilter } = require('../services/airtableService.js');
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const getNextAppointmentForPatient = async (name, after = new Date()) => {
|
|
10
|
-
const filter = `AND(patient_name = "${name}", start_time >= '${ISO_DATE(after)}')`;
|
|
11
|
-
const rows = await getRecordByFilter(Calendar_ID, 'calendar_quimio', filter);
|
|
12
|
-
if (!rows?.length) return null;
|
|
13
|
-
return rows.sort((a, b) => new Date(a.start_time) - new Date(b.start_time))[0];
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const getAppointmentsBetween = async (start, end) => {
|
|
17
|
-
const filter = `AND(start_time >= '${ISO_DATE(start)}', start_time < '${ISO_DATE(end)}')`;
|
|
18
|
-
return getRecordByFilter(Calendar_ID, 'calendar_quimio', filter);
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const updateAppointmentById = async (recordId, data) => {
|
|
22
|
-
return updateRecordByFilter(
|
|
23
|
-
Calendar_ID,
|
|
24
|
-
'calendar_quimio',
|
|
25
|
-
`REGEX_MATCH({record_id}, '${recordId}')`,
|
|
26
|
-
data
|
|
27
|
-
);
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const buildAvailabilityWindow = async originalDate => {
|
|
31
|
-
const start = addDays(originalDate, 1);
|
|
32
|
-
const end = addDays(originalDate, 7);
|
|
33
|
-
|
|
34
|
-
const allSlots = await getAppointmentsBetween(start, addDays(end, 1));
|
|
35
|
-
const availableSlots = allSlots.filter(row => !row.patient);
|
|
36
|
-
|
|
37
|
-
const result = {};
|
|
38
|
-
availableSlots.forEach(row => {
|
|
39
|
-
const { date, time } = dateAndTimeFromStart(row.start_time);
|
|
40
|
-
if (!result[date]) result[date] = [];
|
|
41
|
-
result[date].push({ time, availableSpots: 1 });
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
return result;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const parseStart = parseStartTime;
|
|
48
|
-
|
|
49
|
-
module.exports = {
|
|
50
|
-
getNextAppointmentForPatient,
|
|
51
|
-
getAppointmentsBetween,
|
|
52
|
-
updateAppointmentById,
|
|
53
|
-
parseStart,
|
|
54
|
-
buildAvailabilityWindow
|
|
55
|
-
};
|