@peopl-health/nexus 2.5.5 → 2.5.6-fix-switch

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.
@@ -230,12 +230,18 @@ class BaseAssistant {
230
230
 
231
231
  async close() {
232
232
  this.status = 'closed';
233
- const assistantId = process.env.VARIANT === 'responses' ? this.thread?.prompt_id : this.thread?.assistant_id;
233
+ const assistantId = this.thread?.getAssistantId();
234
234
  if (this.thread?.code && assistantId) {
235
- const filter = process.env.VARIANT === 'responses'
236
- ? { code: this.thread.code, prompt_id: assistantId }
237
- : { code: this.thread.code, assistant_id: assistantId };
238
- await Thread.updateOne(filter, { $set: { active: false } });
235
+ await Thread.updateOne(
236
+ {
237
+ code: this.thread.code,
238
+ $or: [
239
+ { assistant_id: assistantId },
240
+ { prompt_id: assistantId }
241
+ ]
242
+ },
243
+ { $set: { active: false } }
244
+ );
239
245
  }
240
246
 
241
247
  if (this.assistantId) {
@@ -25,10 +25,7 @@ const addInsAssistantController = async (req, res) => {
25
25
  const { code, instruction } = req.body;
26
26
 
27
27
  try {
28
- const variant = process.env.VARIANT || 'assistants';
29
- const role = variant === 'responses' ? 'developer' : 'user';
30
-
31
- const assistantReply = await addInsAssistant(code, instruction, role);
28
+ const assistantReply = await addInsAssistant(code, instruction, 'developer');
32
29
  if (assistantReply) await sendMessage({code, body: assistantReply, fileType: 'text', origin: 'assistant'});
33
30
  return res.status(200).send({ message: 'Instruction added to assistant', success: true });
34
31
  } catch (error) {
@@ -678,9 +678,8 @@ const getOpenAIThreadMessagesController = async (req, res) => {
678
678
  limit: parseInt(limit)
679
679
  };
680
680
 
681
- if (variant === 'assistants' && runId) {
681
+ if (runId) {
682
682
  queryParams.runId = runId;
683
- logger.info('Including runId:', runId);
684
683
  }
685
684
 
686
685
  let messages;
@@ -1,6 +1,3 @@
1
- const llmConfig = require('../config/llmConfig.js');
2
-
3
- const { Thread } = require('../models/threadModel.js');
4
1
  const { createProvider } = require('../providers/createProvider.js');
5
2
  const { withTracing } = require('../utils/tracingDecorator');
6
3
  const { withThreadRecovery } = require('./threadRecoveryHelper');
@@ -8,31 +5,6 @@ const { withThreadRecovery } = require('./threadRecoveryHelper');
8
5
  const { getRecordByFilter } = require('../services/airtableService.js');
9
6
  const { logger } = require('../utils/logger');
10
7
 
11
- const DEFAULT_MAX_RETRIES = parseInt(process.env.MAX_RETRIES || '30', 10);
12
-
13
- async function checkIfFinished(text) {
14
- try {
15
- const provider = llmConfig.requireOpenAIProvider();
16
- const completion = await provider.createChatCompletion({
17
- model: 'gpt-4o-mini',
18
- messages: [
19
- {
20
- role: 'user',
21
- content: `Dado el siguiente dialogo, determina si el paciente desea finalizar la conversación.
22
- Considera que la conversación ha terminado si el paciente: 1)Agradece o se despide, usando expresiones como 'gracias', 'adiós', 'hasta luego', etc.
23
- 2)Indica explícitamente que no tiene más síntomas o preguntas, o que no necesita más ayuda.
24
- 3)Expresa que no desea continuar. O el asistente envia recomendaciones o se despide.
25
- Responde solo con 'Si' si la conversación ha terminado, o 'No' si aún continúa: Texto: "${text}`
26
- }
27
- ]
28
- });
29
-
30
- return completion.choices[0].message.content;
31
- } catch (error) {
32
- logger.error('[checkIfFinished] Error checking run status:', error);
33
- }
34
- }
35
-
36
8
  function getCurRow(baseID, code) {
37
9
  if (code.endsWith('@g.us')) {
38
10
  return getRecordByFilter(baseID, 'estado_general', `FIND("${code}", {Group ID})`);
@@ -55,116 +27,57 @@ const runAssistantAndWait = async ({
55
27
  }
56
28
 
57
29
  const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
58
- const { polling, tools: configTools, ...conversationConfig } = runConfig || {};
59
- const variant = provider.getVariant ? provider.getVariant() : (process.env.VARIANT || 'assistants');
30
+ const { polling, tools: configTools, ...config } = runConfig || {};
60
31
  const tools = assistant.getToolSchemas ? assistant.getToolSchemas() : (configTools || []);
61
32
 
62
33
  return await withThreadRecovery(
63
34
  async (currentThread = thread) => {
64
- const toolMetadata = { numero: currentThread.code, assistant_id: currentThread.getAssistantId() };
65
- const runConfigWithAssistant = variant === 'responses'
66
- ? { ...conversationConfig, assistant, toolMetadata }
67
- : conversationConfig;
68
-
69
- let run = await provider.runConversation({
70
- threadId: currentThread.getConversationId(),
71
- assistantId: currentThread.getAssistantId(),
72
- tools: tools.length > 0 ? tools : undefined,
73
- ...runConfigWithAssistant,
35
+ return await provider.executeRun({
36
+ thread: currentThread,
37
+ assistant,
38
+ tools,
39
+ config,
40
+ polling
74
41
  });
75
-
76
- const filter = currentThread.code ? { code: currentThread.code, active: true } : null;
77
- if (filter) {
78
- await Thread.updateOne(filter, { $set: { run_id: run.id } });
79
- }
80
-
81
- const maxRetries = polling?.maxRetries ?? DEFAULT_MAX_RETRIES;
82
- let completed = false;
83
- let tools_executed = run.tools_executed || [];
84
-
85
- try {
86
- logger.info('[runAssistantAndWait] Run started', { runId: run.id, threadId: currentThread.getConversationId(), assistantId: currentThread.getAssistantId() });
87
- const result = await provider.checkRunStatus(assistant, currentThread.getConversationId(), run.id, 0, maxRetries, false, toolMetadata);
88
- run = result.run;
89
- completed = result.completed;
90
- tools_executed = [...tools_executed, ...(result.tools_executed || [])];
91
- } finally {
92
- if (filter) {
93
- await Thread.updateOne(filter, { $set: { run_id: null } });
94
- }
95
- }
96
-
97
- if (!completed) {
98
- return { run: run, completed: false, output: '', tools_executed };
99
- }
100
-
101
- const output = await provider.getRunText({ threadId: currentThread.getConversationId(), runId: run.id, fallback: '' });
102
-
103
- return { completed: true, output, tools_executed };
104
42
  },
105
- thread,
106
- variant
43
+ thread
107
44
  );
108
45
  };
109
46
 
110
- const executeAssistantAttempt = async (thread, assistant, runConfig, attemptNumber) => {
111
- const result = await runAssistantAndWait({
112
- thread,
113
- assistant,
114
- runConfig
115
- });
116
-
117
- logger.info(`[executeAssistantAttempt] Attempt ${attemptNumber}: completed=${result.completed}, output=${result.output || '(empty)'}`);
118
-
119
- return result;
120
- };
121
-
122
47
  const runAssistantWithRetries = async (thread, assistant, runConfig, patientReply = null) => {
123
48
  if (patientReply) {
124
49
  assistant.setReplies(patientReply);
125
50
  }
126
51
 
127
52
  const startTime = Date.now();
128
- let run, output, completed, tools_executed;
129
- let retries = 0;
130
- const maxRetries = DEFAULT_MAX_RETRIES;
131
53
 
132
- do {
133
- retries++;
134
- ({ run, output, completed, tools_executed } = await withTracing(
135
- executeAssistantAttempt,
136
- 'assistant_attempt',
137
- (thread, assistant, runConfig, attemptNumber) => ({
138
- 'attempt.number': attemptNumber,
139
- 'attempt.is_retry': attemptNumber > 1,
140
- 'attempt.max_attempts': maxRetries
141
- })
142
- )(thread, assistant, runConfig, retries));
143
-
144
- if (completed && output) break;
145
-
146
- if (retries < maxRetries) {
147
- const delay = retries === 1
148
- ? 500
149
- : Math.min(1000 * Math.pow(1.5, retries - 1), 5000);
150
- logger.info(`[runAssistantWithRetries] Retry ${retries}, waiting ${delay}ms`);
151
- await new Promise(resolve => setTimeout(resolve, delay));
152
- }
153
- } while (retries < maxRetries && (!completed || !output));
154
-
54
+ const result = await withTracing(
55
+ runAssistantAndWait,
56
+ 'run_assistant_with_retries',
57
+ () => ({
58
+ 'thread.id': thread.getConversationId(),
59
+ 'assistant.id': thread.getAssistantId()
60
+ })
61
+ )({ thread, assistant, runConfig });
62
+
155
63
  const predictionTimeMs = Date.now() - startTime;
156
-
157
- if (run?.last_error) logger.warn('[runAssistantWithRetries] Run error', { error: run.last_error });
158
- logger.info('[runAssistantWithRetries] Run completed', { completed, outputLength: output?.length || 0, toolsExecuted: tools_executed?.length || 0 });
159
- logger.info('[runAssistantWithRetries] TIMING', { predictionTimeMs, retries });
160
64
 
161
- return { run, output, completed, retries, predictionTimeMs, tools_executed };
65
+ logger.info('[runAssistantWithRetries] Run completed', {
66
+ completed: result.completed,
67
+ outputLength: result.output?.length || 0,
68
+ toolsExecuted: result.tools_executed?.length || 0,
69
+ predictionTimeMs
70
+ });
71
+
72
+ return {
73
+ ...result,
74
+ retries: result.retries || 0,
75
+ predictionTimeMs
76
+ };
162
77
  };
163
78
 
164
79
  module.exports = {
165
- checkIfFinished,
166
80
  getCurRow,
167
81
  runAssistantAndWait,
168
- runAssistantWithRetries,
169
- executeAssistantAttempt
82
+ runAssistantWithRetries
170
83
  };
@@ -138,6 +138,7 @@ async function insertMessage(values) {
138
138
  origin: values.origin,
139
139
  tools_executed: values.tools_executed || [],
140
140
  raw: values.raw || null,
141
+ assistant_id: values.assistant_id || null,
141
142
  statusInfo: values.statusInfo || (values.delivery_status ? {
142
143
  status: values.delivery_status,
143
144
  errorCode: values.delivery_error_code || null,
@@ -1,4 +1,5 @@
1
1
  const { OpenAI } = require('openai');
2
+ const { Thread } = require('../models/threadModel');
2
3
  const { retryWithBackoff } = require('../utils/retryHelper');
3
4
  const { logger } = require('../utils/logger');
4
5
 
@@ -43,32 +44,13 @@ class OpenAIAssistantsProvider {
43
44
  return this.client;
44
45
  }
45
46
 
46
- /**
47
- * Retry helper wrapper that uses shared retry logic
48
- * @private
49
- */
50
- async _retryWithBackoff(operation, options = {}) {
51
- return retryWithBackoff(operation, {
52
- ...options,
53
- providerName: PROVIDER_NAME,
54
- });
55
- }
56
-
57
- /**
58
- * @deprecated Use _retryWithBackoff instead
59
- * Kept for backward compatibility
60
- */
61
- async _retryWithRateLimit(operation, maxRetries = 3, retryCount = 0) {
62
- return this._retryWithBackoff(operation, { maxRetries, retryCount });
63
- }
64
-
65
47
  async createConversation({ metadata, messages = [], toolResources } = {}) {
66
- const thread = await this._retryWithRateLimit(() =>
48
+ const { result: thread } = await retryWithBackoff(() =>
67
49
  this.client.beta.threads.create({
68
50
  metadata,
69
51
  tool_resources: toolResources,
70
52
  })
71
- );
53
+ , { providerName: PROVIDER_NAME });
72
54
 
73
55
  if (Array.isArray(messages) && messages.length > 0) {
74
56
  for (const message of messages) {
@@ -79,10 +61,6 @@ class OpenAIAssistantsProvider {
79
61
  return thread;
80
62
  }
81
63
 
82
- async deleteConversation(threadId) {
83
- await this.client.beta.threads.del(this._ensureId(threadId));
84
- }
85
-
86
64
  async addMessage({ threadId, messages, role = 'user', content, metadata }) {
87
65
  const id = this._ensureId(threadId);
88
66
  const messagesToAdd = messages || [{ role, content, metadata }];
@@ -99,9 +77,9 @@ class OpenAIAssistantsProvider {
99
77
  payload.metadata = msg.metadata;
100
78
  }
101
79
 
102
- const result = await this._retryWithRateLimit(() =>
80
+ const { result } = await retryWithBackoff(() =>
103
81
  this.client.beta.threads.messages.create(id, payload)
104
- );
82
+ , { providerName: PROVIDER_NAME });
105
83
  results.push(result);
106
84
  }
107
85
  return messages ? results : results[0];
@@ -141,6 +119,83 @@ class OpenAIAssistantsProvider {
141
119
  return fallback;
142
120
  }
143
121
 
122
+ /**
123
+ * Normalize thread object to unified format
124
+ */
125
+ _normalizeThread(thread) {
126
+ return {
127
+ conversationId: thread.thread_id || thread.getConversationId?.(),
128
+ assistantId: thread.assistant_id || thread.getAssistantId?.()
129
+ };
130
+ }
131
+
132
+
133
+ /**
134
+ * Main entry point for running assistant
135
+ */
136
+ async executeRun({ thread, assistant, tools = [], config = {}, polling = {} }) {
137
+ const { conversationId, assistantId } = this._normalizeThread(thread);
138
+
139
+ logger.info('[OpenAIAssistantsProvider] Starting run', {
140
+ conversationId,
141
+ assistantId
142
+ });
143
+
144
+ const filter = thread.code ? { code: thread.code, active: true } : null;
145
+
146
+ const run = await this.runConversation({
147
+ threadId: conversationId,
148
+ assistantId,
149
+ tools,
150
+ ...config
151
+ });
152
+
153
+ if (filter) {
154
+ await Thread.updateOne(filter, { $set: { run_id: run.id } });
155
+ }
156
+
157
+ let result;
158
+ try {
159
+ logger.info('[OpenAIAssistantsProvider] Polling run status', { runId: run.id });
160
+ result = await this.checkRunStatus(assistant, conversationId, run.id);
161
+ } finally {
162
+ if (filter) {
163
+ await Thread.updateOne(filter, { $set: { run_id: null } });
164
+ }
165
+ }
166
+
167
+ if (!result.completed) {
168
+ logger.warn('[OpenAIAssistantsProvider] Run did not complete', { runId: run.id });
169
+ return {
170
+ run: result.run,
171
+ completed: false,
172
+ output: '',
173
+ tools_executed: result.tools_executed || [],
174
+ retries: run.retries || 0
175
+ };
176
+ }
177
+
178
+ const output = await this.getRunText({
179
+ threadId: conversationId,
180
+ runId: result.run.id,
181
+ fallback: ''
182
+ });
183
+
184
+ logger.info('[OpenAIAssistantsProvider] Run complete', {
185
+ runId: result.run.id,
186
+ completed: true,
187
+ toolsExecuted: result.tools_executed?.length || 0
188
+ });
189
+
190
+ return {
191
+ run: result.run,
192
+ completed: true,
193
+ output,
194
+ tools_executed: result.tools_executed || [],
195
+ retries: run.retries || 0
196
+ };
197
+ }
198
+
144
199
  async runConversation({
145
200
  threadId,
146
201
  assistantId,
@@ -167,27 +222,23 @@ class OpenAIAssistantsProvider {
167
222
  tools,
168
223
  };
169
224
 
170
- if (Array.isArray(payload.additional_messages) && payload.additional_messages.length === 0) {
171
- delete payload.additional_messages;
172
- }
173
-
174
- if (Array.isArray(payload.tools) && payload.tools.length === 0) {
175
- delete payload.tools;
176
- }
177
-
178
- if (payload.metadata && Object.keys(payload.metadata).length === 0) {
179
- delete payload.metadata;
180
- }
181
-
182
- Object.keys(payload).forEach((key) => {
183
- if (payload[key] === undefined || payload[key] === null) {
225
+ // Clean up empty/null values
226
+ Object.keys(payload).forEach(key => {
227
+ if (payload[key] === null || payload[key] === undefined ||
228
+ (Array.isArray(payload[key]) && payload[key].length === 0) ||
229
+ (payload[key] && typeof payload[key] === 'object' && Object.keys(payload[key]).length === 0)) {
184
230
  delete payload[key];
185
231
  }
186
232
  });
187
233
 
188
- return this._retryWithRateLimit(() =>
234
+ const { result, retries } = await retryWithBackoff(() =>
189
235
  this.client.beta.threads.runs.create(this._ensureId(threadId), payload)
190
- );
236
+ , { providerName: PROVIDER_NAME });
237
+
238
+ return {
239
+ ...result,
240
+ retries: retries
241
+ };
191
242
  }
192
243
 
193
244
  async getRun({ threadId, runId }) {
@@ -195,23 +246,6 @@ class OpenAIAssistantsProvider {
195
246
  return run;
196
247
  }
197
248
 
198
- async listRuns({ threadId, limit, order = 'desc', activeOnly = false } = {}) {
199
- const runs = await this.client.beta.threads.runs.list(this._ensureId(threadId), {
200
- limit,
201
- order,
202
- });
203
-
204
- if (activeOnly) {
205
- const activeStatuses = ['in_progress', 'queued', 'requires_action'];
206
- return {
207
- ...runs,
208
- data: runs.data.filter((run) => activeStatuses.includes(run.status)),
209
- };
210
- }
211
-
212
- return runs;
213
- }
214
-
215
249
  async submitToolOutputs({ threadId, runId, toolOutputs }) {
216
250
  return this.client.beta.threads.runs.submitToolOutputs(
217
251
  this._ensureId(runId),
@@ -222,33 +256,6 @@ class OpenAIAssistantsProvider {
222
256
  );
223
257
  }
224
258
 
225
- async cancelRun({ threadId, runId }) {
226
- return this.client.beta.threads.runs.cancel(
227
- this._ensureId(runId),
228
- { thread_id: this._ensureId(threadId) }
229
- );
230
- }
231
-
232
- async createChatCompletion({ model, messages, temperature, maxTokens, topP, metadata, responseFormat } = {}) {
233
- return this.client.chat.completions.create({
234
- model: model || this.defaults.chatModel,
235
- messages,
236
- temperature,
237
- max_tokens: maxTokens,
238
- top_p: topP,
239
- metadata,
240
- response_format: responseFormat,
241
- });
242
- }
243
-
244
- async uploadFile({ file, purpose }) {
245
- if (!file) {
246
- throw new Error('uploadFile requires a readable file stream or object');
247
- }
248
-
249
- return this.client.files.create({ file, purpose: purpose || 'assistants' });
250
- }
251
-
252
259
  async transcribeAudio({ file, model, language, responseFormat, temperature, prompt } = {}) {
253
260
  return this.client.audio.transcriptions.create({
254
261
  model: model || this.defaults.transcriptionModel,
@@ -292,46 +299,40 @@ class OpenAIAssistantsProvider {
292
299
  return outputs;
293
300
  }
294
301
 
295
- async checkRunStatus(assistant, thread_id, run_id, retryCount = 0, maxRetries = DEFAULT_MAX_RETRIES, actionHandled = false) {
302
+ async checkRunStatus(assistant, threadId, runId, attempt = 0, maxRetries = DEFAULT_MAX_RETRIES) {
303
+ if (attempt >= maxRetries) return { run: null, completed: false };
304
+
296
305
  try {
297
- const run = await this.getRun({ threadId: thread_id, runId: run_id });
298
- logger.info(`Status: ${run.status} ${thread_id} ${run_id} (attempt ${retryCount + 1})`);
299
-
300
- const failedStatuses = ['failed', 'expired', 'incomplete', 'errored'];
301
- if (failedStatuses.includes(run.status)) {
302
- logger.info(`Run failed. ${run.status} `);
303
- logger.info('Error:');
304
- logger.info(run);
305
- return {run, completed: false};
306
- } else if (run.status === 'cancelled') {
307
- logger.info('cancelled');
308
- return {run, completed: true};
309
- } else if (run.status === 'requires_action') {
306
+ const run = await this.getRun({ threadId, runId });
307
+ logger.info(`Status: ${run.status} ${threadId} ${runId} (attempt ${attempt + 1})`);
308
+
309
+ if (['failed', 'expired', 'incomplete', 'errored'].includes(run.status)) {
310
+ logger.info(`Run failed: ${run.status}`);
311
+ return { run, completed: false };
312
+ }
313
+
314
+ if (['completed', 'succeeded'].includes(run.status)) {
315
+ logger.info('Run completed.');
316
+ return { run, completed: true };
317
+ }
318
+
319
+ if (run.status === 'cancelled') {
320
+ logger.info('Run cancelled');
321
+ return { run, completed: true };
322
+ }
323
+
324
+ if (run.status === 'requires_action') {
310
325
  logger.info('requires_action');
311
- if (retryCount >= maxRetries) {
312
- return {run, completed: false};
313
- }
314
- if (!actionHandled) {
315
- await this.handleRequiresAction(assistant, run, thread_id);
316
- await new Promise(resolve => setTimeout(resolve, 5000));
317
- return this.checkRunStatus(assistant, thread_id, run_id, retryCount + 1, maxRetries, true);
318
- } else {
319
- await new Promise(resolve => setTimeout(resolve, 1000));
320
- return this.checkRunStatus(assistant, thread_id, run_id, retryCount + 1, maxRetries, actionHandled);
321
- }
322
- } else if (!['completed', 'succeeded'].includes(run.status)) {
323
- if (retryCount >= maxRetries) {
324
- return {run, completed: false};
325
- }
326
- await new Promise(resolve => setTimeout(resolve, 1000));
327
- return this.checkRunStatus(assistant, thread_id, run_id, retryCount + 1, maxRetries, actionHandled);
326
+ await this.handleRequiresAction(assistant, run, threadId);
327
+ await new Promise(resolve => setTimeout(resolve, 5000));
328
328
  } else {
329
- logger.info('Run completed.');
330
- return {run, completed: true};
329
+ await new Promise(resolve => setTimeout(resolve, 1000));
331
330
  }
331
+
332
+ return this.checkRunStatus(assistant, threadId, runId, attempt + 1, maxRetries);
332
333
  } catch (error) {
333
334
  logger.error('Error checking run status:', error);
334
- return {run: null, completed: false};
335
+ return { run: null, completed: false };
335
336
  }
336
337
  }
337
338