@peopl-health/nexus 2.0.17 → 2.1.0
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/examples/assistants/ExampleAssistant.js +0 -20
- package/lib/assistants/BaseAssistant.js +0 -126
- package/lib/controllers/assistantController.js +2 -1
- package/lib/helpers/assistantHelper.js +2 -51
- package/lib/index.d.ts +0 -1
- package/lib/models/threadModel.js +30 -0
- package/lib/providers/OpenAIAssistantsProvider.js +78 -0
- package/lib/providers/OpenAIResponsesProvider.js +132 -48
- package/lib/services/assistantService.js +42 -74
- package/package.json +2 -2
|
@@ -86,26 +86,6 @@ class ExampleAssistant extends BaseAssistant {
|
|
|
86
86
|
};
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
/**
|
|
90
|
-
* Handle incoming messages with custom logic
|
|
91
|
-
* @param {string} userId - User identifier
|
|
92
|
-
* @param {string} message - User message
|
|
93
|
-
* @returns {Promise<string>} Response
|
|
94
|
-
*/
|
|
95
|
-
async handleMessage(userId, message) {
|
|
96
|
-
// Add custom pre-processing logic here
|
|
97
|
-
console.log(`ExampleAssistant handling message from ${userId}: ${message}`);
|
|
98
|
-
|
|
99
|
-
// Call parent method to handle LLM interaction
|
|
100
|
-
const response = await this.sendMessage(userId, message);
|
|
101
|
-
|
|
102
|
-
// Add custom post-processing logic here
|
|
103
|
-
if (response) {
|
|
104
|
-
console.log(`ExampleAssistant responding to ${userId}: ${response}`);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return response;
|
|
108
|
-
}
|
|
109
89
|
}
|
|
110
90
|
|
|
111
91
|
module.exports = { ExampleAssistant };
|
|
@@ -159,94 +159,6 @@ class BaseAssistant {
|
|
|
159
159
|
return await this.create(code, context);
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
async sendMessage(userId, message, options = {}) {
|
|
163
|
-
this._ensureClient();
|
|
164
|
-
|
|
165
|
-
if (!this.thread || !this.thread.thread_id) {
|
|
166
|
-
throw new Error('Assistant thread not initialized. Call create() before sendMessage().');
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const provider = this.provider || null;
|
|
170
|
-
if (!provider || typeof provider.addMessage !== 'function') {
|
|
171
|
-
throw new Error('Provider not configured. Ensure configureLLMProvider has been called.');
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const assistantId = this.assistantId;
|
|
175
|
-
const threadId = this.thread.thread_id;
|
|
176
|
-
await provider.addMessage({ threadId, role: 'user', content: message });
|
|
177
|
-
|
|
178
|
-
const runConfig = { threadId, assistantId, ...options };
|
|
179
|
-
|
|
180
|
-
const toolSchemas = this.getToolSchemas();
|
|
181
|
-
if (toolSchemas.length > 0) {
|
|
182
|
-
runConfig.tools = toolSchemas;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const run = await provider.runConversation(runConfig);
|
|
186
|
-
|
|
187
|
-
return await this.waitForCompletion(threadId, run.id, options);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
async waitForCompletion(threadId, runId, { interval = 2000, maxAttempts = 30 } = {}) {
|
|
191
|
-
this._ensureClient();
|
|
192
|
-
const provider = this.provider || null;
|
|
193
|
-
if (!provider || typeof provider.getRun !== 'function') {
|
|
194
|
-
throw new Error('Provider not configured. Cannot poll run status.');
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
let attempts = 0;
|
|
198
|
-
while (attempts < maxAttempts) {
|
|
199
|
-
const run = await provider.getRun({ threadId, runId });
|
|
200
|
-
|
|
201
|
-
if (!run) {
|
|
202
|
-
throw new Error('Unable to retrieve run status.');
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (run.status === 'completed') {
|
|
206
|
-
return run;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (run.status === 'requires_action') {
|
|
210
|
-
await this.handleRequiresAction(run);
|
|
211
|
-
} else if (['failed', 'cancelled', 'expired', 'incomplete', 'errored'].includes(run.status)) {
|
|
212
|
-
throw new Error(`Assistant run ended with status '${run.status}'`);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
216
|
-
attempts += 1;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
throw new Error('Assistant run timeout');
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
async getPreviousMessages(threadDoc) {
|
|
223
|
-
this._ensureClient();
|
|
224
|
-
const threadRef = threadDoc || this.thread;
|
|
225
|
-
if (!threadRef) return [];
|
|
226
|
-
|
|
227
|
-
const provider = this.provider || null;
|
|
228
|
-
if (!provider || typeof provider.listMessages !== 'function') {
|
|
229
|
-
throw new Error('OpenAI provider not configured. Cannot list messages.');
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const response = await provider.listMessages({ threadId: threadRef.thread_id, order: 'asc' });
|
|
233
|
-
const messages = Array.isArray(response?.data) ? response.data : Array.isArray(response?.items) ? response.items : [];
|
|
234
|
-
|
|
235
|
-
return messages.map((msg) => {
|
|
236
|
-
const parts = Array.isArray(msg.content) ? msg.content : [];
|
|
237
|
-
const textContents = parts
|
|
238
|
-
.map((part) => {
|
|
239
|
-
if (part.type === 'text' && part.text?.value) return part.text.value;
|
|
240
|
-
if ((part.type === 'input_text' || part.type === 'output_text') && part.text) return part.text;
|
|
241
|
-
return null;
|
|
242
|
-
})
|
|
243
|
-
.filter(Boolean);
|
|
244
|
-
|
|
245
|
-
const content = textContents.length <= 1 ? textContents[0] || '' : textContents;
|
|
246
|
-
return { role: msg.role || 'user', content };
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
|
|
250
162
|
async create(code, context = {}) {
|
|
251
163
|
this._ensureClient();
|
|
252
164
|
this.status = 'active';
|
|
@@ -285,44 +197,6 @@ class BaseAssistant {
|
|
|
285
197
|
}];
|
|
286
198
|
}
|
|
287
199
|
|
|
288
|
-
async handleRequiresAction(run) {
|
|
289
|
-
const toolCalls = run?.required_action?.submit_tool_outputs?.tool_calls || [];
|
|
290
|
-
if (toolCalls.length === 0) {
|
|
291
|
-
return [];
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const outputs = [];
|
|
295
|
-
|
|
296
|
-
for (const call of toolCalls) {
|
|
297
|
-
try {
|
|
298
|
-
const name = call.function?.name;
|
|
299
|
-
const args = call.function?.arguments ? JSON.parse(call.function.arguments) : {};
|
|
300
|
-
const result = await this.executeTool(name, args);
|
|
301
|
-
outputs.push({ tool_call_id: call.id, output: typeof result === 'string' ? result : JSON.stringify(result) });
|
|
302
|
-
} catch (error) {
|
|
303
|
-
console.error('[BaseAssistant] Tool execution failed', error);
|
|
304
|
-
outputs.push({
|
|
305
|
-
tool_call_id: call.id,
|
|
306
|
-
output: JSON.stringify({ success: false, error: error?.message || 'Tool execution failed' })
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const provider = this.provider || null;
|
|
312
|
-
if (!provider || typeof provider.submitToolOutputs !== 'function') {
|
|
313
|
-
console.warn('[BaseAssistant] Cannot submit tool outputs: provider not configured');
|
|
314
|
-
return outputs;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
await provider.submitToolOutputs({
|
|
318
|
-
threadId: run.thread_id || this.thread?.thread_id,
|
|
319
|
-
runId: run.id,
|
|
320
|
-
toolOutputs: outputs
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
return outputs;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
200
|
async close() {
|
|
327
201
|
this.status = 'closed';
|
|
328
202
|
const assistantId = process.env.VARIANT === 'responses' ? this.thread?.prompt_id : this.thread?.assistant_id;
|
|
@@ -88,7 +88,8 @@ const listAssistantController = async (req, res) => {
|
|
|
88
88
|
const nodeEnv = process.env.NODE_ENV;
|
|
89
89
|
const airtableStatus = nodeEnv === 'production' ? 'prod' :
|
|
90
90
|
nodeEnv === 'development' ? 'dev' : nodeEnv;
|
|
91
|
-
const
|
|
91
|
+
const tableName = process.env.VARIANT === 'responses' ? 'prompts' : 'assistants';
|
|
92
|
+
const assistants = await getRecordByFilter(Config_ID, tableName, `status="${airtableStatus}"`);
|
|
92
93
|
return res.status(200).send({ message: 'List assistants' , assistants});
|
|
93
94
|
} catch (error) {
|
|
94
95
|
console.log(error);
|
|
@@ -7,7 +7,6 @@ const { convertPdfToImages } = require('./filesHelper.js');
|
|
|
7
7
|
const { analyzeImage } = require('../helpers/llmsHelper.js');
|
|
8
8
|
|
|
9
9
|
const { getRecordByFilter } = require('../services/airtableService.js');
|
|
10
|
-
const { createProvider } = require('../providers/createProvider');
|
|
11
10
|
|
|
12
11
|
const fs = require('fs');
|
|
13
12
|
const path = require('path');
|
|
@@ -15,51 +14,6 @@ const moment = require('moment-timezone');
|
|
|
15
14
|
|
|
16
15
|
const mode = process.env.NODE_ENV || 'dev';
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
async function checkRunStatus(assistant, thread_id, run_id, retryCount = 0, maxRetries = 10, actionHandled = false) {
|
|
20
|
-
try {
|
|
21
|
-
const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
|
|
22
|
-
const run = await provider.getRun({ threadId: thread_id, runId: run_id });
|
|
23
|
-
console.log(`Status: ${run.status} ${thread_id} ${run_id} (attempt ${retryCount + 1})`);
|
|
24
|
-
|
|
25
|
-
const failedStatuses = ['failed', 'expired', 'incomplete', 'errored'];
|
|
26
|
-
if (failedStatuses.includes(run.status)) {
|
|
27
|
-
console.log(`Run failed. ${run.status} `);
|
|
28
|
-
console.log('Error:');
|
|
29
|
-
console.log(run);
|
|
30
|
-
return false;
|
|
31
|
-
} else if (run.status === 'cancelled') {
|
|
32
|
-
console.log('cancelled');
|
|
33
|
-
return true;
|
|
34
|
-
} else if (run.status === 'requires_action') {
|
|
35
|
-
console.log('requires_action');
|
|
36
|
-
if (retryCount >= maxRetries) {
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
if (!actionHandled) {
|
|
40
|
-
await assistant.handleRequiresAction(run);
|
|
41
|
-
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
42
|
-
return checkRunStatus(assistant, thread_id, run_id, retryCount + 1, maxRetries, true);
|
|
43
|
-
} else {
|
|
44
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
45
|
-
return checkRunStatus(assistant, thread_id, run_id, retryCount + 1, maxRetries, actionHandled);
|
|
46
|
-
}
|
|
47
|
-
} else if (!['completed', 'succeeded'].includes(run.status)) {
|
|
48
|
-
if (retryCount >= maxRetries) {
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
52
|
-
return checkRunStatus(assistant, thread_id, run_id, retryCount + 1, maxRetries, actionHandled);
|
|
53
|
-
} else {
|
|
54
|
-
console.log('Run completed.');
|
|
55
|
-
return true;
|
|
56
|
-
}
|
|
57
|
-
} catch (error) {
|
|
58
|
-
console.error('Error checking run status:', error);
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
17
|
async function checkIfFinished(text) {
|
|
64
18
|
try {
|
|
65
19
|
const provider = llmConfig.requireOpenAIProvider();
|
|
@@ -292,8 +246,7 @@ async function processIndividualMessage(code, reply, provider, thread) {
|
|
|
292
246
|
|
|
293
247
|
console.log('[processIndividualMessage] messagesChat', messagesChat);
|
|
294
248
|
|
|
295
|
-
|
|
296
|
-
const threadId = process.env.VARIANT === 'responses' ? thread?.conversation_id : thread?.thread_id;
|
|
249
|
+
const threadId = thread.getConversationId();
|
|
297
250
|
if (isNotAssistant) {
|
|
298
251
|
await provider.addMessage({
|
|
299
252
|
threadId,
|
|
@@ -303,10 +256,9 @@ async function processIndividualMessage(code, reply, provider, thread) {
|
|
|
303
256
|
console.log(`[processIndividualMessage] User message added to thread ${threadId}`);
|
|
304
257
|
}
|
|
305
258
|
|
|
306
|
-
const assistantId = process.env.VARIANT === 'responses' ? thread.prompt_id : thread.assistant_id;
|
|
307
259
|
await Message.updateOne(
|
|
308
260
|
{ message_id: reply.message_id, timestamp: reply.timestamp },
|
|
309
|
-
{ $set: { assistant_id:
|
|
261
|
+
{ $set: { assistant_id: thread.getAssistantId(), thread_id: threadId } }
|
|
310
262
|
);
|
|
311
263
|
|
|
312
264
|
return {isNotAssistant, url};
|
|
@@ -337,7 +289,6 @@ function getCurRow(baseID, code) {
|
|
|
337
289
|
}
|
|
338
290
|
|
|
339
291
|
module.exports = {
|
|
340
|
-
checkRunStatus,
|
|
341
292
|
checkIfFinished,
|
|
342
293
|
getLastMessages,
|
|
343
294
|
getLastNMessages,
|
package/lib/index.d.ts
CHANGED
|
@@ -159,7 +159,6 @@ declare module '@peopl-health/nexus' {
|
|
|
159
159
|
waitForCompletion(threadId: string, runId: string, opts?: { interval?: number; maxAttempts?: number }): Promise<any>;
|
|
160
160
|
create(code: string, context?: any): Promise<any>;
|
|
161
161
|
buildInitialMessages(context: { code: string; [key: string]: any }): Promise<Array<{ role: string; content: string }>>;
|
|
162
|
-
handleRequiresAction(run: any): Promise<any[]>;
|
|
163
162
|
close(): Promise<string>;
|
|
164
163
|
setThread(thread: any): void;
|
|
165
164
|
setReplies(replies: any): void;
|
|
@@ -18,6 +18,36 @@ threadSchema.index({ code: 1, active: 1 });
|
|
|
18
18
|
threadSchema.index({ thread_id: 1 });
|
|
19
19
|
threadSchema.index({ conversation_id: 1 });
|
|
20
20
|
|
|
21
|
+
threadSchema.methods.getConversationId = function() {
|
|
22
|
+
const variant = process.env.VARIANT || 'assistants';
|
|
23
|
+
return variant === 'assistants' ? this.thread_id : this.conversation_id;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
threadSchema.methods.getAssistantId = function() {
|
|
27
|
+
const variant = process.env.VARIANT || 'assistants';
|
|
28
|
+
return variant === 'assistants' ? this.assistant_id : this.prompt_id;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
threadSchema.statics.setAssistantId = function(threadObj, assistantId) {
|
|
32
|
+
const variant = process.env.VARIANT || 'assistants';
|
|
33
|
+
if (variant === 'assistants') {
|
|
34
|
+
threadObj.assistant_id = assistantId;
|
|
35
|
+
} else {
|
|
36
|
+
threadObj.prompt_id = assistantId;
|
|
37
|
+
}
|
|
38
|
+
return threadObj;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
threadSchema.statics.setConversationId = function(threadObj, conversationId) {
|
|
42
|
+
const variant = process.env.VARIANT || 'assistants';
|
|
43
|
+
if (variant === 'assistants') {
|
|
44
|
+
threadObj.thread_id = conversationId;
|
|
45
|
+
} else {
|
|
46
|
+
threadObj.conversation_id = conversationId;
|
|
47
|
+
}
|
|
48
|
+
return threadObj;
|
|
49
|
+
};
|
|
50
|
+
|
|
21
51
|
const Thread = mongoose.model('Thread', threadSchema);
|
|
22
52
|
|
|
23
53
|
module.exports = { Thread };
|
|
@@ -224,6 +224,84 @@ class OpenAIAssistantsProvider {
|
|
|
224
224
|
});
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
async handleRequiresAction(assistant, run, threadId) {
|
|
228
|
+
const toolCalls = run?.required_action?.submit_tool_outputs?.tool_calls || [];
|
|
229
|
+
if (toolCalls.length === 0) {
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const outputs = [];
|
|
234
|
+
|
|
235
|
+
for (const call of toolCalls) {
|
|
236
|
+
try {
|
|
237
|
+
const name = call.function?.name;
|
|
238
|
+
const args = call.function?.arguments ? JSON.parse(call.function.arguments) : {};
|
|
239
|
+
const result = await assistant.executeTool(name, args);
|
|
240
|
+
outputs.push({ tool_call_id: call.id, output: typeof result === 'string' ? result : JSON.stringify(result) });
|
|
241
|
+
} catch (error) {
|
|
242
|
+
console.error('[OpenAIAssistantsProvider] Tool execution failed', error);
|
|
243
|
+
outputs.push({
|
|
244
|
+
tool_call_id: call.id,
|
|
245
|
+
output: JSON.stringify({ success: false, error: error?.message || 'Tool execution failed' })
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
await this.submitToolOutputs({
|
|
251
|
+
threadId: threadId || run.thread_id,
|
|
252
|
+
runId: run.id,
|
|
253
|
+
toolOutputs: outputs
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
return outputs;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async checkRunStatus(assistant, thread_id, run_id, retryCount = 0, maxRetries = 10, actionHandled = false) {
|
|
260
|
+
try {
|
|
261
|
+
const run = await this.getRun({ threadId: thread_id, runId: run_id });
|
|
262
|
+
console.log(`Status: ${run.status} ${thread_id} ${run_id} (attempt ${retryCount + 1})`);
|
|
263
|
+
|
|
264
|
+
const failedStatuses = ['failed', 'expired', 'incomplete', 'errored'];
|
|
265
|
+
if (failedStatuses.includes(run.status)) {
|
|
266
|
+
console.log(`Run failed. ${run.status} `);
|
|
267
|
+
console.log('Error:');
|
|
268
|
+
console.log(run);
|
|
269
|
+
return {run, completed: false};
|
|
270
|
+
} else if (run.status === 'cancelled') {
|
|
271
|
+
console.log('cancelled');
|
|
272
|
+
return {run, completed: true};
|
|
273
|
+
} else if (run.status === 'requires_action') {
|
|
274
|
+
console.log('requires_action');
|
|
275
|
+
if (retryCount >= maxRetries) {
|
|
276
|
+
return {run, completed: false};
|
|
277
|
+
}
|
|
278
|
+
if (!actionHandled) {
|
|
279
|
+
await this.handleRequiresAction(assistant, run, thread_id);
|
|
280
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
281
|
+
return this.checkRunStatus(assistant, thread_id, run_id, retryCount + 1, maxRetries, true);
|
|
282
|
+
} else {
|
|
283
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
284
|
+
return this.checkRunStatus(assistant, thread_id, run_id, retryCount + 1, maxRetries, actionHandled);
|
|
285
|
+
}
|
|
286
|
+
} else if (!['completed', 'succeeded'].includes(run.status)) {
|
|
287
|
+
if (retryCount >= maxRetries) {
|
|
288
|
+
return {run, completed: false};
|
|
289
|
+
}
|
|
290
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
291
|
+
return this.checkRunStatus(assistant, thread_id, run_id, retryCount + 1, maxRetries, actionHandled);
|
|
292
|
+
} else {
|
|
293
|
+
console.log('Run completed.');
|
|
294
|
+
return {run, completed: true};
|
|
295
|
+
}
|
|
296
|
+
} catch (error) {
|
|
297
|
+
console.error('Error checking run status:', error);
|
|
298
|
+
return {run: null, completed: false};
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Internal helpers
|
|
304
|
+
*/
|
|
227
305
|
_normalizeContent(content) {
|
|
228
306
|
if (content === undefined || content === null) {
|
|
229
307
|
return [{ type: 'text', text: '' }];
|
|
@@ -62,13 +62,14 @@ class OpenAIResponsesProvider {
|
|
|
62
62
|
async deleteConversation(threadId) {
|
|
63
63
|
const id = this._ensurethreadId(threadId);
|
|
64
64
|
if (this.conversations && typeof this.conversations.del === 'function') {
|
|
65
|
-
return await this.conversations.
|
|
65
|
+
return await this.conversations.delete(id);
|
|
66
66
|
}
|
|
67
67
|
return await this._delete(`/conversations/${id}`);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
async addMessage({ threadId, role = 'user', content, metadata }) {
|
|
71
71
|
const id = this._ensurethreadId(threadId);
|
|
72
|
+
|
|
72
73
|
const payload = this._cleanObject({
|
|
73
74
|
role,
|
|
74
75
|
content: this._normalizeContent(role, content),
|
|
@@ -88,20 +89,47 @@ class OpenAIResponsesProvider {
|
|
|
88
89
|
const id = this._ensurethreadId(threadId);
|
|
89
90
|
const query = this._cleanObject({ order, limit });
|
|
90
91
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
return await this.client.conversations.items.list(id, query);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async cleanupOrphanedFunctionCalls(threadId, deleteAll = false) {
|
|
96
|
+
try {
|
|
97
|
+
const id = this._ensurethreadId(threadId);
|
|
98
|
+
const messages = await this.listMessages({ threadId: id, order: 'desc' });
|
|
99
|
+
const items = messages?.data || [];
|
|
100
|
+
|
|
101
|
+
if (items.length === 0) return;
|
|
102
|
+
|
|
103
|
+
if (deleteAll) {
|
|
104
|
+
console.log(`[OpenAIResponsesProvider] Deleting all ${items.length} items from conversation`);
|
|
105
|
+
for (const item of items) {
|
|
106
|
+
await this.conversations.items.delete(item.id, {conversation_id: id});
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
94
110
|
|
|
95
|
-
|
|
111
|
+
const functionCalls = items.filter(item => item.type === 'function_call');
|
|
112
|
+
const functionOutputs = items.filter(item => item.type === 'function_call_output');
|
|
113
|
+
|
|
114
|
+
for (const functionCall of functionCalls) {
|
|
115
|
+
const hasOutput = functionOutputs.some(output => output.call_id === functionCall.call_id);
|
|
116
|
+
|
|
117
|
+
if (!hasOutput) {
|
|
118
|
+
console.log(`[OpenAIResponsesProvider] Deleting orphaned function_call: ${functionCall.id} (${functionCall.call_id})`);
|
|
119
|
+
await this.conversations.items.delete(functionCall.id, {conversation_id: id});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.warn('[OpenAIResponsesProvider] Failed to cleanup conversation:', error?.message);
|
|
124
|
+
}
|
|
96
125
|
}
|
|
97
126
|
|
|
98
127
|
async runConversation({
|
|
99
128
|
assistantId,
|
|
100
129
|
threadId,
|
|
101
|
-
conversationId,
|
|
102
|
-
promptId,
|
|
103
130
|
instructions,
|
|
104
131
|
additionalMessages = [],
|
|
132
|
+
toolOutputs = [],
|
|
105
133
|
additionalInstructions,
|
|
106
134
|
metadata,
|
|
107
135
|
topP,
|
|
@@ -110,28 +138,41 @@ class OpenAIResponsesProvider {
|
|
|
110
138
|
truncationStrategy,
|
|
111
139
|
tools = [],
|
|
112
140
|
} = {}) {
|
|
113
|
-
|
|
141
|
+
try {
|
|
142
|
+
const id = this._ensurethreadId(threadId);
|
|
143
|
+
const messages = this._responseInput(additionalMessages) || [];
|
|
144
|
+
|
|
145
|
+
const payload = this._cleanObject({
|
|
146
|
+
conversation: id,
|
|
147
|
+
prompt: { id: assistantId },
|
|
148
|
+
instructions: additionalInstructions || instructions,
|
|
149
|
+
input: [...messages, ...toolOutputs],
|
|
150
|
+
metadata,
|
|
151
|
+
top_p: topP,
|
|
152
|
+
temperature,
|
|
153
|
+
max_output_tokens: maxOutputTokens,
|
|
154
|
+
truncation_strategy: truncationStrategy,
|
|
155
|
+
tools: Array.isArray(tools) && tools.length ? tools : undefined,
|
|
156
|
+
});
|
|
114
157
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
instructions: additionalInstructions || instructions,
|
|
119
|
-
input: this._responseInput(additionalMessages),
|
|
120
|
-
metadata,
|
|
121
|
-
top_p: topP,
|
|
122
|
-
temperature,
|
|
123
|
-
max_output_tokens: maxOutputTokens,
|
|
124
|
-
truncation_strategy: truncationStrategy,
|
|
125
|
-
tools: Array.isArray(tools) && tools.length ? tools : undefined,
|
|
126
|
-
});
|
|
158
|
+
console.log('payload', payload);
|
|
159
|
+
const response = await this.client.responses.create(payload);
|
|
160
|
+
console.log('response', response);
|
|
127
161
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
162
|
+
if (response?.status !== 'completed') {
|
|
163
|
+
await this.cleanupOrphanedFunctionCalls(id, true);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
...response,
|
|
168
|
+
thread_id: id,
|
|
169
|
+
assistant_id: assistantId,
|
|
170
|
+
object: response.object || 'response',
|
|
171
|
+
};
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error('[OpenAIResponsesProvider] Error running conversation:', error);
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
135
176
|
}
|
|
136
177
|
|
|
137
178
|
async getRun({ threadId, runId }) {
|
|
@@ -140,27 +181,7 @@ class OpenAIResponsesProvider {
|
|
|
140
181
|
}
|
|
141
182
|
|
|
142
183
|
async listRuns({ threadId, limit, order = 'desc', activeOnly = false } = {}) {
|
|
143
|
-
|
|
144
|
-
const query = this._cleanObject({ conversation: id, limit, order });
|
|
145
|
-
|
|
146
|
-
let responseList;
|
|
147
|
-
try {
|
|
148
|
-
responseList = await this._get('/responses', query);
|
|
149
|
-
} catch (error) {
|
|
150
|
-
console.warn('[OpenAIResponsesProvider] Failed to list responses:', error?.message || error);
|
|
151
|
-
responseList = { data: [] };
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (!activeOnly) {
|
|
155
|
-
return responseList;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const activeStatuses = ['queued', 'in_progress', 'processing', 'requires_action'];
|
|
159
|
-
const data = Array.isArray(responseList?.data)
|
|
160
|
-
? responseList.data.filter((run) => activeStatuses.includes(run.status))
|
|
161
|
-
: [];
|
|
162
|
-
|
|
163
|
-
return { ...responseList, data };
|
|
184
|
+
return { data: [] };
|
|
164
185
|
}
|
|
165
186
|
|
|
166
187
|
async submitToolOutputs({ threadId, runId, toolOutputs }) {
|
|
@@ -237,6 +258,69 @@ class OpenAIResponsesProvider {
|
|
|
237
258
|
});
|
|
238
259
|
}
|
|
239
260
|
|
|
261
|
+
async handleRequiresAction(assistant, run, threadId) {
|
|
262
|
+
const functionCalls = run.output?.filter(item => item.type === 'function_call') || [];
|
|
263
|
+
if (functionCalls.length === 0) {
|
|
264
|
+
return [];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const outputs = [];
|
|
268
|
+
|
|
269
|
+
for (const call of functionCalls) {
|
|
270
|
+
try {
|
|
271
|
+
const name = call.name;
|
|
272
|
+
const args = call.arguments ? JSON.parse(call.arguments) : {};
|
|
273
|
+
const result = await assistant.executeTool(name, args);
|
|
274
|
+
outputs.push({
|
|
275
|
+
type: 'function_call_output',
|
|
276
|
+
call_id: call.call_id,
|
|
277
|
+
output: typeof result === 'string' ? result : JSON.stringify(result)
|
|
278
|
+
});
|
|
279
|
+
} catch (error) {
|
|
280
|
+
console.error('[OpenAIResponsesProvider] Tool execution failed', error);
|
|
281
|
+
outputs.push({
|
|
282
|
+
type: 'function_call_output',
|
|
283
|
+
call_id: call.call_id,
|
|
284
|
+
output: JSON.stringify({ success: false, error: error?.message || 'Tool execution failed' })
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return outputs;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async checkRunStatus(assistant, thread_id, run_id, retryCount = 0, maxRetries = 10, actionHandled = false) {
|
|
293
|
+
try {
|
|
294
|
+
let run = await this.getRun({ threadId: thread_id, runId: run_id });
|
|
295
|
+
console.log(`Status: ${run.status} ${thread_id} ${run_id} (attempt ${retryCount + 1})`);
|
|
296
|
+
|
|
297
|
+
const needsFunctionCall = run.output?.some(item => item.type === 'function_call');
|
|
298
|
+
if (needsFunctionCall && !actionHandled) {
|
|
299
|
+
if (retryCount >= maxRetries) {
|
|
300
|
+
return {run, completed: false};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const outputs = await this.handleRequiresAction(assistant, run, thread_id);
|
|
304
|
+
console.log('[OpenAIResponsesProvider] Function call outputs:', outputs);
|
|
305
|
+
|
|
306
|
+
if (outputs.length > 0) {
|
|
307
|
+
run = await this.runConversation({
|
|
308
|
+
threadId: thread_id,
|
|
309
|
+
assistantId: assistant.assistantId || assistant.promptId,
|
|
310
|
+
toolOutputs: outputs
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
return this.checkRunStatus(assistant, thread_id, run.id, retryCount + 1, maxRetries, true);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return {run, completed: true};
|
|
318
|
+
} catch (error) {
|
|
319
|
+
console.error('Error checking run status:', error);
|
|
320
|
+
return {run: null, completed: false};
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
240
324
|
/**
|
|
241
325
|
* Internal helpers
|
|
242
326
|
*/
|
|
@@ -8,7 +8,7 @@ const { createProvider } = require('../providers/createProvider');
|
|
|
8
8
|
const { Message, formatTimestamp } = require('../models/messageModel.js');
|
|
9
9
|
const { Thread } = require('../models/threadModel.js');
|
|
10
10
|
|
|
11
|
-
const {
|
|
11
|
+
const { getCurRow } = require('../helpers/assistantHelper.js');
|
|
12
12
|
const { processIndividualMessage, getLastMessages } = require('../helpers/assistantHelper.js');
|
|
13
13
|
const { combineImagesToPDF, cleanupFiles } = require('../helpers/filesHelper.js');
|
|
14
14
|
const { delay } = require('../helpers/whatsappHelper.js');
|
|
@@ -30,7 +30,7 @@ const runAssistantAndWait = async ({
|
|
|
30
30
|
assistant,
|
|
31
31
|
runConfig = {}
|
|
32
32
|
}) => {
|
|
33
|
-
if (!thread || !
|
|
33
|
+
if (!thread || !thread.getConversationId()) {
|
|
34
34
|
throw new Error('runAssistantAndWait requires a thread with a valid thread_id or conversation_id');
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -41,49 +41,35 @@ const runAssistantAndWait = async ({
|
|
|
41
41
|
const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
|
|
42
42
|
const { polling, ...conversationConfig } = runConfig || {};
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
threadId: thread
|
|
46
|
-
|
|
47
|
-
assistantId: thread?.assistant_id,
|
|
48
|
-
promptId: thread?.prompt_id,
|
|
44
|
+
let run = await provider.runConversation({
|
|
45
|
+
threadId: thread.getConversationId(),
|
|
46
|
+
assistantId: thread.getAssistantId(),
|
|
49
47
|
...conversationConfig,
|
|
50
48
|
});
|
|
51
49
|
|
|
52
|
-
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
await Thread.updateOne(filter, { $set: { run_id: run.id } });
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const maxRetries = polling?.maxRetries ?? 30;
|
|
60
|
-
let completed = false;
|
|
61
|
-
|
|
62
|
-
try {
|
|
63
|
-
console.log('RUN ID', run.id, thread.thread_id);
|
|
64
|
-
completed = await checkRunStatus(assistant, thread.thread_id, run.id, 0, maxRetries);
|
|
65
|
-
} finally {
|
|
66
|
-
if (filter) {
|
|
67
|
-
await Thread.updateOne(filter, { $set: { run_id: null } });
|
|
68
|
-
}
|
|
69
|
-
}
|
|
50
|
+
const filter = thread.code ? { code: thread.code, active: true } : null;
|
|
51
|
+
if (filter) {
|
|
52
|
+
await Thread.updateOne(filter, { $set: { run_id: run.id } });
|
|
53
|
+
}
|
|
70
54
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
finalRun = await provider.getRun({ threadId: run.thread_id, runId: run.id });
|
|
74
|
-
} catch (error) {
|
|
75
|
-
console.warn('Warning: unable to retrieve final run state:', error?.message || error);
|
|
76
|
-
}
|
|
55
|
+
const maxRetries = polling?.maxRetries ?? 30;
|
|
56
|
+
let completed = false;
|
|
77
57
|
|
|
78
|
-
|
|
79
|
-
|
|
58
|
+
try {
|
|
59
|
+
console.log('RUN ID', run.id, 'THREAD ID', thread.getConversationId(), 'ASSISTANT ID', thread.getAssistantId());
|
|
60
|
+
({run, completed} = await provider.checkRunStatus(assistant, thread.getConversationId(), run.id, 0, maxRetries));
|
|
61
|
+
} finally {
|
|
62
|
+
if (filter) {
|
|
63
|
+
await Thread.updateOne(filter, { $set: { run_id: null } });
|
|
80
64
|
}
|
|
65
|
+
}
|
|
81
66
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
output = run.output_text;
|
|
67
|
+
if (!completed) {
|
|
68
|
+
return { run: run, completed: false, output: '' };
|
|
85
69
|
}
|
|
86
70
|
|
|
71
|
+
const output = await provider.getRunText({ threadId: thread.getConversationId(), runId: run.id, fallback: '' });
|
|
72
|
+
|
|
87
73
|
return { completed: true, output };
|
|
88
74
|
};
|
|
89
75
|
|
|
@@ -193,18 +179,11 @@ const getAssistantById = (assistant_id, thread) => {
|
|
|
193
179
|
const createAssistant = async (code, assistant_id, messages=[], force=false) => {
|
|
194
180
|
// If thread already exists, update it
|
|
195
181
|
const findThread = await Thread.findOne({ code: code });
|
|
196
|
-
const variant = process.env.VARIANT || 'assistants';
|
|
197
182
|
console.log('[createAssistant] findThread', findThread);
|
|
198
|
-
if (
|
|
183
|
+
if (findThread && findThread.getConversationId()) {
|
|
199
184
|
console.log('[createAssistant] Thread already exists');
|
|
200
185
|
const updateFields = { active: true, stopped: false };
|
|
201
|
-
|
|
202
|
-
if (variant === 'responses') {
|
|
203
|
-
updateFields.prompt_id = assistant_id;
|
|
204
|
-
} else {
|
|
205
|
-
updateFields.assistant_id = assistant_id;
|
|
206
|
-
}
|
|
207
|
-
|
|
186
|
+
Thread.setAssistantId(updateFields, assistant_id);
|
|
208
187
|
await Thread.updateOne({ code: code }, { $set: updateFields });
|
|
209
188
|
return findThread;
|
|
210
189
|
}
|
|
@@ -218,7 +197,7 @@ const createAssistant = async (code, assistant_id, messages=[], force=false) =>
|
|
|
218
197
|
const initialThread = await assistant.create(code, curRow[0]);
|
|
219
198
|
|
|
220
199
|
// Add new messages to memory
|
|
221
|
-
const provider = createProvider({ variant:
|
|
200
|
+
const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
|
|
222
201
|
for (const message of messages) {
|
|
223
202
|
await provider.addMessage({
|
|
224
203
|
threadId: initialThread.id,
|
|
@@ -227,32 +206,23 @@ const createAssistant = async (code, assistant_id, messages=[], force=false) =>
|
|
|
227
206
|
});
|
|
228
207
|
}
|
|
229
208
|
|
|
230
|
-
console.log('[createAssistant] initialThread', initialThread);
|
|
231
|
-
// Define new thread data
|
|
232
209
|
const thread = {
|
|
233
210
|
code: code,
|
|
234
|
-
assistant_id: assistant_id.startsWith('asst') ? assistant_id : null,
|
|
235
|
-
thread_id: initialThread.id.startsWith('thread') ? initialThread.id : null,
|
|
236
|
-
conversation_id: initialThread.id.startsWith('conv') ? initialThread.id : null,
|
|
237
|
-
prompt_id: assistant_id.startsWith('pmpt') ? assistant_id : null,
|
|
238
211
|
patient_id: patientId,
|
|
239
212
|
nombre: nombre,
|
|
240
213
|
active: true
|
|
241
214
|
};
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
Object.entries(thread).filter(([_, v]) => v != null)
|
|
245
|
-
);
|
|
246
|
-
console.log('[createAssistant] updateData', updateData);
|
|
215
|
+
Thread.setAssistantId(thread, assistant_id);
|
|
216
|
+
Thread.setConversationId(thread, initialThread.id);
|
|
247
217
|
|
|
248
218
|
const condition = { $or: [{ code: code }] };
|
|
249
219
|
const options = { new: true, upsert: true };
|
|
250
|
-
const updatedThread = await Thread.findOneAndUpdate(condition, {run_id: null, ...
|
|
220
|
+
const updatedThread = await Thread.findOneAndUpdate(condition, {run_id: null, ...thread}, options);
|
|
251
221
|
console.log('[createAssistant] Updated thread:', updatedThread);
|
|
252
222
|
|
|
253
223
|
// Delete previous thread
|
|
254
224
|
if (force) {
|
|
255
|
-
await provider.deleteConversation(findThread.
|
|
225
|
+
await provider.deleteConversation(findThread.getConversationId());
|
|
256
226
|
}
|
|
257
227
|
|
|
258
228
|
return thread;
|
|
@@ -268,7 +238,7 @@ const addMsgAssistant = async (code, inMessages, reply = false) => {
|
|
|
268
238
|
for (const message of inMessages) {
|
|
269
239
|
console.log(message);
|
|
270
240
|
await provider.addMessage({
|
|
271
|
-
threadId: thread.
|
|
241
|
+
threadId: thread.getConversationId(),
|
|
272
242
|
role: 'assistant',
|
|
273
243
|
content: message
|
|
274
244
|
});
|
|
@@ -279,7 +249,7 @@ const addMsgAssistant = async (code, inMessages, reply = false) => {
|
|
|
279
249
|
let output, completed;
|
|
280
250
|
let retries = 0;
|
|
281
251
|
const maxRetries = 10;
|
|
282
|
-
const assistant = getAssistantById(thread
|
|
252
|
+
const assistant = getAssistantById(thread.getAssistantId(), thread);
|
|
283
253
|
do {
|
|
284
254
|
({ output, completed } = await runAssistantAndWait({ thread, assistant }));
|
|
285
255
|
console.log(`Attempt ${retries + 1}: completed=${completed}, output=${output || '(empty)'}`);
|
|
@@ -306,7 +276,7 @@ const addInsAssistant = async (code, instruction) => {
|
|
|
306
276
|
let output, completed;
|
|
307
277
|
let retries = 0;
|
|
308
278
|
const maxRetries = 10;
|
|
309
|
-
const assistant = getAssistantById(thread
|
|
279
|
+
const assistant = getAssistantById(thread.getAssistantId(), thread);
|
|
310
280
|
do {
|
|
311
281
|
({ output, completed } = await runAssistantAndWait({
|
|
312
282
|
thread,
|
|
@@ -352,7 +322,7 @@ const getThread = async (code, message = null) => {
|
|
|
352
322
|
while (thread && thread.run_id) {
|
|
353
323
|
console.log(`Wait for ${thread.run_id} to be executed`);
|
|
354
324
|
const activeProvider = provider || llmConfig.requireOpenAIProvider();
|
|
355
|
-
const run = await activeProvider.getRun({ threadId: thread.
|
|
325
|
+
const run = await activeProvider.getRun({ threadId: thread.getConversationId(), runId: thread.run_id });
|
|
356
326
|
if (run.status === 'cancelled' || run.status === 'expired' || run.status === 'completed') {
|
|
357
327
|
await Thread.updateOne({ code: code }, { $set: { run_id: null } });
|
|
358
328
|
}
|
|
@@ -390,16 +360,14 @@ const replyAssistant = async function (code, message_ = null, thread_ = null, ru
|
|
|
390
360
|
}
|
|
391
361
|
|
|
392
362
|
const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
await delay(5000);
|
|
402
|
-
}
|
|
363
|
+
let activeRuns = await provider.listRuns({ threadId: thread.getConversationId(), activeOnly: true });
|
|
364
|
+
let activeRunsCount = activeRuns?.data?.length || 0;
|
|
365
|
+
console.log('ACTIVE RUNS:', activeRunsCount, activeRuns?.data?.map(run => ({ id: run.id, status: run.status })));
|
|
366
|
+
while (activeRunsCount > 0) {
|
|
367
|
+
console.log(`WAITING FOR ${activeRunsCount} ACTIVE RUNS TO COMPLETE - ${thread.getConversationId()}`);
|
|
368
|
+
activeRuns = await provider.listRuns({ threadId: thread.getConversationId(), activeOnly: true });
|
|
369
|
+
activeRunsCount = activeRuns?.data?.length || 0;
|
|
370
|
+
await delay(5000);
|
|
403
371
|
}
|
|
404
372
|
|
|
405
373
|
let patientMsg = false;
|
|
@@ -427,7 +395,7 @@ const replyAssistant = async function (code, message_ = null, thread_ = null, ru
|
|
|
427
395
|
|
|
428
396
|
if (!patientMsg) return null;
|
|
429
397
|
|
|
430
|
-
const assistant = getAssistantById(
|
|
398
|
+
const assistant = getAssistantById(thread.getAssistantId(), thread);
|
|
431
399
|
assistant.setReplies(patientReply);
|
|
432
400
|
|
|
433
401
|
let run, output, completed;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peopl-health/nexus",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Core messaging and assistant library for WhatsApp communication platforms",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"whatsapp",
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
"@anthropic-ai/sdk": "^0.32.0",
|
|
94
94
|
"baileys": "^6.4.0",
|
|
95
95
|
"express": "4.21.2",
|
|
96
|
-
"openai": "6.
|
|
96
|
+
"openai": "6.7.0",
|
|
97
97
|
"twilio": "5.6.0"
|
|
98
98
|
},
|
|
99
99
|
"engines": {
|