@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.
@@ -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 assistants = await getRecordByFilter(Config_ID, 'assistants', `status="${airtableStatus}"`);
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
- // ONLY add user messages to the thread
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: assistantId, thread_id: threadId } }
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.del(id);
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
- if (this.conversations?.items && typeof this.conversations.items.list === 'function') {
92
- return await this.conversations.items.list(id, query);
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
- return await this._get(`/conversations/${id}/items`, query);
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
- const id = this._ensurethreadId(conversationId || threadId);
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
- const payload = this._cleanObject({
116
- conversation: id,
117
- prompt: { id: promptId },
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
- const response = await this.client.responses.create(payload);
129
- return {
130
- ...response,
131
- thread_id: id,
132
- assistant_id: assistantId,
133
- object: response.object || 'response',
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
- const id = this._ensurethreadId(threadId);
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 { checkRunStatus, getCurRow } = require('../helpers/assistantHelper.js');
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 || !(thread.thread_id || thread.conversation_id)) {
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
- const run = await provider.runConversation({
45
- threadId: thread?.thread_id,
46
- conversationId: thread?.conversation_id,
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
- let output = null;
53
- if (thread?.thread_id) {
54
- const filter = thread.code ? { code: thread.code, active: true } : null;
55
- if (filter) {
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
- let finalRun = run;
72
- try {
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
- if (!completed) {
79
- return { run: finalRun, completed: false, output: '' };
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
- output = await provider.getRunText({ threadId: run.thread_id, runId: run.id, fallback: '' });
83
- } else {
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 ((findThread?.conversation_id && variant === 'responses') || (findThread?.thread_id && variant === 'assistants')) {
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: variant || 'assistants' });
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
- const updateData = Object.fromEntries(
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, ...updateData}, options);
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.conversation_id || findThread.thread_id);
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.conversation_id || thread.thread_id,
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?.prompt_id || thread?.assistant_id, 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?.prompt_id || thread?.assistant_id, 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.conversation_id || thread.thread_id, runId: thread.run_id });
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
- if (!(thread?.conversation_id)) {
394
- let activeRuns = await provider.listRuns({ threadId: thread.thread_id, activeOnly: true });
395
- let activeRunsCount = activeRuns?.data?.length || 0;
396
- console.log('ACTIVE RUNS:', activeRunsCount, activeRuns?.data?.map(run => ({ id: run.id, status: run.status })));
397
- while (activeRunsCount > 0) {
398
- console.log(`WAITING FOR ${activeRunsCount} ACTIVE RUNS TO COMPLETE - ${thread.thread_id}`);
399
- activeRuns = await provider.listRuns({ threadId: thread.thread_id, activeOnly: true });
400
- activeRunsCount = activeRuns?.data?.length || 0;
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(process.env.VARIANT === 'responses' ? thread?.prompt_id : thread?.assistant_id, thread);
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.17",
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.2.0",
96
+ "openai": "6.7.0",
97
97
  "twilio": "5.6.0"
98
98
  },
99
99
  "engines": {