@peopl-health/nexus 1.6.2 → 1.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION_GUIDE.md +1 -1
- package/README.md +2 -2
- package/examples/consumer-server.js +11 -11
- package/lib/adapters/BaileysProvider.js +6 -6
- package/lib/adapters/TwilioProvider.js +12 -12
- package/lib/assistants/BaseAssistant.js +22 -2
- package/lib/config/llmConfig.js +53 -1
- package/lib/core/MessageProvider.js +1 -1
- package/lib/core/NexusMessaging.js +4 -10
- package/lib/helpers/assistantHelper.js +13 -19
- package/lib/index.d.ts +1 -1
- package/lib/index.js +22 -8
- package/lib/interactive/index.js +3 -3
- package/lib/providers/OpenAIProvider.js +297 -0
- package/lib/services/assistantService.js +135 -69
- package/lib/storage/MongoStorage.js +1 -1
- package/lib/utils/defaultLLMProvider.js +6 -8
- package/package.json +1 -1
package/MIGRATION_GUIDE.md
CHANGED
|
@@ -20,7 +20,7 @@ await nexus.sendMessage({ code: 'whatsapp:+521555...', message: 'Hi' });
|
|
|
20
20
|
```
|
|
21
21
|
After (preferred):
|
|
22
22
|
```js
|
|
23
|
-
await nexus.sendMessage({
|
|
23
|
+
await nexus.sendMessage({ code: '+521555...', message: 'Hi' });
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
## Templates & Flows (Twilio)
|
package/README.md
CHANGED
|
@@ -88,10 +88,10 @@ registerFlow('greeting_qr', {
|
|
|
88
88
|
type: 'quick-reply', language: 'es', body: 'Hola {{1}}', variables: { '1': 'Nombre' },
|
|
89
89
|
buttons: [{ text: 'Sí' }, { text: 'No' }]
|
|
90
90
|
});
|
|
91
|
-
await sendInteractive(nexus, {
|
|
91
|
+
await sendInteractive(nexus, { code: '+521555...', id: 'greeting_qr' });
|
|
92
92
|
|
|
93
93
|
registerInteractiveHandler({ type: 'button', id: /sí|si/i }, async (msg, messaging) => {
|
|
94
|
-
await messaging.sendMessage({
|
|
94
|
+
await messaging.sendMessage({ code: msg.from, message: '¡Confirmado!' });
|
|
95
95
|
});
|
|
96
96
|
attachInteractiveRouter(nexus);
|
|
97
97
|
```
|
|
@@ -51,7 +51,7 @@ async function initializeNexus() {
|
|
|
51
51
|
const response = await processTextMessage(messageData);
|
|
52
52
|
if (response) {
|
|
53
53
|
await nexus.sendMessage({
|
|
54
|
-
|
|
54
|
+
code: messageData.from,
|
|
55
55
|
message: response
|
|
56
56
|
});
|
|
57
57
|
}
|
|
@@ -64,7 +64,7 @@ async function initializeNexus() {
|
|
|
64
64
|
switch (command) {
|
|
65
65
|
case 'help':
|
|
66
66
|
await nexus.sendMessage({
|
|
67
|
-
|
|
67
|
+
code: messageData.from,
|
|
68
68
|
message: `🤖 Available Commands:
|
|
69
69
|
/help - Show this help
|
|
70
70
|
/support - Connect to support assistant
|
|
@@ -78,7 +78,7 @@ async function initializeNexus() {
|
|
|
78
78
|
const supportResponse = await assistants.support.handleMessage(messageData.from, 'Hello, I need support.');
|
|
79
79
|
if (supportResponse) {
|
|
80
80
|
await nexus.sendMessage({
|
|
81
|
-
|
|
81
|
+
code: messageData.from,
|
|
82
82
|
message: supportResponse
|
|
83
83
|
});
|
|
84
84
|
}
|
|
@@ -88,7 +88,7 @@ async function initializeNexus() {
|
|
|
88
88
|
const salesResponse = await assistants.sales.handleMessage(messageData.from, 'Hello, I\'m interested in your products.');
|
|
89
89
|
if (salesResponse) {
|
|
90
90
|
await nexus.sendMessage({
|
|
91
|
-
|
|
91
|
+
code: messageData.from,
|
|
92
92
|
message: salesResponse
|
|
93
93
|
});
|
|
94
94
|
}
|
|
@@ -96,7 +96,7 @@ async function initializeNexus() {
|
|
|
96
96
|
|
|
97
97
|
case 'status':
|
|
98
98
|
await nexus.sendMessage({
|
|
99
|
-
|
|
99
|
+
code: messageData.from,
|
|
100
100
|
message: `System Status: ${nexus.isConnected() ? '✅ Connected' : '❌ Disconnected'}`
|
|
101
101
|
});
|
|
102
102
|
break;
|
|
@@ -106,14 +106,14 @@ async function initializeNexus() {
|
|
|
106
106
|
await nexus.getStorage().clearThread(messageData.from);
|
|
107
107
|
}
|
|
108
108
|
await nexus.sendMessage({
|
|
109
|
-
|
|
109
|
+
code: messageData.from,
|
|
110
110
|
message: 'Conversation reset! You can start fresh now.'
|
|
111
111
|
});
|
|
112
112
|
break;
|
|
113
113
|
|
|
114
114
|
default:
|
|
115
115
|
await nexus.sendMessage({
|
|
116
|
-
|
|
116
|
+
code: messageData.from,
|
|
117
117
|
message: `Unknown command: /${command}. Type /help for available commands.`
|
|
118
118
|
});
|
|
119
119
|
}
|
|
@@ -136,10 +136,10 @@ app.post('/webhook', async (req, res) => {
|
|
|
136
136
|
// API endpoints
|
|
137
137
|
app.post('/api/send-message', async (req, res) => {
|
|
138
138
|
try {
|
|
139
|
-
const {
|
|
139
|
+
const { code, message, fileUrl, fileType } = req.body;
|
|
140
140
|
|
|
141
141
|
const result = await nexus.sendMessage({
|
|
142
|
-
|
|
142
|
+
code,
|
|
143
143
|
message,
|
|
144
144
|
fileUrl,
|
|
145
145
|
fileType
|
|
@@ -153,10 +153,10 @@ app.post('/api/send-message', async (req, res) => {
|
|
|
153
153
|
|
|
154
154
|
app.post('/api/send-template', async (req, res) => {
|
|
155
155
|
try {
|
|
156
|
-
const {
|
|
156
|
+
const { code, contentSid, variables } = req.body;
|
|
157
157
|
|
|
158
158
|
const result = await nexus.sendMessage({
|
|
159
|
-
|
|
159
|
+
code,
|
|
160
160
|
contentSid,
|
|
161
161
|
variables
|
|
162
162
|
});
|
|
@@ -84,9 +84,9 @@ class BaileysProvider extends MessageProvider {
|
|
|
84
84
|
throw new Error('Baileys provider not connected');
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
const {
|
|
87
|
+
const { code, message, fileUrl, fileType, hidePreview = false } = messageData;
|
|
88
88
|
|
|
89
|
-
if (!
|
|
89
|
+
if (!code) {
|
|
90
90
|
throw new Error('Recipient is required');
|
|
91
91
|
}
|
|
92
92
|
|
|
@@ -94,7 +94,7 @@ class BaileysProvider extends MessageProvider {
|
|
|
94
94
|
throw new Error('Message or file URL is required');
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
const
|
|
97
|
+
const formattedCode = this.formatCode(code);
|
|
98
98
|
let sendOptions = {};
|
|
99
99
|
let formattedMessage = this.formatMessage(message || '');
|
|
100
100
|
|
|
@@ -135,7 +135,7 @@ class BaileysProvider extends MessageProvider {
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
try {
|
|
138
|
-
const result = await this.waSocket.sendMessage(
|
|
138
|
+
const result = await this.waSocket.sendMessage(formattedCode, sendOptions);
|
|
139
139
|
return {
|
|
140
140
|
success: true,
|
|
141
141
|
messageId: result?.key?.id,
|
|
@@ -156,7 +156,7 @@ class BaileysProvider extends MessageProvider {
|
|
|
156
156
|
: async (payload) => await this.sendMessage(payload);
|
|
157
157
|
|
|
158
158
|
console.log('[BaileysProvider] Scheduled message created', {
|
|
159
|
-
|
|
159
|
+
code: scheduledMessage.code,
|
|
160
160
|
delay,
|
|
161
161
|
hasMedia: Boolean(scheduledMessage.fileUrl)
|
|
162
162
|
});
|
|
@@ -166,7 +166,7 @@ class BaileysProvider extends MessageProvider {
|
|
|
166
166
|
const payload = { ...scheduledMessage };
|
|
167
167
|
delete payload.__nexusSend;
|
|
168
168
|
console.log('[BaileysProvider] Timer fired', {
|
|
169
|
-
|
|
169
|
+
code: payload.to || payload.code,
|
|
170
170
|
hasMessage: Boolean(payload.message || payload.body),
|
|
171
171
|
hasMedia: Boolean(payload.fileUrl)
|
|
172
172
|
});
|
|
@@ -47,16 +47,16 @@ class TwilioProvider extends MessageProvider {
|
|
|
47
47
|
const { code, message, fileUrl, fileType, variables, contentSid } = messageData;
|
|
48
48
|
|
|
49
49
|
const formattedFrom = this.ensureWhatsAppFormat(this.whatsappNumber);
|
|
50
|
-
const
|
|
50
|
+
const formattedCode = this.ensureWhatsAppFormat(code);
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
if (!formattedFrom || !
|
|
53
|
+
if (!formattedFrom || !formattedCode) {
|
|
54
54
|
throw new Error('Invalid sender or recipient number');
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
const messageParams = {
|
|
58
58
|
from: formattedFrom,
|
|
59
|
-
to:
|
|
59
|
+
to: formattedCode
|
|
60
60
|
};
|
|
61
61
|
|
|
62
62
|
// Handle template messages
|
|
@@ -83,7 +83,7 @@ class TwilioProvider extends MessageProvider {
|
|
|
83
83
|
|
|
84
84
|
// Handle media messages
|
|
85
85
|
if (fileUrl && fileType !== 'text') {
|
|
86
|
-
const mediaPrep = await this.prepareOutboundMedia(messageData,
|
|
86
|
+
const mediaPrep = await this.prepareOutboundMedia(messageData, formattedCode);
|
|
87
87
|
const outboundMediaUrl = mediaPrep.mediaUrl || fileUrl;
|
|
88
88
|
messageParams.mediaUrl = [outboundMediaUrl];
|
|
89
89
|
if (!messageParams.body || messageParams.body.trim() === '') {
|
|
@@ -91,7 +91,7 @@ class TwilioProvider extends MessageProvider {
|
|
|
91
91
|
}
|
|
92
92
|
if (mediaPrep.uploaded) {
|
|
93
93
|
console.log('[TwilioProvider] Outbound media uploaded to S3', {
|
|
94
|
-
|
|
94
|
+
code: formattedCode,
|
|
95
95
|
bucket: mediaPrep.bucketName,
|
|
96
96
|
key: mediaPrep.key
|
|
97
97
|
});
|
|
@@ -108,14 +108,14 @@ class TwilioProvider extends MessageProvider {
|
|
|
108
108
|
if (this.messageStorage && typeof this.messageStorage.saveMessage === 'function') {
|
|
109
109
|
try {
|
|
110
110
|
console.log('[TwilioProvider] Persisting outbound message', {
|
|
111
|
-
|
|
111
|
+
code: formattedCode,
|
|
112
112
|
from: formattedFrom,
|
|
113
113
|
hasMedia: Boolean(messageParams.mediaUrl && messageParams.mediaUrl.length),
|
|
114
114
|
hasTemplate: Boolean(messageParams.contentSid)
|
|
115
115
|
});
|
|
116
116
|
await this.messageStorage.saveMessage({
|
|
117
117
|
...messageData,
|
|
118
|
-
|
|
118
|
+
code: formattedCode,
|
|
119
119
|
from: formattedFrom,
|
|
120
120
|
messageId: result.sid,
|
|
121
121
|
provider: 'twilio',
|
|
@@ -147,7 +147,7 @@ class TwilioProvider extends MessageProvider {
|
|
|
147
147
|
: async (payload) => await this.sendMessage(payload);
|
|
148
148
|
|
|
149
149
|
console.log('[TwilioProvider] Scheduled message created', {
|
|
150
|
-
|
|
150
|
+
code: scheduledMessage.code,
|
|
151
151
|
delay,
|
|
152
152
|
hasContentSid: Boolean(scheduledMessage.contentSid)
|
|
153
153
|
});
|
|
@@ -158,7 +158,7 @@ class TwilioProvider extends MessageProvider {
|
|
|
158
158
|
const payload = scheduledMessage.toObject ? scheduledMessage.toObject() : { ...scheduledMessage };
|
|
159
159
|
delete payload.__nexusSend;
|
|
160
160
|
console.log('[TwilioProvider] Timer fired', {
|
|
161
|
-
|
|
161
|
+
code: payload.code,
|
|
162
162
|
hasMessage: Boolean(payload.message || payload.body),
|
|
163
163
|
hasMedia: Boolean(payload.fileUrl)
|
|
164
164
|
});
|
|
@@ -176,7 +176,7 @@ class TwilioProvider extends MessageProvider {
|
|
|
176
176
|
return true;
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
async prepareOutboundMedia(messageData,
|
|
179
|
+
async prepareOutboundMedia(messageData, formattedCode) {
|
|
180
180
|
const bucketName = runtimeConfig.get('AWS_S3_BUCKET_NAME') || process.env.AWS_S3_BUCKET_NAME;
|
|
181
181
|
const fileUrl = messageData.fileUrl;
|
|
182
182
|
|
|
@@ -221,7 +221,7 @@ class TwilioProvider extends MessageProvider {
|
|
|
221
221
|
|
|
222
222
|
if (!validation.valid) {
|
|
223
223
|
console.warn('[TwilioProvider] Outbound media validation warning', {
|
|
224
|
-
|
|
224
|
+
code: formattedCode,
|
|
225
225
|
message: validation.message
|
|
226
226
|
});
|
|
227
227
|
}
|
|
@@ -270,7 +270,7 @@ class TwilioProvider extends MessageProvider {
|
|
|
270
270
|
};
|
|
271
271
|
} catch (error) {
|
|
272
272
|
console.error('[TwilioProvider] Failed to upload outbound media to S3. Using original URL.', {
|
|
273
|
-
|
|
273
|
+
code: formattedCode,
|
|
274
274
|
error: error?.message || error
|
|
275
275
|
});
|
|
276
276
|
|
|
@@ -17,7 +17,27 @@ class BaseAssistant {
|
|
|
17
17
|
this.lastMessages = null;
|
|
18
18
|
this.createdAt = new Date();
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
const existingProvider = llmConfig.getOpenAIProvider({ instantiate: false });
|
|
21
|
+
this.provider = options.provider || existingProvider || null;
|
|
22
|
+
this.client = options.client
|
|
23
|
+
|| (this.provider && this.provider.getClient ? this.provider.getClient() : null)
|
|
24
|
+
|| llmConfig.openaiClient
|
|
25
|
+
|| null;
|
|
26
|
+
|
|
27
|
+
if (!this.provider && this.client) {
|
|
28
|
+
try {
|
|
29
|
+
const { OpenAIProvider } = require('../providers/OpenAIProvider');
|
|
30
|
+
const provider = new OpenAIProvider({ client: this.client });
|
|
31
|
+
this.provider = provider;
|
|
32
|
+
if (typeof llmConfig.setOpenAIProvider === 'function') {
|
|
33
|
+
llmConfig.setOpenAIProvider(provider);
|
|
34
|
+
}
|
|
35
|
+
} catch (err) {
|
|
36
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
37
|
+
console.warn('[BaseAssistant] Failed to initialise OpenAIProvider from client:', err?.message || err);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
21
41
|
this.tools = new Map();
|
|
22
42
|
|
|
23
43
|
if (Array.isArray(options.tools)) {
|
|
@@ -67,7 +87,7 @@ class BaseAssistant {
|
|
|
67
87
|
this.thread = thread;
|
|
68
88
|
}
|
|
69
89
|
|
|
70
|
-
|
|
90
|
+
setReplies(replies) {
|
|
71
91
|
this.replies = replies;
|
|
72
92
|
}
|
|
73
93
|
|
package/lib/config/llmConfig.js
CHANGED
|
@@ -1,6 +1,53 @@
|
|
|
1
1
|
const runtimeConfig = require('./runtimeConfig');
|
|
2
|
+
const { OpenAIProvider } = require('../providers/OpenAIProvider');
|
|
2
3
|
|
|
3
4
|
let anthropicClient = null;
|
|
5
|
+
let openaiClient = null;
|
|
6
|
+
let openaiProviderInstance = null;
|
|
7
|
+
|
|
8
|
+
const setOpenAIClient = (client) => {
|
|
9
|
+
openaiClient = client || null;
|
|
10
|
+
module.exports.openaiClient = openaiClient;
|
|
11
|
+
if (!client) {
|
|
12
|
+
openaiProviderInstance = null;
|
|
13
|
+
module.exports.openaiProvider = null;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const setOpenAIProvider = (provider) => {
|
|
18
|
+
openaiProviderInstance = provider || null;
|
|
19
|
+
module.exports.openaiProvider = openaiProviderInstance;
|
|
20
|
+
|
|
21
|
+
if (!provider) {
|
|
22
|
+
setOpenAIClient(null);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const client = typeof provider.getClient === 'function'
|
|
27
|
+
? provider.getClient()
|
|
28
|
+
: provider.client;
|
|
29
|
+
|
|
30
|
+
if (client) {
|
|
31
|
+
setOpenAIClient(client);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const getOpenAIProvider = ({ instantiate = true } = {}) => {
|
|
36
|
+
if (openaiProviderInstance) return openaiProviderInstance;
|
|
37
|
+
if (!instantiate) return null;
|
|
38
|
+
if (!openaiClient) return null;
|
|
39
|
+
const provider = new OpenAIProvider({ client: openaiClient });
|
|
40
|
+
setOpenAIProvider(provider);
|
|
41
|
+
return provider;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const requireOpenAIProvider = (options) => {
|
|
45
|
+
const provider = getOpenAIProvider(options);
|
|
46
|
+
if (!provider) {
|
|
47
|
+
throw new Error('OpenAI provider not configured. Call configureLLMProvider first.');
|
|
48
|
+
}
|
|
49
|
+
return provider;
|
|
50
|
+
};
|
|
4
51
|
|
|
5
52
|
const resolveAnthropicClient = () => {
|
|
6
53
|
if (anthropicClient) return anthropicClient;
|
|
@@ -21,7 +68,12 @@ const resolveAnthropicClient = () => {
|
|
|
21
68
|
};
|
|
22
69
|
|
|
23
70
|
module.exports = {
|
|
24
|
-
openaiClient
|
|
71
|
+
openaiClient,
|
|
72
|
+
openaiProvider: openaiProviderInstance,
|
|
73
|
+
setOpenAIClient,
|
|
74
|
+
setOpenAIProvider,
|
|
75
|
+
getOpenAIProvider,
|
|
76
|
+
requireOpenAIProvider,
|
|
25
77
|
get anthropicClient() {
|
|
26
78
|
return resolveAnthropicClient();
|
|
27
79
|
}
|
|
@@ -27,7 +27,7 @@ class MessageProvider {
|
|
|
27
27
|
/**
|
|
28
28
|
* Send a message
|
|
29
29
|
* @param {Object} messageData - Message data
|
|
30
|
-
* @param {string} messageData.
|
|
30
|
+
* @param {string} messageData.code - Recipient
|
|
31
31
|
* @param {string} messageData.message - Message text
|
|
32
32
|
* @param {string} messageData.fileUrl - Optional file URL
|
|
33
33
|
* @param {string} messageData.fileType - File type (text, image, document, audio)
|
|
@@ -270,13 +270,7 @@ class NexusMessaging {
|
|
|
270
270
|
throw new Error('No provider initialized');
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
-
|
|
274
|
-
const normalized = { ...messageData };
|
|
275
|
-
if (!normalized.to && normalized.code) {
|
|
276
|
-
normalized.to = normalized.code;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const result = await this.provider.sendMessage(normalized);
|
|
273
|
+
const result = await this.provider.sendMessage(messageData);
|
|
280
274
|
|
|
281
275
|
// Store message only if provider does not handle persistence itself
|
|
282
276
|
const providerStoresMessage = typeof this.provider.supportsMessageStorage === 'function'
|
|
@@ -285,7 +279,7 @@ class NexusMessaging {
|
|
|
285
279
|
|
|
286
280
|
if (this.messageStorage && !providerStoresMessage) {
|
|
287
281
|
await this.messageStorage.saveMessage({
|
|
288
|
-
...
|
|
282
|
+
...messageData,
|
|
289
283
|
messageId: result.messageId,
|
|
290
284
|
provider: result.provider,
|
|
291
285
|
timestamp: new Date(),
|
|
@@ -405,7 +399,7 @@ class NexusMessaging {
|
|
|
405
399
|
|
|
406
400
|
if (response) {
|
|
407
401
|
await this.sendMessage({
|
|
408
|
-
|
|
402
|
+
code: from,
|
|
409
403
|
message: response
|
|
410
404
|
});
|
|
411
405
|
}
|
|
@@ -455,7 +449,7 @@ class NexusMessaging {
|
|
|
455
449
|
|
|
456
450
|
if (response) {
|
|
457
451
|
await this.sendMessage({
|
|
458
|
-
|
|
452
|
+
code: from,
|
|
459
453
|
message: response
|
|
460
454
|
});
|
|
461
455
|
}
|
|
@@ -16,9 +16,8 @@ const mode = process.env.NODE_ENV || 'dev';
|
|
|
16
16
|
|
|
17
17
|
async function checkRunStatus(assistant, thread_id, run_id, retryCount = 0, maxRetries = 30) {
|
|
18
18
|
try {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
const run = await client.beta.threads.runs.retrieve(thread_id, run_id);
|
|
19
|
+
const provider = llmConfig.requireOpenAIProvider();
|
|
20
|
+
const run = await provider.getRun({ conversationId: thread_id, runId: run_id });
|
|
22
21
|
console.log(`Status: ${run.status} ${thread_id} ${run_id} (attempt ${retryCount + 1})`);
|
|
23
22
|
|
|
24
23
|
if (run.status === 'failed' || run.status === 'expired' || run.status === 'incomplete') {
|
|
@@ -49,9 +48,8 @@ async function checkRunStatus(assistant, thread_id, run_id, retryCount = 0, maxR
|
|
|
49
48
|
|
|
50
49
|
async function checkIfFinished(text) {
|
|
51
50
|
try {
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
const completion = await client.chat.completions.create({
|
|
51
|
+
const provider = llmConfig.requireOpenAIProvider();
|
|
52
|
+
const completion = await provider.createChatCompletion({
|
|
55
53
|
model: 'gpt-4o-mini',
|
|
56
54
|
messages: [
|
|
57
55
|
{
|
|
@@ -189,6 +187,7 @@ async function downloadMediaAndCreateFile(code, reply) {
|
|
|
189
187
|
|
|
190
188
|
async function processMessage(code, reply, thread) {
|
|
191
189
|
try {
|
|
190
|
+
const provider = llmConfig.requireOpenAIProvider();
|
|
192
191
|
const formattedMessage = formatMessage(reply);
|
|
193
192
|
const isNotAssistant = !reply.from_me;
|
|
194
193
|
let messagesChat = [];
|
|
@@ -234,9 +233,7 @@ async function processMessage(code, reply, thread) {
|
|
|
234
233
|
});
|
|
235
234
|
} else {
|
|
236
235
|
console.log('Add attachment');
|
|
237
|
-
const
|
|
238
|
-
if (!client) throw new Error('OpenAI client not configured');
|
|
239
|
-
const file = await client.files.create({
|
|
236
|
+
const file = await provider.uploadFile({
|
|
240
237
|
file: fs.createReadStream(fileName),
|
|
241
238
|
purpose: 'vision',
|
|
242
239
|
});
|
|
@@ -246,18 +243,16 @@ async function processMessage(code, reply, thread) {
|
|
|
246
243
|
});
|
|
247
244
|
}
|
|
248
245
|
} else if (fileName.includes('audio')) {
|
|
249
|
-
const
|
|
250
|
-
if (!client) throw new Error('OpenAI client not configured');
|
|
251
|
-
const audioTranscript = await client.audio.transcriptions.create({
|
|
252
|
-
model: 'whisper-1',
|
|
246
|
+
const audioTranscript = await provider.transcribeAudio({
|
|
253
247
|
file: fs.createReadStream(fileName),
|
|
254
|
-
|
|
248
|
+
responseFormat: 'text',
|
|
255
249
|
language: 'es'
|
|
256
250
|
});
|
|
257
|
-
|
|
251
|
+
const transcriptText = audioTranscript?.text || audioTranscript;
|
|
252
|
+
console.log('Inside AUDIO', transcriptText);
|
|
258
253
|
messagesChat.push({
|
|
259
254
|
type: 'text',
|
|
260
|
-
text:
|
|
255
|
+
text: transcriptText,
|
|
261
256
|
});
|
|
262
257
|
}
|
|
263
258
|
}
|
|
@@ -266,9 +261,8 @@ async function processMessage(code, reply, thread) {
|
|
|
266
261
|
console.log('messagesChat', messagesChat);
|
|
267
262
|
console.log('attachments', attachments);
|
|
268
263
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
await client.beta.threads.messages.create(thread.thread_id, {
|
|
264
|
+
await provider.addMessage({
|
|
265
|
+
conversationId: thread.thread_id,
|
|
272
266
|
role: 'user',
|
|
273
267
|
content: messagesChat,
|
|
274
268
|
attachments: attachments
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -131,11 +131,17 @@ class Nexus {
|
|
|
131
131
|
if (llm === 'openai') {
|
|
132
132
|
this.llmProvider = new DefaultLLMProvider(llmConfig);
|
|
133
133
|
try {
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
const providerInstance = typeof this.llmProvider.getProvider === 'function'
|
|
135
|
+
? this.llmProvider.getProvider()
|
|
136
|
+
: null;
|
|
137
|
+
|
|
138
|
+
if (providerInstance && typeof llmConfigModule.setOpenAIProvider === 'function') {
|
|
139
|
+
llmConfigModule.setOpenAIProvider(providerInstance);
|
|
140
|
+
} else if (typeof this.llmProvider.getClient === 'function' && typeof llmConfigModule.setOpenAIClient === 'function') {
|
|
141
|
+
llmConfigModule.setOpenAIClient(this.llmProvider.getClient());
|
|
136
142
|
}
|
|
137
143
|
} catch (err) {
|
|
138
|
-
console.warn('[Nexus] Failed to expose OpenAI
|
|
144
|
+
console.warn('[Nexus] Failed to expose OpenAI provider:', err?.message || err);
|
|
139
145
|
}
|
|
140
146
|
}
|
|
141
147
|
|
|
@@ -163,9 +169,16 @@ class Nexus {
|
|
|
163
169
|
// Configure Assistants (registry + overrides)
|
|
164
170
|
const assistantsConfig = assistantsOpt || assistantOpt;
|
|
165
171
|
try {
|
|
166
|
-
if (this.llmProvider && typeof configureAssistantsLLM === 'function'
|
|
167
|
-
|
|
168
|
-
|
|
172
|
+
if (this.llmProvider && typeof configureAssistantsLLM === 'function') {
|
|
173
|
+
const providerInstance = typeof this.llmProvider.getProvider === 'function'
|
|
174
|
+
? this.llmProvider.getProvider()
|
|
175
|
+
: null;
|
|
176
|
+
|
|
177
|
+
if (providerInstance) {
|
|
178
|
+
configureAssistantsLLM(providerInstance);
|
|
179
|
+
} else if (typeof this.llmProvider.getClient === 'function') {
|
|
180
|
+
configureAssistantsLLM(this.llmProvider.getClient());
|
|
181
|
+
}
|
|
169
182
|
}
|
|
170
183
|
if (assistantsConfig) {
|
|
171
184
|
if (assistantsConfig.registry && typeof assistantsConfig.registry === 'object') {
|
|
@@ -198,7 +211,7 @@ class Nexus {
|
|
|
198
211
|
/**
|
|
199
212
|
* Send a message
|
|
200
213
|
* @param {Object} messageData - Message data
|
|
201
|
-
* @param {string} messageData.
|
|
214
|
+
* @param {string} messageData.code - Recipient phone number
|
|
202
215
|
* @param {string} messageData.message - Message text
|
|
203
216
|
* @param {string} [messageData.fileUrl] - Optional file URL
|
|
204
217
|
* @param {string} [messageData.fileType] - File type
|
|
@@ -216,7 +229,7 @@ class Nexus {
|
|
|
216
229
|
/**
|
|
217
230
|
* Send a scheduled message
|
|
218
231
|
* @param {Object} scheduledMessage - Scheduled message data
|
|
219
|
-
* @param {string} scheduledMessage.
|
|
232
|
+
* @param {string} scheduledMessage.code - Recipient phone number
|
|
220
233
|
* @param {string} scheduledMessage.message - Message text
|
|
221
234
|
* @param {Date|string} scheduledMessage.sendAt - When to send the message
|
|
222
235
|
* @returns {Promise<Object>} Scheduled message result
|
|
@@ -313,6 +326,7 @@ module.exports = {
|
|
|
313
326
|
MongoStorage,
|
|
314
327
|
MessageParser,
|
|
315
328
|
DefaultLLMProvider,
|
|
329
|
+
OpenAIProvider: require('./providers/OpenAIProvider').OpenAIProvider,
|
|
316
330
|
BaseAssistant: CoreBaseAssistant,
|
|
317
331
|
registerAssistant,
|
|
318
332
|
configureAssistantsLLM,
|
package/lib/interactive/index.js
CHANGED
|
@@ -3,7 +3,7 @@ const { toTwilioContent } = require('./twilioMapper');
|
|
|
3
3
|
const { registerFlow, getFlow, listFlows, registerInteractiveHandler, listInteractiveHandlers } = require('./registry');
|
|
4
4
|
|
|
5
5
|
async function sendInteractive(nexusOrMessaging, params) {
|
|
6
|
-
const {
|
|
6
|
+
const { code, spec, id, variables } = params || {};
|
|
7
7
|
if (!nexusOrMessaging) throw new Error('sendInteractive requires a Nexus or NexusMessaging instance');
|
|
8
8
|
const messaging = typeof nexusOrMessaging.getMessaging === 'function' ? nexusOrMessaging.getMessaging() : nexusOrMessaging;
|
|
9
9
|
const provider = typeof messaging.getProvider === 'function' ? messaging.getProvider() : null;
|
|
@@ -14,14 +14,14 @@ async function sendInteractive(nexusOrMessaging, params) {
|
|
|
14
14
|
|
|
15
15
|
// If user supplied a contentSid directly in spec, just send it
|
|
16
16
|
if (useSpec.contentSid) {
|
|
17
|
-
return await provider.sendMessage({
|
|
17
|
+
return await provider.sendMessage({ code, contentSid: useSpec.contentSid, variables });
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
// Twilio mapping
|
|
21
21
|
if (provider.constructor && provider.constructor.name === 'TwilioProvider') {
|
|
22
22
|
const content = toTwilioContent(useSpec);
|
|
23
23
|
const created = await provider.createTemplate(content);
|
|
24
|
-
return await provider.sendMessage({
|
|
24
|
+
return await provider.sendMessage({ code, contentSid: created.sid, variables });
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
// Baileys or others: not supported yet
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
const { OpenAI } = require('openai');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wrapper around the OpenAI SDK that exposes a higher level interface for
|
|
5
|
+
* common operations while remaining compatible with existing callers that
|
|
6
|
+
* expect the raw SDK surface (e.g. `.beta`).
|
|
7
|
+
*/
|
|
8
|
+
class OpenAIProvider {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
const {
|
|
11
|
+
apiKey = process.env.OPENAI_API_KEY,
|
|
12
|
+
organization,
|
|
13
|
+
client,
|
|
14
|
+
defaultModels = {},
|
|
15
|
+
} = options;
|
|
16
|
+
|
|
17
|
+
if (!client && !apiKey) {
|
|
18
|
+
throw new Error('OpenAIProvider requires an API key or a preconfigured client');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
this.client = client || new OpenAI({ apiKey, organization });
|
|
22
|
+
this.defaults = {
|
|
23
|
+
responseModel: 'o4-mini',
|
|
24
|
+
chatModel: 'gpt-4o-mini',
|
|
25
|
+
transcriptionModel: 'whisper-1',
|
|
26
|
+
reasoningEffort: 'medium',
|
|
27
|
+
...defaultModels,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Expose raw SDK sub-clients for backward compatibility
|
|
31
|
+
this.beta = this.client.beta;
|
|
32
|
+
this.responses = this.client.responses;
|
|
33
|
+
this.chat = this.client.chat;
|
|
34
|
+
this.audio = this.client.audio;
|
|
35
|
+
this.files = this.client.files;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getClient() {
|
|
39
|
+
return this.client;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async createConversation({ metadata, messages = [], toolResources } = {}) {
|
|
43
|
+
const thread = await this.client.beta.threads.create({
|
|
44
|
+
metadata,
|
|
45
|
+
tool_resources: toolResources,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (Array.isArray(messages) && messages.length > 0) {
|
|
49
|
+
for (const message of messages) {
|
|
50
|
+
await this.addMessage({ conversationId: thread.id, ...message });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return thread;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async deleteConversation(conversationId) {
|
|
58
|
+
await this.client.beta.threads.del(this._ensureId(conversationId));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async addMessage({ conversationId, role = 'user', content, attachments = [], metadata }) {
|
|
62
|
+
const formattedContent = this._normalizeContent(content);
|
|
63
|
+
|
|
64
|
+
const payload = {
|
|
65
|
+
role,
|
|
66
|
+
content: formattedContent,
|
|
67
|
+
attachments,
|
|
68
|
+
metadata,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
if (payload.metadata && Object.keys(payload.metadata).length === 0) {
|
|
72
|
+
delete payload.metadata;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!payload.attachments || payload.attachments.length === 0) {
|
|
76
|
+
delete payload.attachments;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return this.client.beta.threads.messages.create(this._ensureId(conversationId), payload);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async listMessages({ conversationId, runId, order = 'desc', limit } = {}) {
|
|
83
|
+
return this.client.beta.threads.messages.list(this._ensureId(conversationId), {
|
|
84
|
+
run_id: runId,
|
|
85
|
+
order,
|
|
86
|
+
limit,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async getRunText({
|
|
91
|
+
conversationId,
|
|
92
|
+
runId,
|
|
93
|
+
messageIndex = 0,
|
|
94
|
+
contentIndex = 0,
|
|
95
|
+
fallback = '',
|
|
96
|
+
} = {}) {
|
|
97
|
+
const messages = await this.listMessages({ conversationId, runId });
|
|
98
|
+
const message = messages?.data?.[messageIndex];
|
|
99
|
+
const content = message?.content?.[contentIndex];
|
|
100
|
+
|
|
101
|
+
if (!content) {
|
|
102
|
+
return fallback;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (content?.text?.value) {
|
|
106
|
+
return content.text.value;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (content?.text && typeof content.text === 'string') {
|
|
110
|
+
return content.text;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return fallback;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async runConversation({
|
|
117
|
+
conversationId,
|
|
118
|
+
assistantId,
|
|
119
|
+
instructions,
|
|
120
|
+
additionalMessages = [],
|
|
121
|
+
additionalInstructions,
|
|
122
|
+
metadata,
|
|
123
|
+
topP,
|
|
124
|
+
temperature,
|
|
125
|
+
maxOutputTokens,
|
|
126
|
+
truncationStrategy,
|
|
127
|
+
tools = [],
|
|
128
|
+
} = {}) {
|
|
129
|
+
const payload = {
|
|
130
|
+
assistant_id: assistantId,
|
|
131
|
+
instructions,
|
|
132
|
+
additional_messages: additionalMessages,
|
|
133
|
+
additional_instructions: additionalInstructions,
|
|
134
|
+
metadata,
|
|
135
|
+
top_p: topP,
|
|
136
|
+
temperature,
|
|
137
|
+
max_output_tokens: maxOutputTokens,
|
|
138
|
+
truncation_strategy: truncationStrategy,
|
|
139
|
+
tools,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
if (Array.isArray(payload.additional_messages) && payload.additional_messages.length === 0) {
|
|
143
|
+
delete payload.additional_messages;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (Array.isArray(payload.tools) && payload.tools.length === 0) {
|
|
147
|
+
delete payload.tools;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (payload.metadata && Object.keys(payload.metadata).length === 0) {
|
|
151
|
+
delete payload.metadata;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
Object.keys(payload).forEach((key) => {
|
|
155
|
+
if (payload[key] === undefined || payload[key] === null) {
|
|
156
|
+
delete payload[key];
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return this.client.beta.threads.runs.create(this._ensureId(conversationId), payload);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async getRun({ conversationId, runId }) {
|
|
164
|
+
return this.client.beta.threads.runs.retrieve(this._ensureId(conversationId), this._ensureId(runId));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async listRuns({ conversationId, limit, order = 'desc' } = {}) {
|
|
168
|
+
return this.client.beta.threads.runs.list(this._ensureId(conversationId), {
|
|
169
|
+
limit,
|
|
170
|
+
order,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async submitToolOutputs({ conversationId, runId, toolOutputs }) {
|
|
175
|
+
return this.client.beta.threads.runs.submitToolOutputs(
|
|
176
|
+
this._ensureId(conversationId),
|
|
177
|
+
this._ensureId(runId),
|
|
178
|
+
{ tool_outputs: toolOutputs }
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async cancelRun({ conversationId, runId }) {
|
|
183
|
+
return this.client.beta.threads.runs.cancel(
|
|
184
|
+
this._ensureId(conversationId),
|
|
185
|
+
this._ensureId(runId)
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async runPrompt({
|
|
190
|
+
model,
|
|
191
|
+
input,
|
|
192
|
+
instructions,
|
|
193
|
+
reasoningEffort = this.defaults.reasoningEffort,
|
|
194
|
+
responseFormat,
|
|
195
|
+
metadata,
|
|
196
|
+
temperature,
|
|
197
|
+
maxOutputTokens,
|
|
198
|
+
tools,
|
|
199
|
+
} = {}) {
|
|
200
|
+
const payload = {
|
|
201
|
+
model: model || this.defaults.responseModel,
|
|
202
|
+
input,
|
|
203
|
+
instructions,
|
|
204
|
+
reasoning: reasoningEffort ? { effort: reasoningEffort } : undefined,
|
|
205
|
+
text: responseFormat ? { format: responseFormat } : undefined,
|
|
206
|
+
metadata,
|
|
207
|
+
temperature,
|
|
208
|
+
max_output_tokens: maxOutputTokens,
|
|
209
|
+
tools,
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
if (Array.isArray(payload.tools) && payload.tools.length === 0) {
|
|
213
|
+
delete payload.tools;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (payload.metadata && Object.keys(payload.metadata).length === 0) {
|
|
217
|
+
delete payload.metadata;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
Object.keys(payload).forEach((key) => {
|
|
221
|
+
if (payload[key] === undefined || payload[key] === null) {
|
|
222
|
+
delete payload[key];
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
return this.client.responses.create(payload);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async createChatCompletion({ model, messages, temperature, maxTokens, topP, metadata, responseFormat } = {}) {
|
|
230
|
+
return this.client.chat.completions.create({
|
|
231
|
+
model: model || this.defaults.chatModel,
|
|
232
|
+
messages,
|
|
233
|
+
temperature,
|
|
234
|
+
max_tokens: maxTokens,
|
|
235
|
+
top_p: topP,
|
|
236
|
+
metadata,
|
|
237
|
+
response_format: responseFormat,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async uploadFile({ file, purpose }) {
|
|
242
|
+
if (!file) {
|
|
243
|
+
throw new Error('uploadFile requires a readable file stream or object');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return this.client.files.create({ file, purpose: purpose || 'assistants' });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async transcribeAudio({ file, model, language, responseFormat, temperature, prompt } = {}) {
|
|
250
|
+
return this.client.audio.transcriptions.create({
|
|
251
|
+
model: model || this.defaults.transcriptionModel,
|
|
252
|
+
file,
|
|
253
|
+
language,
|
|
254
|
+
response_format: responseFormat,
|
|
255
|
+
temperature,
|
|
256
|
+
prompt,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
_normalizeContent(content) {
|
|
261
|
+
if (content === undefined || content === null) {
|
|
262
|
+
return [{ type: 'text', text: '' }];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (Array.isArray(content)) {
|
|
266
|
+
return content.map((item) => {
|
|
267
|
+
if (typeof item === 'string') {
|
|
268
|
+
return { type: 'text', text: item };
|
|
269
|
+
}
|
|
270
|
+
return item;
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (typeof content === 'string') {
|
|
275
|
+
return [{ type: 'text', text: content }];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return [content];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
_ensureId(value) {
|
|
282
|
+
if (!value) {
|
|
283
|
+
throw new Error('Identifier value is required');
|
|
284
|
+
}
|
|
285
|
+
if (typeof value === 'string') {
|
|
286
|
+
return value;
|
|
287
|
+
}
|
|
288
|
+
if (typeof value === 'object' && value.id) {
|
|
289
|
+
return value.id;
|
|
290
|
+
}
|
|
291
|
+
throw new Error('Unable to resolve identifier value');
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
module.exports = {
|
|
296
|
+
OpenAIProvider,
|
|
297
|
+
};
|
|
@@ -5,10 +5,21 @@ const { addRecord } = require('../services/airtableService.js');
|
|
|
5
5
|
const runtimeConfig = require('../config/runtimeConfig');
|
|
6
6
|
const llmConfig = require('../config/llmConfig');
|
|
7
7
|
const { BaseAssistant } = require('../assistants/BaseAssistant');
|
|
8
|
+
const { OpenAIProvider } = require('../providers/OpenAIProvider');
|
|
8
9
|
|
|
9
|
-
let llmProvider = null;
|
|
10
10
|
const configureLLMProvider = (provider) => {
|
|
11
|
-
|
|
11
|
+
if (!provider) {
|
|
12
|
+
throw new Error('configureLLMProvider requires an OpenAI provider or raw client');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (provider instanceof OpenAIProvider || typeof provider.runConversation === 'function') {
|
|
16
|
+
llmConfig.setOpenAIProvider(provider);
|
|
17
|
+
return provider;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const wrappedProvider = new OpenAIProvider({ client: provider });
|
|
21
|
+
llmConfig.setOpenAIProvider(wrappedProvider);
|
|
22
|
+
return wrappedProvider;
|
|
12
23
|
};
|
|
13
24
|
|
|
14
25
|
let assistantConfig = null;
|
|
@@ -29,6 +40,60 @@ const configureAssistants = (config) => {
|
|
|
29
40
|
assistantConfig = config;
|
|
30
41
|
};
|
|
31
42
|
|
|
43
|
+
const runAssistantAndWait = async ({
|
|
44
|
+
thread,
|
|
45
|
+
assistant,
|
|
46
|
+
runConfig = {}
|
|
47
|
+
}) => {
|
|
48
|
+
if (!thread || !thread.thread_id) {
|
|
49
|
+
throw new Error('runAssistantAndWait requires a thread with a valid thread_id');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!assistant) {
|
|
53
|
+
throw new Error('runAssistantAndWait requires an assistant instance');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const provider = llmConfig.requireOpenAIProvider();
|
|
57
|
+
const { polling, ...conversationConfig } = runConfig || {};
|
|
58
|
+
|
|
59
|
+
const run = await provider.runConversation({
|
|
60
|
+
conversationId: thread.thread_id,
|
|
61
|
+
assistantId: thread.assistant_id,
|
|
62
|
+
...conversationConfig,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const filter = thread.code ? { code: thread.code, active: true } : null;
|
|
66
|
+
if (filter) {
|
|
67
|
+
await Thread.updateOne(filter, { $set: { run_id: run.id } });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const maxRetries = polling?.maxRetries ?? 30;
|
|
71
|
+
let completed = false;
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
completed = await checkRunStatus(assistant, run.thread_id, run.id, 0, maxRetries);
|
|
75
|
+
} finally {
|
|
76
|
+
if (filter) {
|
|
77
|
+
await Thread.updateOne(filter, { $set: { run_id: null } });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let finalRun = run;
|
|
82
|
+
try {
|
|
83
|
+
finalRun = await provider.getRun({ conversationId: run.thread_id, runId: run.id });
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.warn('Warning: unable to retrieve final run state:', error?.message || error);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!completed) {
|
|
89
|
+
return { run: finalRun, completed: false, output: '' };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const output = await provider.getRunText({ conversationId: run.thread_id, runId: run.id, fallback: '' });
|
|
93
|
+
|
|
94
|
+
return { run: finalRun, completed: true, output };
|
|
95
|
+
};
|
|
96
|
+
|
|
32
97
|
const registerAssistant = (assistantId, definition) => {
|
|
33
98
|
if (!assistantId || typeof assistantId !== 'string') {
|
|
34
99
|
throw new Error('registerAssistant requires a string assistantId');
|
|
@@ -49,10 +114,17 @@ const registerAssistant = (assistantId, definition) => {
|
|
|
49
114
|
|
|
50
115
|
class ConfiguredAssistant extends ParentClass {
|
|
51
116
|
constructor(options = {}) {
|
|
117
|
+
const provider = options.provider || llmConfig.getOpenAIProvider({ instantiate: false });
|
|
118
|
+
const sharedClient = options.client
|
|
119
|
+
|| provider?.getClient?.()
|
|
120
|
+
|| llmConfig.openaiClient
|
|
121
|
+
|| null;
|
|
122
|
+
|
|
52
123
|
super({
|
|
53
124
|
...options,
|
|
54
125
|
assistantId,
|
|
55
|
-
client:
|
|
126
|
+
client: sharedClient,
|
|
127
|
+
provider,
|
|
56
128
|
tools: [...tools, ...(options.tools || [])]
|
|
57
129
|
});
|
|
58
130
|
|
|
@@ -100,20 +172,27 @@ const getAssistantById = (assistant_id, thread) => {
|
|
|
100
172
|
throw new Error(`Assistant '${assistant_id}' not found. Available assistants: ${Object.keys(assistantRegistry).join(', ')}`);
|
|
101
173
|
}
|
|
102
174
|
|
|
103
|
-
const
|
|
175
|
+
const provider = llmConfig.getOpenAIProvider({ instantiate: false });
|
|
176
|
+
const sharedClient = provider?.getClient?.() || llmConfig.openaiClient || null;
|
|
104
177
|
|
|
105
178
|
if (AssistantClass.prototype instanceof BaseAssistant) {
|
|
106
179
|
return new AssistantClass({
|
|
107
180
|
assistantId: assistant_id,
|
|
108
181
|
thread,
|
|
109
|
-
client: sharedClient
|
|
182
|
+
client: sharedClient,
|
|
183
|
+
provider
|
|
110
184
|
});
|
|
111
185
|
}
|
|
112
186
|
|
|
113
187
|
try {
|
|
114
188
|
return new AssistantClass(thread);
|
|
115
189
|
} catch (error) {
|
|
116
|
-
return new AssistantClass({
|
|
190
|
+
return new AssistantClass({
|
|
191
|
+
thread,
|
|
192
|
+
assistantId: assistant_id,
|
|
193
|
+
client: sharedClient,
|
|
194
|
+
provider
|
|
195
|
+
});
|
|
117
196
|
}
|
|
118
197
|
};
|
|
119
198
|
|
|
@@ -135,10 +214,13 @@ const createAssistant = async (code, assistant_id, messages=[], prevThread=null)
|
|
|
135
214
|
const initialThread = await assistant.create(code, curRow[0]);
|
|
136
215
|
|
|
137
216
|
// Add new messages to memory
|
|
217
|
+
const provider = llmConfig.requireOpenAIProvider();
|
|
138
218
|
for (const message of messages) {
|
|
139
|
-
await
|
|
140
|
-
initialThread.id,
|
|
141
|
-
|
|
219
|
+
await provider.addMessage({
|
|
220
|
+
conversationId: initialThread.id,
|
|
221
|
+
role: 'assistant',
|
|
222
|
+
content: message
|
|
223
|
+
});
|
|
142
224
|
}
|
|
143
225
|
|
|
144
226
|
// Define new thread data
|
|
@@ -159,7 +241,7 @@ const createAssistant = async (code, assistant_id, messages=[], prevThread=null)
|
|
|
159
241
|
|
|
160
242
|
// Delete previous thread
|
|
161
243
|
if (prevThread) {
|
|
162
|
-
await
|
|
244
|
+
await provider.deleteConversation(prevThread.thread_id);
|
|
163
245
|
}
|
|
164
246
|
|
|
165
247
|
return thread;
|
|
@@ -171,32 +253,24 @@ const addMsgAssistant = async (code, inMessages, reply = false) => {
|
|
|
171
253
|
console.log(thread);
|
|
172
254
|
if (thread === null) return null;
|
|
173
255
|
|
|
256
|
+
const provider = llmConfig.requireOpenAIProvider();
|
|
257
|
+
|
|
174
258
|
for (const message of inMessages) {
|
|
175
259
|
console.log(message);
|
|
176
|
-
await
|
|
177
|
-
thread.thread_id,
|
|
178
|
-
|
|
260
|
+
await provider.addMessage({
|
|
261
|
+
conversationId: thread.thread_id,
|
|
262
|
+
role: 'assistant',
|
|
263
|
+
content: message
|
|
264
|
+
});
|
|
179
265
|
}
|
|
180
266
|
|
|
181
267
|
if (!reply) return null;
|
|
182
268
|
|
|
183
269
|
const assistant = getAssistantById(thread.assistant_id, thread);
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
{
|
|
187
|
-
assistant_id: thread.assistant_id
|
|
188
|
-
}
|
|
189
|
-
);
|
|
190
|
-
|
|
191
|
-
await Thread.updateOne({ code: thread.code, active: true }, { $set: { run_id: run.id } });
|
|
192
|
-
await checkRunStatus(assistant, run.thread_id, run.id);
|
|
193
|
-
await Thread.updateOne({ code: thread.code, active: true }, { $set: { run_id: null } });
|
|
270
|
+
const { output } = await runAssistantAndWait({ thread, assistant });
|
|
271
|
+
console.log('THE ANS IS', output);
|
|
194
272
|
|
|
195
|
-
|
|
196
|
-
const ans = messages.data[0].content[0].text.value;
|
|
197
|
-
console.log('THE ANS IS', ans);
|
|
198
|
-
|
|
199
|
-
return ans;
|
|
273
|
+
return output;
|
|
200
274
|
} catch (error) {
|
|
201
275
|
console.log(error);
|
|
202
276
|
return null;
|
|
@@ -210,25 +284,19 @@ const addInsAssistant = async (code, instruction) => {
|
|
|
210
284
|
if (thread === null) return null;
|
|
211
285
|
|
|
212
286
|
const assistant = getAssistantById(thread.assistant_id, thread);
|
|
213
|
-
const
|
|
214
|
-
thread
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
287
|
+
const { output } = await runAssistantAndWait({
|
|
288
|
+
thread,
|
|
289
|
+
assistant,
|
|
290
|
+
runConfig: {
|
|
291
|
+
additionalInstructions: instruction,
|
|
292
|
+
additionalMessages: [
|
|
218
293
|
{ role: 'user', content: instruction }
|
|
219
294
|
]
|
|
220
295
|
}
|
|
221
|
-
);
|
|
222
|
-
|
|
223
|
-
await Thread.updateOne({ code: thread.code, active: true }, { $set: { run_id: run.id } });
|
|
224
|
-
await checkRunStatus(assistant, run.thread_id, run.id);
|
|
225
|
-
await Thread.updateOne({ code: thread.code, active: true }, { $set: { run_id: null } });
|
|
296
|
+
});
|
|
297
|
+
console.log('RUN RESPONSE', output);
|
|
226
298
|
|
|
227
|
-
|
|
228
|
-
console.log(messages.data[0].content);
|
|
229
|
-
const ans = messages.data[0].content[0].text.value;
|
|
230
|
-
|
|
231
|
-
return ans;
|
|
299
|
+
return output;
|
|
232
300
|
} catch (error) {
|
|
233
301
|
console.log(error);
|
|
234
302
|
return null;
|
|
@@ -250,9 +318,11 @@ const getThread = async (code, message = null) => {
|
|
|
250
318
|
return null;
|
|
251
319
|
}
|
|
252
320
|
|
|
321
|
+
const provider = llmConfig.getOpenAIProvider({ instantiate: false });
|
|
253
322
|
while (thread && thread.run_id) {
|
|
254
323
|
console.log(`Wait for ${thread.run_id} to be executed`);
|
|
255
|
-
const
|
|
324
|
+
const activeProvider = provider || llmConfig.requireOpenAIProvider();
|
|
325
|
+
const run = await activeProvider.getRun({ conversationId: thread.thread_id, runId: thread.run_id });
|
|
256
326
|
if (run.status === 'cancelled' || run.status === 'expired' || run.status === 'completed') {
|
|
257
327
|
await Thread.updateOne({ code: code }, { $set: { run_id: null } });
|
|
258
328
|
}
|
|
@@ -283,18 +353,22 @@ const replyAssistant = async function (code, message_ = null, thread_ = null, ru
|
|
|
283
353
|
console.log('THREAD STOPPED', code, thread?.active);
|
|
284
354
|
if (!thread || !thread.active) return null;
|
|
285
355
|
|
|
286
|
-
const patientReply = await getLastMessages(code);
|
|
356
|
+
const patientReply = message_ ? [message_] : await getLastMessages(code);
|
|
287
357
|
console.log('UNREAD DATA', patientReply);
|
|
288
358
|
if (!patientReply) {
|
|
289
359
|
console.log('No relevant data found for this assistant.');
|
|
290
360
|
return null;
|
|
291
361
|
}
|
|
292
362
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
363
|
+
const provider = llmConfig.requireOpenAIProvider();
|
|
364
|
+
|
|
365
|
+
let activeRuns = await provider.listRuns({ conversationId: thread.thread_id });
|
|
366
|
+
let activeRunsCount = activeRuns?.data?.length || 0;
|
|
367
|
+
console.log('ACTIVE RUNS:', activeRunsCount);
|
|
368
|
+
while (activeRunsCount > 0) {
|
|
296
369
|
console.log(`ACTIVE RUNS ${thread.thread_id}`);
|
|
297
|
-
activeRuns = await
|
|
370
|
+
activeRuns = await provider.listRuns({ conversationId: thread.thread_id });
|
|
371
|
+
activeRunsCount = activeRuns?.data?.length || 0;
|
|
298
372
|
await delay(5000);
|
|
299
373
|
}
|
|
300
374
|
|
|
@@ -335,27 +409,18 @@ const replyAssistant = async function (code, message_ = null, thread_ = null, ru
|
|
|
335
409
|
if (!patientMsg || !thread || thread?.stopped) return null;
|
|
336
410
|
|
|
337
411
|
const assistant = getAssistantById(thread.assistant_id, thread);
|
|
338
|
-
|
|
339
|
-
thread.thread_id,
|
|
340
|
-
{
|
|
341
|
-
assistant_id: thread.assistant_id,
|
|
342
|
-
...runOptions
|
|
343
|
-
}
|
|
344
|
-
);
|
|
345
|
-
console.log('RUN LAST ERROR:', run.last_error);
|
|
346
|
-
|
|
347
|
-
assistant.set_replies(patientReply);
|
|
348
|
-
|
|
349
|
-
await Thread.updateOne({ code: thread.code, active: true }, { $set: { run_id: run.id } });
|
|
350
|
-
const runStatus = await checkRunStatus(assistant, run.thread_id, run.id);
|
|
351
|
-
console.log('RUN STATUS', runStatus);
|
|
352
|
-
await Thread.updateOne({ code: thread.code, active: true }, { $set: { run_id: null } });
|
|
412
|
+
assistant.setReplies(patientReply);
|
|
353
413
|
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
414
|
+
const { run, output, completed } = await runAssistantAndWait({
|
|
415
|
+
thread,
|
|
416
|
+
assistant,
|
|
417
|
+
runConfig: runOptions
|
|
418
|
+
});
|
|
419
|
+
console.log('RUN LAST ERROR:', run?.last_error);
|
|
420
|
+
console.log('RUN STATUS', completed);
|
|
421
|
+
console.log(output);
|
|
357
422
|
|
|
358
|
-
return
|
|
423
|
+
return output;
|
|
359
424
|
} catch (err) {
|
|
360
425
|
console.log(`Error inside reply assistant ${err} ${code}`);
|
|
361
426
|
}
|
|
@@ -386,5 +451,6 @@ module.exports = {
|
|
|
386
451
|
configureAssistants,
|
|
387
452
|
registerAssistant,
|
|
388
453
|
configureLLMProvider,
|
|
389
|
-
overrideGetAssistantById
|
|
454
|
+
overrideGetAssistantById,
|
|
455
|
+
runAssistantAndWait
|
|
390
456
|
};
|
|
@@ -62,7 +62,7 @@ class MongoStorage {
|
|
|
62
62
|
async saveMessage(messageData) {
|
|
63
63
|
try {
|
|
64
64
|
console.log('[MongoStorage] saveMessage called', {
|
|
65
|
-
|
|
65
|
+
code: messageData?.to || messageData?.code || messageData?.numero,
|
|
66
66
|
from: messageData?.from,
|
|
67
67
|
provider: messageData?.provider || 'unknown',
|
|
68
68
|
hasRaw: Boolean(messageData?.raw),
|
|
@@ -1,21 +1,19 @@
|
|
|
1
|
-
const
|
|
1
|
+
const { OpenAIProvider } = require('../providers/OpenAIProvider');
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Default LLM Provider using OpenAI
|
|
5
5
|
*/
|
|
6
6
|
class DefaultLLMProvider {
|
|
7
7
|
constructor(config = {}) {
|
|
8
|
-
|
|
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
|
-
}
|
|
8
|
+
this.provider = new OpenAIProvider(config);
|
|
9
|
+
}
|
|
13
10
|
|
|
14
|
-
|
|
11
|
+
getProvider() {
|
|
12
|
+
return this.provider;
|
|
15
13
|
}
|
|
16
14
|
|
|
17
15
|
getClient() {
|
|
18
|
-
return this.
|
|
16
|
+
return this.provider.getClient();
|
|
19
17
|
}
|
|
20
18
|
}
|
|
21
19
|
|