@peopl-health/nexus 1.0.2

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.
@@ -0,0 +1,183 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ /**
4
+ * MongoDB storage interface for messages and interactions
5
+ */
6
+ class MongoStorage {
7
+ constructor(config) {
8
+ this.mongoUri = config.mongoUri;
9
+ this.dbName = config.dbName;
10
+ this.collections = config.collections || {
11
+ messages: 'messages',
12
+ interactions: 'interactions',
13
+ threads: 'threads'
14
+ };
15
+ this.schemas = this.createSchemas();
16
+ }
17
+
18
+ createSchemas() {
19
+ const messageSchema = new mongoose.Schema({
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
+
34
+ const interactionSchema = new mongoose.Schema({
35
+ messageId: String,
36
+ numero: String,
37
+ interactionType: String, // 'button', 'list', 'flow'
38
+ payload: mongoose.Schema.Types.Mixed,
39
+ timestamp: String,
40
+ createdAt: { type: Date, default: Date.now }
41
+ });
42
+
43
+ const threadSchema = new mongoose.Schema({
44
+ code: String,
45
+ assistantId: String,
46
+ threadId: String,
47
+ patientId: String,
48
+ runId: String,
49
+ nombre: String,
50
+ active: { type: Boolean, default: true },
51
+ stopped: { type: Boolean, default: false },
52
+ nextSid: [String],
53
+ createdAt: { type: Date, default: Date.now }
54
+ });
55
+
56
+ return {
57
+ Message: mongoose.model('Message', messageSchema),
58
+ Interaction: mongoose.model('Interaction', interactionSchema),
59
+ Thread: mongoose.model('Thread', threadSchema)
60
+ };
61
+ }
62
+
63
+ async connect() {
64
+ try {
65
+ await mongoose.connect(this.mongoUri, {
66
+ useNewUrlParser: true,
67
+ useUnifiedTopology: true
68
+ });
69
+ console.log('MongoDB connected successfully');
70
+ } catch (error) {
71
+ throw new Error(`MongoDB connection failed: ${error.message}`);
72
+ }
73
+ }
74
+
75
+ async saveMessage(messageData) {
76
+ try {
77
+ const message = new this.schemas.Message({
78
+ messageId: messageData.messageId,
79
+ numero: messageData.to || messageData.from,
80
+ body: messageData.message || messageData.body,
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;
93
+ } catch (error) {
94
+ console.error('Error saving message:', error);
95
+ throw error;
96
+ }
97
+ }
98
+
99
+ async saveInteractive(interactionData) {
100
+ try {
101
+ const interaction = new this.schemas.Interaction({
102
+ messageId: interactionData.messageId,
103
+ numero: interactionData.from,
104
+ interactionType: interactionData.type, // 'button', 'list', 'flow'
105
+ payload: interactionData.payload,
106
+ timestamp: this.formatTimestamp(interactionData.timestamp)
107
+ });
108
+
109
+ await interaction.save();
110
+ return interaction;
111
+ } catch (error) {
112
+ console.error('Error saving interaction:', error);
113
+ throw error;
114
+ }
115
+ }
116
+
117
+ async getMessages(numero, limit = 50) {
118
+ try {
119
+ return await this.schemas.Message
120
+ .find({ numero })
121
+ .sort({ createdAt: -1 })
122
+ .limit(limit);
123
+ } catch (error) {
124
+ console.error('Error getting messages:', error);
125
+ throw error;
126
+ }
127
+ }
128
+
129
+ async getThread(code) {
130
+ try {
131
+ return await this.schemas.Thread.findOne({ code, active: true });
132
+ } catch (error) {
133
+ console.error('Error getting thread:', error);
134
+ throw error;
135
+ }
136
+ }
137
+
138
+ async createThread(threadData) {
139
+ try {
140
+ const thread = new this.schemas.Thread(threadData);
141
+ await thread.save();
142
+ return thread;
143
+ } catch (error) {
144
+ console.error('Error creating thread:', error);
145
+ throw error;
146
+ }
147
+ }
148
+
149
+ async updateThread(code, updateData) {
150
+ try {
151
+ return await this.schemas.Thread.findOneAndUpdate(
152
+ { code },
153
+ { $set: updateData },
154
+ { new: true }
155
+ );
156
+ } catch (error) {
157
+ console.error('Error updating thread:', error);
158
+ throw error;
159
+ }
160
+ }
161
+
162
+ formatTimestamp(timestamp) {
163
+ if (!timestamp) timestamp = new Date();
164
+ const date = new Date(timestamp);
165
+ const mexicoTime = new Date(date.getTime() - (6 * 60 * 60 * 1000));
166
+
167
+ return mexicoTime.getUTCFullYear() + '-' +
168
+ String(mexicoTime.getUTCMonth() + 1).padStart(2, '0') + '-' +
169
+ String(mexicoTime.getUTCDate()).padStart(2, '0') + ' ' +
170
+ String(mexicoTime.getUTCHours()).padStart(2, '0') + ':' +
171
+ String(mexicoTime.getUTCMinutes()).padStart(2, '0') + ':' +
172
+ String(mexicoTime.getUTCSeconds()).padStart(2, '0');
173
+ }
174
+
175
+ async disconnect() {
176
+ await mongoose.disconnect();
177
+ if (this.client) {
178
+ await this.client.close();
179
+ }
180
+ }
181
+ }
182
+
183
+ module.exports = { MongoStorage };
@@ -0,0 +1,5 @@
1
+ const { MongoStorage } = require('./MongoStorage');
2
+
3
+ module.exports = {
4
+ MongoStorage
5
+ };
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Configurable assistant manager for handling AI interactions
3
+ */
4
+ class AssistantManager {
5
+ constructor(config = {}) {
6
+ this.config = config;
7
+ this.assistants = new Map();
8
+ this.llmClient = null;
9
+ this.handlers = {
10
+ onRequiresAction: null,
11
+ onCompleted: null,
12
+ onFailed: null
13
+ };
14
+ }
15
+
16
+ /**
17
+ * Initialize with LLM client (OpenAI, etc.)
18
+ * @param {Object} llmClient - LLM client instance
19
+ */
20
+ setLLMClient(llmClient) {
21
+ this.llmClient = llmClient;
22
+ }
23
+
24
+ /**
25
+ * Register assistant configurations
26
+ * @param {Object} assistantConfigs - Map of assistant IDs to configurations
27
+ */
28
+ registerAssistants(assistantConfigs) {
29
+ Object.entries(assistantConfigs).forEach(([key, config]) => {
30
+ this.assistants.set(key, config);
31
+ });
32
+ }
33
+
34
+ /**
35
+ * Set event handlers for assistant interactions
36
+ * @param {Object} handlers - Handler functions
37
+ */
38
+ setHandlers(handlers) {
39
+ this.handlers = { ...this.handlers, ...handlers };
40
+ }
41
+
42
+ /**
43
+ * Create a new thread for a conversation
44
+ * @param {string} code - User/conversation identifier
45
+ * @param {string} assistantId - Assistant ID to use
46
+ * @param {Array} initialMessages - Initial messages for context
47
+ */
48
+ async createThread(code, assistantId, initialMessages = []) {
49
+ if (!this.llmClient) {
50
+ throw new Error('LLM client not configured');
51
+ }
52
+
53
+ try {
54
+ const thread = await this.llmClient.beta.threads.create();
55
+
56
+ // Add initial messages if provided
57
+ for (const message of initialMessages) {
58
+ await this.llmClient.beta.threads.messages.create(
59
+ thread.id,
60
+ { role: 'assistant', content: message }
61
+ );
62
+ }
63
+
64
+ return {
65
+ code,
66
+ assistantId,
67
+ threadId: thread.id,
68
+ active: true,
69
+ createdAt: new Date()
70
+ };
71
+ } catch (error) {
72
+ throw new Error(`Failed to create thread: ${error.message}`);
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Send message to assistant and get response
78
+ * @param {Object} threadData - Thread information
79
+ * @param {string} message - User message
80
+ * @param {Object} runOptions - Additional run options
81
+ */
82
+ async sendMessage(threadData, message, runOptions = {}) {
83
+ if (!this.llmClient) {
84
+ throw new Error('LLM client not configured');
85
+ }
86
+
87
+ try {
88
+ // Add user message to thread
89
+ await this.llmClient.beta.threads.messages.create(
90
+ threadData.threadId,
91
+ { role: 'user', content: message }
92
+ );
93
+
94
+ // Create run
95
+ const run = await this.llmClient.beta.threads.runs.create(
96
+ threadData.threadId,
97
+ {
98
+ assistant_id: threadData.assistantId,
99
+ ...runOptions
100
+ }
101
+ );
102
+
103
+ // Wait for completion and handle actions
104
+ const result = await this.waitForCompletion(threadData.threadId, run.id);
105
+
106
+ if (result.status === 'completed') {
107
+ const messages = await this.llmClient.beta.threads.messages.list(
108
+ threadData.threadId,
109
+ { run_id: run.id }
110
+ );
111
+ return messages.data[0]?.content[0]?.text?.value || '';
112
+ } else if (result.status === 'requires_action') {
113
+ if (this.handlers.onRequiresAction) {
114
+ return await this.handlers.onRequiresAction(result, threadData);
115
+ }
116
+ }
117
+
118
+ return null;
119
+ } catch (error) {
120
+ throw new Error(`Assistant interaction failed: ${error.message}`);
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Wait for run completion and handle different statuses
126
+ * @param {string} threadId - Thread ID
127
+ * @param {string} runId - Run ID
128
+ */
129
+ async waitForCompletion(threadId, runId) {
130
+ const maxAttempts = 30;
131
+ let attempts = 0;
132
+
133
+ while (attempts < maxAttempts) {
134
+ const run = await this.llmClient.beta.threads.runs.retrieve(threadId, runId);
135
+
136
+ if (run.status === 'completed') {
137
+ return { status: 'completed', run };
138
+ } else if (run.status === 'requires_action') {
139
+ return { status: 'requires_action', run };
140
+ } else if (run.status === 'failed' || run.status === 'cancelled' || run.status === 'expired') {
141
+ if (this.handlers.onFailed) {
142
+ await this.handlers.onFailed(run);
143
+ }
144
+ return { status: 'failed', run };
145
+ }
146
+
147
+ // Wait before next check
148
+ await new Promise(resolve => setTimeout(resolve, 2000));
149
+ attempts++;
150
+ }
151
+
152
+ throw new Error('Assistant run timeout');
153
+ }
154
+
155
+ /**
156
+ * Submit tool outputs for function calls
157
+ * @param {string} threadId - Thread ID
158
+ * @param {string} runId - Run ID
159
+ * @param {Array} toolOutputs - Tool outputs array
160
+ */
161
+ async submitToolOutputs(threadId, runId, toolOutputs) {
162
+ if (!this.llmClient) {
163
+ throw new Error('LLM client not configured');
164
+ }
165
+
166
+ try {
167
+ const run = await this.llmClient.beta.threads.runs.submitToolOutputs(
168
+ threadId,
169
+ runId,
170
+ { tool_outputs: toolOutputs }
171
+ );
172
+
173
+ return await this.waitForCompletion(threadId, run.id);
174
+ } catch (error) {
175
+ throw new Error(`Failed to submit tool outputs: ${error.message}`);
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Add instruction to existing thread
181
+ * @param {Object} threadData - Thread information
182
+ * @param {string} instruction - Additional instruction
183
+ */
184
+ async addInstruction(threadData, instruction) {
185
+ if (!this.llmClient) {
186
+ throw new Error('LLM client not configured');
187
+ }
188
+
189
+ try {
190
+ const run = await this.llmClient.beta.threads.runs.create(
191
+ threadData.threadId,
192
+ {
193
+ assistant_id: threadData.assistantId,
194
+ additional_instructions: instruction,
195
+ additional_messages: [
196
+ { role: 'user', content: instruction }
197
+ ]
198
+ }
199
+ );
200
+
201
+ const result = await this.waitForCompletion(threadData.threadId, run.id);
202
+
203
+ if (result.status === 'completed') {
204
+ const messages = await this.llmClient.beta.threads.messages.list(
205
+ threadData.threadId,
206
+ { run_id: run.id }
207
+ );
208
+ return messages.data[0]?.content[0]?.text?.value || '';
209
+ }
210
+
211
+ return null;
212
+ } catch (error) {
213
+ throw new Error(`Failed to add instruction: ${error.message}`);
214
+ }
215
+ }
216
+ }
217
+
218
+ module.exports = { AssistantManager };
@@ -0,0 +1,22 @@
1
+ const { OpenAI } = require('openai');
2
+
3
+ /**
4
+ * Default LLM Provider using OpenAI
5
+ */
6
+ class DefaultLLMProvider {
7
+ constructor(config = {}) {
8
+ const apiKey = config.apiKey || process.env.OPENAI_API_KEY;
9
+
10
+ if (!apiKey) {
11
+ throw new Error('OpenAI API key is required. Set OPENAI_API_KEY environment variable or pass apiKey in config.');
12
+ }
13
+
14
+ this.client = new OpenAI({ apiKey });
15
+ }
16
+
17
+ getClient() {
18
+ return this.client;
19
+ }
20
+ }
21
+
22
+ module.exports = { DefaultLLMProvider };
@@ -0,0 +1,249 @@
1
+ /**
2
+ * Configurable message parser for different message types
3
+ */
4
+ class MessageParser {
5
+ constructor(config = {}) {
6
+ this.config = config;
7
+ this.commandPrefixes = config.commandPrefixes || ['/', '!'];
8
+ this.keywords = config.keywords || [];
9
+ this.flowTriggers = config.flowTriggers || [];
10
+ }
11
+
12
+ /**
13
+ * Parse incoming message and determine its type
14
+ * @param {Object} rawMessage - Raw message from provider
15
+ * @returns {Object} Parsed message with type and data
16
+ */
17
+ parseMessage(rawMessage) {
18
+ const messageData = {
19
+ id: rawMessage.id || rawMessage.key?.id,
20
+ from: this.extractSender(rawMessage),
21
+ timestamp: rawMessage.timestamp || Date.now(),
22
+ raw: rawMessage
23
+ };
24
+
25
+ // Check for interactive messages (buttons, lists, flows)
26
+ if (this.isInteractiveMessage(rawMessage)) {
27
+ return {
28
+ ...messageData,
29
+ type: 'interactive',
30
+ interactive: this.parseInteractive(rawMessage)
31
+ };
32
+ }
33
+
34
+ // Check for media messages
35
+ if (this.isMediaMessage(rawMessage)) {
36
+ return {
37
+ ...messageData,
38
+ type: 'media',
39
+ media: this.parseMedia(rawMessage)
40
+ };
41
+ }
42
+
43
+ // Parse text content
44
+ const textContent = this.extractTextContent(rawMessage);
45
+ if (!textContent) {
46
+ return { ...messageData, type: 'unknown' };
47
+ }
48
+
49
+ messageData.message = textContent;
50
+
51
+ // Check for commands
52
+ if (this.isCommand(textContent)) {
53
+ return {
54
+ ...messageData,
55
+ type: 'command',
56
+ command: this.parseCommand(textContent)
57
+ };
58
+ }
59
+
60
+ // Check for keywords
61
+ const keyword = this.findKeyword(textContent);
62
+ if (keyword) {
63
+ return {
64
+ ...messageData,
65
+ type: 'keyword',
66
+ keyword: keyword
67
+ };
68
+ }
69
+
70
+ // Check for flow triggers
71
+ const flowTrigger = this.findFlowTrigger(textContent);
72
+ if (flowTrigger) {
73
+ return {
74
+ ...messageData,
75
+ type: 'flow',
76
+ flow: flowTrigger
77
+ };
78
+ }
79
+
80
+ // Default to regular message
81
+ return {
82
+ ...messageData,
83
+ type: 'message'
84
+ };
85
+ }
86
+
87
+ extractSender(rawMessage) {
88
+ // Twilio format
89
+ if (rawMessage.From) {
90
+ return rawMessage.From;
91
+ }
92
+
93
+ // Baileys format
94
+ if (rawMessage.key?.remoteJid) {
95
+ return rawMessage.key.remoteJid;
96
+ }
97
+
98
+ return null;
99
+ }
100
+
101
+ extractTextContent(rawMessage) {
102
+ // Twilio format
103
+ if (rawMessage.Body) {
104
+ return rawMessage.Body;
105
+ }
106
+
107
+ // Baileys format
108
+ if (rawMessage.message?.conversation) {
109
+ return rawMessage.message.conversation;
110
+ }
111
+
112
+ if (rawMessage.message?.extendedTextMessage?.text) {
113
+ return rawMessage.message.extendedTextMessage.text;
114
+ }
115
+
116
+ return null;
117
+ }
118
+
119
+ isInteractiveMessage(rawMessage) {
120
+ // Twilio interactive messages
121
+ return !!(rawMessage.ButtonPayload || rawMessage.ListId || rawMessage.FlowData || rawMessage.InteractiveData);
122
+ }
123
+
124
+ parseInteractive(rawMessage) {
125
+ if (rawMessage.ButtonPayload) {
126
+ return {
127
+ type: 'button',
128
+ payload: rawMessage.ButtonPayload,
129
+ title: rawMessage.ButtonText
130
+ };
131
+ }
132
+
133
+ if (rawMessage.ListId) {
134
+ return {
135
+ type: 'list',
136
+ id: rawMessage.ListId,
137
+ title: rawMessage.ListTitle,
138
+ description: rawMessage.ListDescription
139
+ };
140
+ }
141
+
142
+ if (rawMessage.FlowData || rawMessage.InteractiveData) {
143
+ return {
144
+ type: 'flow',
145
+ data: rawMessage.FlowData || rawMessage.InteractiveData
146
+ };
147
+ }
148
+
149
+ return null;
150
+ }
151
+
152
+ isMediaMessage(rawMessage) {
153
+ // Twilio format
154
+ if (rawMessage.NumMedia && parseInt(rawMessage.NumMedia) > 0) {
155
+ return true;
156
+ }
157
+
158
+ // Baileys format
159
+ if (rawMessage.message) {
160
+ const messageTypes = Object.keys(rawMessage.message);
161
+ return messageTypes.some(type =>
162
+ type.match(/(image|audio|video|document)/)
163
+ );
164
+ }
165
+
166
+ return false;
167
+ }
168
+
169
+ parseMedia(rawMessage) {
170
+ // Twilio format
171
+ if (rawMessage.MediaUrl0) {
172
+ return {
173
+ url: rawMessage.MediaUrl0,
174
+ contentType: rawMessage.MediaContentType0,
175
+ filename: rawMessage.MediaFilename0
176
+ };
177
+ }
178
+
179
+ // Baileys format - would need additional processing
180
+ if (rawMessage.message) {
181
+ const messageTypes = Object.keys(rawMessage.message);
182
+ const mediaType = messageTypes.find(type =>
183
+ type.match(/(image|audio|video|document)/)
184
+ );
185
+
186
+ if (mediaType) {
187
+ return {
188
+ type: mediaType,
189
+ data: rawMessage.message[mediaType]
190
+ };
191
+ }
192
+ }
193
+
194
+ return null;
195
+ }
196
+
197
+ isCommand(text) {
198
+ return this.commandPrefixes.some(prefix => text.startsWith(prefix));
199
+ }
200
+
201
+ parseCommand(text) {
202
+ const prefix = this.commandPrefixes.find(p => text.startsWith(p));
203
+ const commandText = text.substring(prefix.length).trim();
204
+ const parts = commandText.split(' ');
205
+
206
+ return {
207
+ prefix,
208
+ command: parts[0],
209
+ args: parts.slice(1)
210
+ };
211
+ }
212
+
213
+ findKeyword(text) {
214
+ const lowerText = text.toLowerCase();
215
+ return this.keywords.find(keyword => {
216
+ if (typeof keyword === 'string') {
217
+ return lowerText.includes(keyword.toLowerCase());
218
+ } else if (keyword.pattern) {
219
+ return new RegExp(keyword.pattern, 'i').test(text);
220
+ }
221
+ return false;
222
+ });
223
+ }
224
+
225
+ findFlowTrigger(text) {
226
+ const lowerText = text.toLowerCase();
227
+ return this.flowTriggers.find(trigger => {
228
+ if (typeof trigger === 'string') {
229
+ return lowerText.includes(trigger.toLowerCase());
230
+ } else if (trigger.pattern) {
231
+ return new RegExp(trigger.pattern, 'i').test(text);
232
+ }
233
+ return false;
234
+ });
235
+ }
236
+
237
+ /**
238
+ * Update parser configuration
239
+ * @param {Object} newConfig - New configuration
240
+ */
241
+ updateConfig(newConfig) {
242
+ this.config = { ...this.config, ...newConfig };
243
+ this.commandPrefixes = this.config.commandPrefixes || this.commandPrefixes;
244
+ this.keywords = this.config.keywords || this.keywords;
245
+ this.flowTriggers = this.config.flowTriggers || this.flowTriggers;
246
+ }
247
+ }
248
+
249
+ module.exports = { MessageParser };
@@ -0,0 +1,22 @@
1
+ const { MessageParser } = require('./MessageParser');
2
+ const { DefaultLLMProvider } = require('./DefaultLLMProvider');
3
+ const { logger, createLogger } = require('./logger');
4
+ const { delay, formatCode, calculateDelay } = require('./whatsappHelper');
5
+ const { useMongoDBAuthState } = require('./mongoAuthConfig');
6
+ const { convertTwilioToInternalFormat, downloadMediaFromTwilio, getMediaTypeFromContentType, extractTitle, ensureWhatsAppFormat } = require('./twilioHelper');
7
+
8
+ module.exports = {
9
+ MessageParser,
10
+ DefaultLLMProvider,
11
+ logger,
12
+ createLogger,
13
+ delay,
14
+ formatCode,
15
+ calculateDelay,
16
+ useMongoDBAuthState,
17
+ convertTwilioToInternalFormat,
18
+ downloadMediaFromTwilio,
19
+ getMediaTypeFromContentType,
20
+ extractTitle,
21
+ ensureWhatsAppFormat
22
+ };