@peopl-health/nexus 1.5.8 → 1.5.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +0 -0
- package/LICENSE +0 -0
- package/MIGRATION_GUIDE.md +0 -0
- package/README.md +0 -0
- package/examples/.env.example +0 -0
- package/examples/assistants/BaseAssistant.js +1 -242
- package/examples/assistants/DoctorScheduleAssistant.js +83 -0
- package/examples/assistants/ExampleAssistant.js +0 -0
- package/examples/assistants/index.js +3 -1
- package/examples/basic-usage.js +0 -0
- package/examples/consumer-server.js +0 -0
- package/lib/adapters/BaileysProvider.js +0 -0
- package/lib/adapters/TwilioProvider.js +0 -0
- package/lib/adapters/index.js +0 -0
- package/lib/adapters/registry.js +0 -0
- package/lib/assistants/BaseAssistant.js +294 -0
- package/lib/assistants/index.js +5 -0
- package/lib/config/airtableConfig.js +0 -0
- package/lib/config/awsConfig.js +0 -0
- package/lib/config/configLoader.js +0 -0
- package/lib/config/llmConfig.js +0 -0
- package/lib/config/mongoAuthConfig.js +0 -0
- package/lib/config/runtimeConfig.js +0 -0
- package/lib/controllers/assistantController.js +0 -0
- package/lib/controllers/conversationController.js +0 -0
- package/lib/controllers/mediaController.js +0 -0
- package/lib/controllers/messageController.js +0 -0
- package/lib/controllers/templateController.js +0 -0
- package/lib/controllers/templateFlowController.js +0 -0
- package/lib/controllers/uploadController.js +0 -0
- package/lib/core/MessageProvider.js +0 -0
- package/lib/core/NexusMessaging.js +0 -0
- package/lib/core/index.js +0 -0
- package/lib/helpers/assistantHelper.js +0 -0
- package/lib/helpers/baileysHelper.js +0 -0
- package/lib/helpers/filesHelper.js +0 -0
- package/lib/helpers/llmsHelper.js +0 -0
- package/lib/helpers/mediaHelper.js +0 -0
- package/lib/helpers/mongoHelper.js +0 -0
- package/lib/helpers/qrHelper.js +0 -0
- package/lib/helpers/twilioHelper.js +0 -0
- package/lib/helpers/twilioMediaProcessor.js +0 -0
- package/lib/helpers/whatsappHelper.js +0 -0
- package/lib/index.d.ts +51 -0
- package/lib/index.js +6 -0
- package/lib/interactive/index.js +0 -0
- package/lib/interactive/registry.js +0 -0
- package/lib/interactive/twilioMapper.js +0 -0
- package/lib/models/agendaMessageModel.js +0 -0
- package/lib/models/index.js +0 -0
- package/lib/models/messageModel.js +0 -0
- package/lib/models/templateModel.js +0 -0
- package/lib/models/threadModel.js +0 -0
- package/lib/routes/index.js +0 -0
- package/lib/services/airtableService.js +0 -0
- package/lib/services/assistantService.js +66 -4
- package/lib/services/conversationService.js +0 -0
- package/lib/services/twilioService.js +0 -0
- package/lib/storage/MongoStorage.js +0 -0
- package/lib/storage/NoopStorage.js +0 -0
- package/lib/storage/index.js +0 -0
- package/lib/storage/registry.js +0 -0
- package/lib/templates/predefinedTemplates.js +0 -0
- package/lib/templates/templateStructure.js +0 -0
- package/lib/utils/dateUtils.js +0 -0
- package/lib/utils/defaultLLMProvider.js +0 -0
- package/lib/utils/errorHandler.js +0 -0
- package/lib/utils/index.js +0 -0
- package/lib/utils/logger.js +0 -0
- package/lib/utils/mediaValidator.js +0 -0
- package/lib/utils/messageParser.js +0 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
File without changes
|
package/LICENSE
CHANGED
|
File without changes
|
package/MIGRATION_GUIDE.md
CHANGED
|
File without changes
|
package/README.md
CHANGED
|
File without changes
|
package/examples/.env.example
CHANGED
|
File without changes
|
|
@@ -1,242 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Base Assistant class for consumer server extensions
|
|
3
|
-
* Provides common functionality for AI assistant implementations
|
|
4
|
-
*/
|
|
5
|
-
class BaseAssistant {
|
|
6
|
-
constructor(config = {}) {
|
|
7
|
-
this.assistantId = config.assistantId;
|
|
8
|
-
this.llmClient = config.llmClient;
|
|
9
|
-
this.storage = config.storage;
|
|
10
|
-
this.nexus = config.nexus;
|
|
11
|
-
this.functions = new Map();
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Register a function that can be called by the assistant
|
|
16
|
-
* @param {string} name - Function name
|
|
17
|
-
* @param {Function} handler - Function handler
|
|
18
|
-
* @param {Object} schema - Function schema for LLM
|
|
19
|
-
*/
|
|
20
|
-
registerFunction(name, handler, schema) {
|
|
21
|
-
this.functions.set(name, { handler, schema });
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Get all registered function schemas for LLM
|
|
26
|
-
* @returns {Array} Function schemas
|
|
27
|
-
*/
|
|
28
|
-
getFunctionSchemas() {
|
|
29
|
-
return Array.from(this.functions.values()).map(f => f.schema);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Execute a function call from the assistant
|
|
34
|
-
* @param {string} name - Function name
|
|
35
|
-
* @param {Object} args - Function arguments
|
|
36
|
-
* @returns {Promise<any>} Function result
|
|
37
|
-
*/
|
|
38
|
-
async executeFunction(name, args) {
|
|
39
|
-
const func = this.functions.get(name);
|
|
40
|
-
if (!func) {
|
|
41
|
-
throw new Error(`Unknown function: ${name}`);
|
|
42
|
-
}
|
|
43
|
-
return await func.handler(args);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Create a new thread for this assistant
|
|
48
|
-
* @param {string} userId - User identifier
|
|
49
|
-
* @param {Array} initialMessages - Initial messages
|
|
50
|
-
* @returns {Promise<Object>} Thread data
|
|
51
|
-
*/
|
|
52
|
-
async createThread(userId, initialMessages = []) {
|
|
53
|
-
if (!this.llmClient) {
|
|
54
|
-
throw new Error('LLM client not configured');
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const thread = await this.llmClient.beta.threads.create();
|
|
58
|
-
|
|
59
|
-
// Add initial messages if provided
|
|
60
|
-
for (const message of initialMessages) {
|
|
61
|
-
await this.llmClient.beta.threads.messages.create(
|
|
62
|
-
thread.id,
|
|
63
|
-
{ role: "assistant", content: message }
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const threadData = {
|
|
68
|
-
code: userId,
|
|
69
|
-
assistantId: this.assistantId,
|
|
70
|
-
threadId: thread.id,
|
|
71
|
-
active: true,
|
|
72
|
-
createdAt: new Date()
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
// Save to storage if available
|
|
76
|
-
if (this.storage) {
|
|
77
|
-
await this.storage.createThread(threadData);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return threadData;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Send message to assistant and handle response
|
|
85
|
-
* @param {string} userId - User identifier
|
|
86
|
-
* @param {string} message - User message
|
|
87
|
-
* @param {Object} options - Additional options
|
|
88
|
-
* @returns {Promise<string>} Assistant response
|
|
89
|
-
*/
|
|
90
|
-
async sendMessage(userId, message, options = {}) {
|
|
91
|
-
if (!this.llmClient) {
|
|
92
|
-
throw new Error('LLM client not configured');
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Get or create thread
|
|
96
|
-
let threadData = null;
|
|
97
|
-
if (this.storage) {
|
|
98
|
-
threadData = await this.storage.getThread(userId);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (!threadData) {
|
|
102
|
-
threadData = await this.createThread(userId);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Add user message to thread
|
|
106
|
-
await this.llmClient.beta.threads.messages.create(
|
|
107
|
-
threadData.threadId,
|
|
108
|
-
{ role: "user", content: message }
|
|
109
|
-
);
|
|
110
|
-
|
|
111
|
-
// Create run with function schemas if available
|
|
112
|
-
const runConfig = {
|
|
113
|
-
assistant_id: this.assistantId,
|
|
114
|
-
...options
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
const functionSchemas = this.getFunctionSchemas();
|
|
118
|
-
if (functionSchemas.length > 0) {
|
|
119
|
-
runConfig.tools = functionSchemas;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const run = await this.llmClient.beta.threads.runs.create(
|
|
123
|
-
threadData.threadId,
|
|
124
|
-
runConfig
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
// Wait for completion and handle function calls
|
|
128
|
-
const result = await this.waitForCompletion(threadData.threadId, run.id);
|
|
129
|
-
|
|
130
|
-
if (result.status === 'completed') {
|
|
131
|
-
const messages = await this.llmClient.beta.threads.messages.list(
|
|
132
|
-
threadData.threadId,
|
|
133
|
-
{ run_id: run.id }
|
|
134
|
-
);
|
|
135
|
-
return messages.data[0]?.content[0]?.text?.value || "";
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Wait for run completion and handle function calls
|
|
143
|
-
* @param {string} threadId - Thread ID
|
|
144
|
-
* @param {string} runId - Run ID
|
|
145
|
-
* @returns {Promise<Object>} Run result
|
|
146
|
-
*/
|
|
147
|
-
async waitForCompletion(threadId, runId) {
|
|
148
|
-
const maxAttempts = 30;
|
|
149
|
-
let attempts = 0;
|
|
150
|
-
|
|
151
|
-
while (attempts < maxAttempts) {
|
|
152
|
-
const run = await this.llmClient.beta.threads.runs.retrieve(threadId, runId);
|
|
153
|
-
|
|
154
|
-
if (run.status === 'completed') {
|
|
155
|
-
return { status: 'completed', run };
|
|
156
|
-
} else if (run.status === 'requires_action') {
|
|
157
|
-
// Handle function calls
|
|
158
|
-
const toolOutputs = await this.handleRequiredActions(run);
|
|
159
|
-
|
|
160
|
-
if (toolOutputs.length > 0) {
|
|
161
|
-
await this.llmClient.beta.threads.runs.submitToolOutputs(
|
|
162
|
-
threadId,
|
|
163
|
-
runId,
|
|
164
|
-
{ tool_outputs: toolOutputs }
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
// Continue waiting for completion
|
|
168
|
-
attempts = 0; // Reset attempts after submitting outputs
|
|
169
|
-
await this.delay(2000);
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
} else if (run.status === 'failed' || run.status === 'cancelled' || run.status === 'expired') {
|
|
173
|
-
return { status: 'failed', run };
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
await this.delay(2000);
|
|
177
|
-
attempts++;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
throw new Error('Assistant run timeout');
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Handle required actions (function calls)
|
|
185
|
-
* @param {Object} run - Run object with required actions
|
|
186
|
-
* @returns {Promise<Array>} Tool outputs
|
|
187
|
-
*/
|
|
188
|
-
async handleRequiredActions(run) {
|
|
189
|
-
const toolCalls = run.required_action?.submit_tool_outputs?.tool_calls || [];
|
|
190
|
-
const toolOutputs = [];
|
|
191
|
-
|
|
192
|
-
for (const toolCall of toolCalls) {
|
|
193
|
-
try {
|
|
194
|
-
const functionName = toolCall.function.name;
|
|
195
|
-
const args = JSON.parse(toolCall.function.arguments);
|
|
196
|
-
|
|
197
|
-
const result = await this.executeFunction(functionName, args);
|
|
198
|
-
|
|
199
|
-
toolOutputs.push({
|
|
200
|
-
tool_call_id: toolCall.id,
|
|
201
|
-
output: JSON.stringify(result)
|
|
202
|
-
});
|
|
203
|
-
} catch (error) {
|
|
204
|
-
console.error(`Error executing function ${toolCall.function.name}:`, error);
|
|
205
|
-
toolOutputs.push({
|
|
206
|
-
tool_call_id: toolCall.id,
|
|
207
|
-
output: JSON.stringify({ error: error.message })
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return toolOutputs;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Send a message through Nexus
|
|
217
|
-
* @param {string} to - Recipient
|
|
218
|
-
* @param {string} message - Message text
|
|
219
|
-
* @param {Object} options - Additional options
|
|
220
|
-
*/
|
|
221
|
-
async sendNexusMessage(to, message, options = {}) {
|
|
222
|
-
if (!this.nexus) {
|
|
223
|
-
throw new Error('Nexus instance not provided');
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return await this.nexus.sendMessage({
|
|
227
|
-
to,
|
|
228
|
-
message,
|
|
229
|
-
...options
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Utility delay function
|
|
235
|
-
* @param {number} ms - Milliseconds to delay
|
|
236
|
-
*/
|
|
237
|
-
delay(ms) {
|
|
238
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
module.exports = { BaseAssistant };
|
|
1
|
+
module.exports = require('../../lib/assistants/BaseAssistant');
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const { BaseAssistant } = require('../../lib/assistants/BaseAssistant');
|
|
2
|
+
|
|
3
|
+
class DoctorScheduleAssistant extends BaseAssistant {
|
|
4
|
+
constructor(thread) {
|
|
5
|
+
super({ assistantId: 'doctor_schedule', thread });
|
|
6
|
+
|
|
7
|
+
this.registerTool({
|
|
8
|
+
name: 'storePatientAppointment',
|
|
9
|
+
description: 'Stores a requested appointment so that an operator can follow up with the patient.',
|
|
10
|
+
parameters: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
required: ['contact_number', 'appointment_type'],
|
|
13
|
+
properties: {
|
|
14
|
+
contact_number: {
|
|
15
|
+
type: 'string',
|
|
16
|
+
description: 'WhatsApp phone number of the patient in international format (e.g. +521234567890).'
|
|
17
|
+
},
|
|
18
|
+
appointment_type: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
description: 'Type of medical appointment requested by the patient.'
|
|
21
|
+
},
|
|
22
|
+
patient_name: {
|
|
23
|
+
type: 'string',
|
|
24
|
+
description: 'Full name of the patient if provided.'
|
|
25
|
+
},
|
|
26
|
+
preferred_date: {
|
|
27
|
+
type: 'string',
|
|
28
|
+
description: 'Preferred appointment date in ISO format (YYYY-MM-DD).'
|
|
29
|
+
},
|
|
30
|
+
preferred_time: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
description: 'Preferred appointment time in HH:mm format.'
|
|
33
|
+
},
|
|
34
|
+
notes: {
|
|
35
|
+
type: 'string',
|
|
36
|
+
description: 'Any extra notes supplied by the patient.'
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
handler: async (payload = {}) => {
|
|
41
|
+
console.log('[DoctorScheduleAssistant] storePatientAppointment', payload);
|
|
42
|
+
|
|
43
|
+
if (!payload.contact_number || !payload.appointment_type) {
|
|
44
|
+
return JSON.stringify({
|
|
45
|
+
success: false,
|
|
46
|
+
message: 'Falta el número de contacto o el tipo de cita. Pide esa información al paciente.'
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const normalizedNumber = String(payload.contact_number).replace(/[^0-9+]/g, '');
|
|
51
|
+
|
|
52
|
+
const appointment = {
|
|
53
|
+
contact_number: normalizedNumber,
|
|
54
|
+
appointment_type: payload.appointment_type,
|
|
55
|
+
patient_name: payload.patient_name || null,
|
|
56
|
+
preferred_date: payload.preferred_date || null,
|
|
57
|
+
preferred_time: payload.preferred_time || null,
|
|
58
|
+
notes: payload.notes || null
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
console.log('[DoctorScheduleAssistant] Appointment captured', appointment);
|
|
62
|
+
|
|
63
|
+
return JSON.stringify({ success: true, appointment });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async create(code, curRow) {
|
|
69
|
+
await super.create(code, curRow);
|
|
70
|
+
|
|
71
|
+
const summary = this.lastMessages
|
|
72
|
+
? [{ role: 'assistant', content: `Últimos mensajes:
|
|
73
|
+
${this.lastMessages}` }]
|
|
74
|
+
: [];
|
|
75
|
+
|
|
76
|
+
const client = this.client;
|
|
77
|
+
if (!client) throw new Error('OpenAI client not configured');
|
|
78
|
+
|
|
79
|
+
return await client.beta.threads.create({ messages: summary });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = { DoctorScheduleAssistant };
|
|
File without changes
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
const { BaseAssistant } = require('./BaseAssistant');
|
|
2
2
|
const { ExampleAssistant } = require('./ExampleAssistant');
|
|
3
|
+
const { DoctorScheduleAssistant } = require('./DoctorScheduleAssistant');
|
|
3
4
|
|
|
4
5
|
module.exports = {
|
|
5
6
|
BaseAssistant,
|
|
6
|
-
ExampleAssistant
|
|
7
|
+
ExampleAssistant,
|
|
8
|
+
DoctorScheduleAssistant
|
|
7
9
|
};
|
package/examples/basic-usage.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/lib/adapters/index.js
CHANGED
|
File without changes
|
package/lib/adapters/registry.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
const llmConfig = require('../config/llmConfig');
|
|
2
|
+
const { Thread } = require('../models/threadModel');
|
|
3
|
+
const { getLastNMessages } = require('../helpers/assistantHelper');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Flexible base assistant implementation that integrates with OpenAI Threads
|
|
7
|
+
* and supports dynamic tool registration.
|
|
8
|
+
*/
|
|
9
|
+
class BaseAssistant {
|
|
10
|
+
constructor(input = {}) {
|
|
11
|
+
const options = this._normalizeOptions(input);
|
|
12
|
+
|
|
13
|
+
this.assistantId = options.assistantId || null;
|
|
14
|
+
this.thread = options.thread || null;
|
|
15
|
+
this.status = options.status || 'idle';
|
|
16
|
+
this.replies = null;
|
|
17
|
+
this.lastMessages = null;
|
|
18
|
+
this.createdAt = new Date();
|
|
19
|
+
|
|
20
|
+
this.client = options.client || llmConfig.openaiClient || null;
|
|
21
|
+
this.tools = new Map();
|
|
22
|
+
|
|
23
|
+
if (Array.isArray(options.tools)) {
|
|
24
|
+
options.tools.forEach((tool) => this.registerTool(tool));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (options.setup && typeof options.setup === 'function') {
|
|
28
|
+
options.setup.call(this, options);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (this.assistantId) {
|
|
32
|
+
console.log(`Assistant ${this.assistantId} initialized.`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
_normalizeOptions(input) {
|
|
37
|
+
if (!input || typeof input !== 'object') {
|
|
38
|
+
return { thread: input };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (input && input._id && !input.assistantId && !input.client && !input.tools) {
|
|
42
|
+
return { thread: input };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return input;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_ensureClient() {
|
|
49
|
+
if (!this.client) {
|
|
50
|
+
throw new Error('LLM client not configured. Ensure openaiClient is initialized.');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async getAssistantId() {
|
|
55
|
+
return this.assistantId;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async getStatus() {
|
|
59
|
+
return this.status;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async getNextAssistant() {
|
|
63
|
+
return this.nextAssistant || null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
setThread(thread) {
|
|
67
|
+
this.thread = thread;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
setReplies(replies) {
|
|
71
|
+
this.replies = replies;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
registerTool(nameOrConfig, schema, handler) {
|
|
75
|
+
let name;
|
|
76
|
+
let definition;
|
|
77
|
+
let executor;
|
|
78
|
+
|
|
79
|
+
if (typeof nameOrConfig === 'object' && nameOrConfig !== null) {
|
|
80
|
+
name = nameOrConfig.name;
|
|
81
|
+
definition = nameOrConfig.definition || nameOrConfig.schema || nameOrConfig;
|
|
82
|
+
executor = nameOrConfig.handler || nameOrConfig.execute || nameOrConfig.run;
|
|
83
|
+
} else {
|
|
84
|
+
name = nameOrConfig;
|
|
85
|
+
definition = schema;
|
|
86
|
+
executor = handler;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!name || typeof name !== 'string') {
|
|
90
|
+
throw new Error('Tool name is required');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!definition || typeof definition !== 'object') {
|
|
94
|
+
throw new Error(`Tool definition for '${name}' must be an object`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!executor || typeof executor !== 'function') {
|
|
98
|
+
throw new Error(`Tool handler for '${name}' must be a function`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const toolSchema = {
|
|
102
|
+
type: 'function',
|
|
103
|
+
function: {
|
|
104
|
+
name,
|
|
105
|
+
description: definition.description || 'Custom assistant tool',
|
|
106
|
+
parameters: definition.parameters || {
|
|
107
|
+
type: 'object',
|
|
108
|
+
properties: {},
|
|
109
|
+
additionalProperties: true
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
this.tools.set(name, {
|
|
115
|
+
schema: toolSchema,
|
|
116
|
+
execute: executor
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
getToolSchemas() {
|
|
121
|
+
return Array.from(this.tools.values()).map((value) => value.schema);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async executeTool(name, args) {
|
|
125
|
+
const tool = this.tools.get(name);
|
|
126
|
+
if (!tool) {
|
|
127
|
+
throw new Error(`Unknown tool '${name}' requested by assistant`);
|
|
128
|
+
}
|
|
129
|
+
return await tool.execute(args);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async createThread(code, context = {}) {
|
|
133
|
+
return await this.create(code, context);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async sendMessage(userId, message, options = {}) {
|
|
137
|
+
this._ensureClient();
|
|
138
|
+
|
|
139
|
+
if (!this.thread || !this.thread.thread_id) {
|
|
140
|
+
throw new Error('Assistant thread not initialized. Call create() before sendMessage().');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
await this.client.beta.threads.messages.create(
|
|
144
|
+
this.thread.thread_id,
|
|
145
|
+
{ role: 'user', content: message }
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const runConfig = {
|
|
149
|
+
assistant_id: this.assistantId,
|
|
150
|
+
...options
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const toolSchemas = this.getToolSchemas();
|
|
154
|
+
if (toolSchemas.length > 0) {
|
|
155
|
+
runConfig.tools = toolSchemas;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const run = await this.client.beta.threads.runs.create(
|
|
159
|
+
this.thread.thread_id,
|
|
160
|
+
runConfig
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
return await this.waitForCompletion(this.thread.thread_id, run.id);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async waitForCompletion(threadId, runId, { interval = 2000, maxAttempts = 30 } = {}) {
|
|
167
|
+
this._ensureClient();
|
|
168
|
+
let attempts = 0;
|
|
169
|
+
|
|
170
|
+
while (attempts < maxAttempts) {
|
|
171
|
+
const run = await this.client.beta.threads.runs.retrieve(threadId, runId);
|
|
172
|
+
|
|
173
|
+
if (run.status === 'completed') {
|
|
174
|
+
return run;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (run.status === 'requires_action') {
|
|
178
|
+
await this.handleRequiresAction(run);
|
|
179
|
+
} else if (['failed', 'cancelled', 'expired', 'incomplete'].includes(run.status)) {
|
|
180
|
+
throw new Error(`Assistant run ended with status '${run.status}'`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
184
|
+
attempts += 1;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
throw new Error('Assistant run timeout');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async getPreviousMessages(threadDoc) {
|
|
191
|
+
this._ensureClient();
|
|
192
|
+
const threadRef = threadDoc || this.thread;
|
|
193
|
+
if (!threadRef) return [];
|
|
194
|
+
|
|
195
|
+
const response = await this.client.beta.threads.messages.list(threadRef.thread_id, { order: 'asc' });
|
|
196
|
+
return response.data.map((msg) => {
|
|
197
|
+
const textContents = msg.content
|
|
198
|
+
.filter((part) => part.type === 'text')
|
|
199
|
+
.map((part) => part.text.value);
|
|
200
|
+
const content = textContents.length <= 1 ? textContents[0] || '' : textContents;
|
|
201
|
+
return { role: msg.role, content };
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async create(code, context = {}) {
|
|
206
|
+
this._ensureClient();
|
|
207
|
+
this.status = 'active';
|
|
208
|
+
|
|
209
|
+
const whatsappId = context?.whatsapp_id || code;
|
|
210
|
+
if (whatsappId) {
|
|
211
|
+
this.lastMessages = await getLastNMessages(whatsappId, 20);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const initialMessages = await this.buildInitialMessages({ code, context });
|
|
215
|
+
const threadPayload = {};
|
|
216
|
+
|
|
217
|
+
if (initialMessages.length > 0) {
|
|
218
|
+
threadPayload.messages = initialMessages.map((msg) => ({
|
|
219
|
+
role: msg.role || 'assistant',
|
|
220
|
+
content: msg.content
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const thread = await this.client.beta.threads.create(threadPayload);
|
|
225
|
+
this.thread = thread;
|
|
226
|
+
return thread;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async buildInitialMessages({ code }) {
|
|
230
|
+
if (!this.lastMessages) return [];
|
|
231
|
+
return [{
|
|
232
|
+
role: 'assistant',
|
|
233
|
+
content: `Últimos mensajes para ${code}:
|
|
234
|
+
${this.lastMessages}`
|
|
235
|
+
}];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async handleRequiresAction(run) {
|
|
239
|
+
const toolCalls = run?.required_action?.submit_tool_outputs?.tool_calls || [];
|
|
240
|
+
if (toolCalls.length === 0) {
|
|
241
|
+
return [];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const outputs = [];
|
|
245
|
+
|
|
246
|
+
for (const call of toolCalls) {
|
|
247
|
+
try {
|
|
248
|
+
const name = call.function?.name;
|
|
249
|
+
const args = call.function?.arguments ? JSON.parse(call.function.arguments) : {};
|
|
250
|
+
const result = await this.executeTool(name, args);
|
|
251
|
+
outputs.push({ tool_call_id: call.id, output: typeof result === 'string' ? result : JSON.stringify(result) });
|
|
252
|
+
} catch (error) {
|
|
253
|
+
console.error('[BaseAssistant] Tool execution failed', error);
|
|
254
|
+
outputs.push({
|
|
255
|
+
tool_call_id: call.id,
|
|
256
|
+
output: JSON.stringify({ success: false, error: error?.message || 'Tool execution failed' })
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!this.client) {
|
|
262
|
+
console.warn('[BaseAssistant] Cannot submit tool outputs: client not configured');
|
|
263
|
+
return outputs;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
await this.client.beta.threads.runs.submitToolOutputs(
|
|
267
|
+
run.thread_id,
|
|
268
|
+
run.id,
|
|
269
|
+
{ tool_outputs: outputs }
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
return outputs;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async close() {
|
|
276
|
+
this.status = 'closed';
|
|
277
|
+
if (this.thread?.code && this.thread?.assistant_id) {
|
|
278
|
+
await Thread.updateOne(
|
|
279
|
+
{ code: this.thread.code, assistant_id: this.thread.assistant_id },
|
|
280
|
+
{ $set: { active: false } }
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (this.assistantId) {
|
|
285
|
+
console.log(`Assistant ${this.assistantId} closed`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return `Assistant ${this.assistantId || ''} successfully closed.`.trim();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
module.exports = {
|
|
293
|
+
BaseAssistant
|
|
294
|
+
};
|
|
File without changes
|
package/lib/config/awsConfig.js
CHANGED
|
File without changes
|
|
File without changes
|
package/lib/config/llmConfig.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/lib/core/index.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/lib/helpers/qrHelper.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/lib/index.d.ts
CHANGED
|
@@ -117,6 +117,48 @@ declare module '@peopl-health/nexus' {
|
|
|
117
117
|
onFlow?: FlowHandler;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
export interface AssistantToolDefinition {
|
|
121
|
+
name: string;
|
|
122
|
+
description?: string;
|
|
123
|
+
parameters?: any;
|
|
124
|
+
handler: (args: any) => any;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface AssistantConfigDefinition {
|
|
128
|
+
extends?: typeof BaseAssistant;
|
|
129
|
+
create?: (this: BaseAssistant, code: string, context?: any) => Promise<any>;
|
|
130
|
+
tools?: Array<AssistantToolDefinition | { name: string; definition?: any; handler: (args: any) => any }>;
|
|
131
|
+
setup?: (this: BaseAssistant, context: { assistantId: string; thread?: any; options?: any }) => void;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export class BaseAssistant {
|
|
135
|
+
constructor(options?: {
|
|
136
|
+
assistantId?: string;
|
|
137
|
+
thread?: any;
|
|
138
|
+
client?: any;
|
|
139
|
+
tools?: Array<AssistantToolDefinition | { name: string; definition?: any; handler: (args: any) => any }>;
|
|
140
|
+
setup?: (context: { assistantId: string; thread?: any; options?: any }) => void;
|
|
141
|
+
status?: string;
|
|
142
|
+
} | any);
|
|
143
|
+
assistantId: string | null;
|
|
144
|
+
thread: any;
|
|
145
|
+
status: string;
|
|
146
|
+
createdAt: Date;
|
|
147
|
+
registerTool(definition: AssistantToolDefinition | string, schema?: any, handler?: (args: any) => any): void;
|
|
148
|
+
getToolSchemas(): any[];
|
|
149
|
+
executeTool(name: string, args: any): Promise<any>;
|
|
150
|
+
getPreviousMessages(thread?: any): Promise<Array<{ role: string; content: any }>>;
|
|
151
|
+
createThread(code: string, context?: any): Promise<any>;
|
|
152
|
+
sendMessage(userId: string, message: string, options?: any): Promise<any>;
|
|
153
|
+
waitForCompletion(threadId: string, runId: string, opts?: { interval?: number; maxAttempts?: number }): Promise<any>;
|
|
154
|
+
create(code: string, context?: any): Promise<any>;
|
|
155
|
+
buildInitialMessages(context: { code: string; [key: string]: any }): Promise<Array<{ role: string; content: string }>>;
|
|
156
|
+
handleRequiresAction(run: any): Promise<any[]>;
|
|
157
|
+
close(): Promise<string>;
|
|
158
|
+
setThread(thread: any): void;
|
|
159
|
+
setReplies(replies: any): void;
|
|
160
|
+
}
|
|
161
|
+
|
|
120
162
|
// Core Classes
|
|
121
163
|
export abstract class MessageProvider {
|
|
122
164
|
constructor(config: any);
|
|
@@ -127,6 +169,15 @@ declare module '@peopl-health/nexus' {
|
|
|
127
169
|
abstract disconnect(): Promise<void>;
|
|
128
170
|
}
|
|
129
171
|
|
|
172
|
+
export function registerAssistant(
|
|
173
|
+
assistantId: string,
|
|
174
|
+
definition: typeof BaseAssistant | ((thread?: any) => any) | AssistantConfigDefinition
|
|
175
|
+
): any;
|
|
176
|
+
|
|
177
|
+
export function configureAssistantsLLM(client: any): void;
|
|
178
|
+
export function overrideGetAssistantById(resolver: (assistantId: string, thread?: any) => any): void;
|
|
179
|
+
export function configureAssistants(config: any): void;
|
|
180
|
+
|
|
130
181
|
export class TwilioProvider extends MessageProvider {
|
|
131
182
|
constructor(config: TwilioConfig);
|
|
132
183
|
initialize(): Promise<void>;
|
package/lib/index.js
CHANGED
|
@@ -16,6 +16,7 @@ const {
|
|
|
16
16
|
} = require('./services/assistantService');
|
|
17
17
|
const { TwilioProvider } = require('./adapters/TwilioProvider');
|
|
18
18
|
const { BaileysProvider } = require('./adapters/BaileysProvider');
|
|
19
|
+
const { BaseAssistant: CoreBaseAssistant } = require('./assistants/BaseAssistant');
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Main Nexus class that orchestrates all components
|
|
@@ -312,6 +313,11 @@ module.exports = {
|
|
|
312
313
|
MongoStorage,
|
|
313
314
|
MessageParser,
|
|
314
315
|
DefaultLLMProvider,
|
|
316
|
+
BaseAssistant: CoreBaseAssistant,
|
|
317
|
+
registerAssistant,
|
|
318
|
+
configureAssistantsLLM,
|
|
319
|
+
overrideGetAssistantById,
|
|
320
|
+
configureAssistants: setAssistantsConfig,
|
|
315
321
|
routes,
|
|
316
322
|
setupDefaultRoutes: routes.setupDefaultRoutes,
|
|
317
323
|
createRouter: routes.createRouter,
|
package/lib/interactive/index.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/lib/models/index.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/lib/routes/index.js
CHANGED
|
File without changes
|
|
File without changes
|
|
@@ -3,6 +3,8 @@ const AWS = require('../config/awsConfig.js');
|
|
|
3
3
|
const { combineImagesToPDF, cleanupFiles } = require('../helpers/filesHelper.js');
|
|
4
4
|
const { addRecord } = require('../services/airtableService.js');
|
|
5
5
|
const runtimeConfig = require('../config/runtimeConfig');
|
|
6
|
+
const llmConfig = require('../config/llmConfig');
|
|
7
|
+
const { BaseAssistant } = require('../assistants/BaseAssistant');
|
|
6
8
|
|
|
7
9
|
let llmProvider = null;
|
|
8
10
|
const configureLLMProvider = (provider) => {
|
|
@@ -27,8 +29,54 @@ const configureAssistants = (config) => {
|
|
|
27
29
|
assistantConfig = config;
|
|
28
30
|
};
|
|
29
31
|
|
|
30
|
-
const registerAssistant = (assistantId,
|
|
31
|
-
|
|
32
|
+
const registerAssistant = (assistantId, definition) => {
|
|
33
|
+
if (!assistantId || typeof assistantId !== 'string') {
|
|
34
|
+
throw new Error('registerAssistant requires a string assistantId');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (typeof definition === 'function') {
|
|
38
|
+
assistantRegistry[assistantId] = definition;
|
|
39
|
+
return definition;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (definition && typeof definition === 'object') {
|
|
43
|
+
const {
|
|
44
|
+
extends: ParentClass = BaseAssistant,
|
|
45
|
+
create,
|
|
46
|
+
tools = [],
|
|
47
|
+
setup
|
|
48
|
+
} = definition;
|
|
49
|
+
|
|
50
|
+
class ConfiguredAssistant extends ParentClass {
|
|
51
|
+
constructor(options = {}) {
|
|
52
|
+
super({
|
|
53
|
+
...options,
|
|
54
|
+
assistantId,
|
|
55
|
+
client: options.client || llmProvider || llmConfig.openaiClient || null,
|
|
56
|
+
tools: [...tools, ...(options.tools || [])]
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (typeof setup === 'function') {
|
|
60
|
+
setup.call(this, {
|
|
61
|
+
assistantId,
|
|
62
|
+
thread: this.thread,
|
|
63
|
+
options
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (typeof create === 'function') {
|
|
70
|
+
ConfiguredAssistant.prototype.create = async function(code, context) {
|
|
71
|
+
return await create.call(this, code, context);
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
assistantRegistry[assistantId] = ConfiguredAssistant;
|
|
76
|
+
return ConfiguredAssistant;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
throw new Error('registerAssistant expects a class/function or configuration object');
|
|
32
80
|
};
|
|
33
81
|
|
|
34
82
|
const overrideGetAssistantById = (resolverFn) => {
|
|
@@ -51,8 +99,22 @@ const getAssistantById = (assistant_id, thread) => {
|
|
|
51
99
|
if (!AssistantClass) {
|
|
52
100
|
throw new Error(`Assistant '${assistant_id}' not found. Available assistants: ${Object.keys(assistantRegistry).join(', ')}`);
|
|
53
101
|
}
|
|
54
|
-
|
|
55
|
-
|
|
102
|
+
|
|
103
|
+
const sharedClient = llmProvider || llmConfig.openaiClient || null;
|
|
104
|
+
|
|
105
|
+
if (AssistantClass.prototype instanceof BaseAssistant) {
|
|
106
|
+
return new AssistantClass({
|
|
107
|
+
assistantId: assistant_id,
|
|
108
|
+
thread,
|
|
109
|
+
client: sharedClient
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
return new AssistantClass(thread);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
return new AssistantClass({ thread, assistantId: assistant_id, client: sharedClient });
|
|
117
|
+
}
|
|
56
118
|
};
|
|
57
119
|
|
|
58
120
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/lib/storage/index.js
CHANGED
|
File without changes
|
package/lib/storage/registry.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/lib/utils/dateUtils.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/lib/utils/index.js
CHANGED
|
File without changes
|
package/lib/utils/logger.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|