@peopl-health/nexus 1.5.7 → 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.
Files changed (72) hide show
  1. package/CHANGELOG.md +0 -0
  2. package/LICENSE +0 -0
  3. package/MIGRATION_GUIDE.md +0 -0
  4. package/README.md +0 -0
  5. package/examples/.env.example +0 -0
  6. package/examples/assistants/BaseAssistant.js +1 -242
  7. package/examples/assistants/DoctorScheduleAssistant.js +83 -0
  8. package/examples/assistants/ExampleAssistant.js +0 -0
  9. package/examples/assistants/index.js +3 -1
  10. package/examples/basic-usage.js +0 -0
  11. package/examples/consumer-server.js +0 -0
  12. package/lib/adapters/BaileysProvider.js +0 -0
  13. package/lib/adapters/TwilioProvider.js +0 -0
  14. package/lib/adapters/index.js +0 -0
  15. package/lib/adapters/registry.js +0 -0
  16. package/lib/assistants/BaseAssistant.js +294 -0
  17. package/lib/assistants/index.js +5 -0
  18. package/lib/config/airtableConfig.js +0 -0
  19. package/lib/config/awsConfig.js +1 -1
  20. package/lib/config/configLoader.js +0 -0
  21. package/lib/config/llmConfig.js +0 -0
  22. package/lib/config/mongoAuthConfig.js +0 -0
  23. package/lib/config/runtimeConfig.js +0 -0
  24. package/lib/controllers/assistantController.js +0 -0
  25. package/lib/controllers/conversationController.js +0 -0
  26. package/lib/controllers/mediaController.js +0 -0
  27. package/lib/controllers/messageController.js +0 -0
  28. package/lib/controllers/templateController.js +0 -0
  29. package/lib/controllers/templateFlowController.js +0 -0
  30. package/lib/controllers/uploadController.js +0 -0
  31. package/lib/core/MessageProvider.js +0 -0
  32. package/lib/core/NexusMessaging.js +0 -0
  33. package/lib/core/index.js +0 -0
  34. package/lib/helpers/assistantHelper.js +0 -0
  35. package/lib/helpers/baileysHelper.js +0 -0
  36. package/lib/helpers/filesHelper.js +0 -0
  37. package/lib/helpers/llmsHelper.js +0 -0
  38. package/lib/helpers/mediaHelper.js +0 -0
  39. package/lib/helpers/mongoHelper.js +0 -0
  40. package/lib/helpers/qrHelper.js +0 -0
  41. package/lib/helpers/twilioHelper.js +0 -0
  42. package/lib/helpers/twilioMediaProcessor.js +0 -0
  43. package/lib/helpers/whatsappHelper.js +0 -0
  44. package/lib/index.d.ts +51 -0
  45. package/lib/index.js +6 -0
  46. package/lib/interactive/index.js +0 -0
  47. package/lib/interactive/registry.js +0 -0
  48. package/lib/interactive/twilioMapper.js +0 -0
  49. package/lib/models/agendaMessageModel.js +0 -0
  50. package/lib/models/index.js +0 -0
  51. package/lib/models/messageModel.js +0 -0
  52. package/lib/models/templateModel.js +0 -0
  53. package/lib/models/threadModel.js +0 -0
  54. package/lib/routes/index.js +0 -0
  55. package/lib/services/airtableService.js +0 -0
  56. package/lib/services/assistantService.js +66 -4
  57. package/lib/services/conversationService.js +0 -0
  58. package/lib/services/twilioService.js +0 -0
  59. package/lib/storage/MongoStorage.js +0 -0
  60. package/lib/storage/NoopStorage.js +0 -0
  61. package/lib/storage/index.js +0 -0
  62. package/lib/storage/registry.js +0 -0
  63. package/lib/templates/predefinedTemplates.js +0 -0
  64. package/lib/templates/templateStructure.js +0 -0
  65. package/lib/utils/dateUtils.js +0 -0
  66. package/lib/utils/defaultLLMProvider.js +0 -0
  67. package/lib/utils/errorHandler.js +0 -0
  68. package/lib/utils/index.js +0 -0
  69. package/lib/utils/logger.js +0 -0
  70. package/lib/utils/mediaValidator.js +0 -0
  71. package/lib/utils/messageParser.js +0 -0
  72. package/package.json +44 -34
package/CHANGELOG.md CHANGED
File without changes
package/LICENSE CHANGED
File without changes
File without changes
package/README.md CHANGED
File without changes
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
  };
File without changes
File without changes
File without changes
File without changes
File without changes
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
+ };
@@ -0,0 +1,5 @@
1
+ const { BaseAssistant } = require('./BaseAssistant');
2
+
3
+ module.exports = {
4
+ BaseAssistant
5
+ };
File without changes
@@ -86,7 +86,7 @@ async function generatePresignedUrl(bucketName, key, expiration = 300) {
86
86
  };
87
87
 
88
88
  try {
89
- const url = s3.getSignedUrlPromise('getObject', params);
89
+ const url = await s3.getSignedUrlPromise('getObject', params);
90
90
  console.log(`Presigned URL generated: ${url}`);
91
91
  return url;
92
92
  } catch (error) {
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
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
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,
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
@@ -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, AssistantClass) => {
31
- assistantRegistry[assistantId] = AssistantClass;
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
- return new AssistantClass(thread);
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
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/package.json CHANGED
@@ -1,12 +1,26 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "1.5.7",
3
+ "version": "1.5.9",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
- "publishConfig": {
6
- "access": "public"
5
+ "keywords": [
6
+ "whatsapp",
7
+ "messaging",
8
+ "ai-assistant",
9
+ "twilio",
10
+ "baileys",
11
+ "communication"
12
+ ],
13
+ "homepage": "https://github.com/peopl-health/nexus#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/peopl-health/nexus/issues"
7
16
  },
8
- "main": "lib/index.js",
9
- "types": "lib/index.d.ts",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/peopl-health/nexus.git"
20
+ },
21
+ "license": "MIT",
22
+ "author": "PEOPL Health Tech",
23
+ "type": "commonjs",
10
24
  "exports": {
11
25
  ".": {
12
26
  "import": "./lib/index.js",
@@ -30,6 +44,21 @@
30
44
  "require": "./lib/routes/index.js"
31
45
  }
32
46
  },
47
+ "main": "lib/index.js",
48
+ "types": "lib/index.d.ts",
49
+ "directories": {
50
+ "example": "examples",
51
+ "lib": "lib",
52
+ "test": "test"
53
+ },
54
+ "files": [
55
+ "lib/",
56
+ "examples/",
57
+ "README.md",
58
+ "LICENSE",
59
+ "CHANGELOG.md",
60
+ "MIGRATION_GUIDE.md"
61
+ ],
33
62
  "scripts": {
34
63
  "build": "tsc",
35
64
  "dev": "tsc --watch",
@@ -40,16 +69,6 @@
40
69
  "version": "npm run prepublishOnly && git add -A lib",
41
70
  "postversion": "git push && git push --tags"
42
71
  },
43
- "keywords": [
44
- "whatsapp",
45
- "messaging",
46
- "ai-assistant",
47
- "twilio",
48
- "baileys",
49
- "communication"
50
- ],
51
- "author": "PEOPL Health Tech",
52
- "license": "MIT",
53
72
  "dependencies": {
54
73
  "airtable": "^0.12.2",
55
74
  "aws-sdk": "2.1674.0",
@@ -63,13 +82,6 @@
63
82
  "pino-pretty": "^10.2.0",
64
83
  "uuid": "^9.0.0"
65
84
  },
66
- "peerDependencies": {
67
- "baileys": "^6.4.0",
68
- "express": "4.21.2",
69
- "openai": "^4.0.0",
70
- "twilio": "5.6.0",
71
- "@anthropic-ai/sdk": "^0.32.0"
72
- },
73
85
  "devDependencies": {
74
86
  "@types/node": "^20.5.0",
75
87
  "eslint": "^8.47.0",
@@ -77,19 +89,17 @@
77
89
  "sharp": "0.32.6",
78
90
  "typescript": "^5.1.6"
79
91
  },
92
+ "peerDependencies": {
93
+ "baileys": "^6.4.0",
94
+ "express": "4.21.2",
95
+ "openai": "^4.0.0",
96
+ "twilio": "5.6.0",
97
+ "@anthropic-ai/sdk": "^0.32.0"
98
+ },
80
99
  "engines": {
81
100
  "node": ">=20.0.0"
82
101
  },
83
- "repository": {
84
- "type": "git",
85
- "url": "https://github.com/peopl-health/nexus.git"
86
- },
87
- "files": [
88
- "lib/",
89
- "examples/",
90
- "README.md",
91
- "LICENSE",
92
- "CHANGELOG.md",
93
- "MIGRATION_GUIDE.md"
94
- ]
102
+ "publishConfig": {
103
+ "access": "public"
104
+ }
95
105
  }