@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.
- package/CHANGELOG.md +55 -0
- package/LICENSE +21 -0
- package/MIGRATION_GUIDE.md +388 -0
- package/README.md +532 -0
- package/examples/.env.example +24 -0
- package/examples/assistants/BaseAssistant.js +242 -0
- package/examples/assistants/ExampleAssistant.js +111 -0
- package/examples/assistants/index.js +7 -0
- package/examples/basic-usage.js +109 -0
- package/examples/consumer-server.js +206 -0
- package/lib/adapters/BaileysProvider.js +180 -0
- package/lib/adapters/TwilioProvider.js +118 -0
- package/lib/adapters/index.js +7 -0
- package/lib/core/MessageProvider.js +71 -0
- package/lib/core/NexusMessaging.js +231 -0
- package/lib/core/index.js +7 -0
- package/lib/index.d.ts +276 -0
- package/lib/index.js +204 -0
- package/lib/models/index.js +9 -0
- package/lib/models/messageModel.js +91 -0
- package/lib/models/threadModel.js +20 -0
- package/lib/storage/MongoStorage.js +183 -0
- package/lib/storage/index.js +5 -0
- package/lib/utils/AssistantManager.js +218 -0
- package/lib/utils/DefaultLLMProvider.js +22 -0
- package/lib/utils/MessageParser.js +249 -0
- package/lib/utils/index.js +22 -0
- package/lib/utils/logger.js +22 -0
- package/lib/utils/mongoAuthConfig.js +139 -0
- package/lib/utils/twilioHelper.js +77 -0
- package/lib/utils/whatsappHelper.js +68 -0
- package/package.json +82 -0
|
@@ -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,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
|
+
};
|