@peopl-health/nexus 2.5.5-message-tracking → 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.
@@ -2,144 +2,97 @@ const { logger } = require('../utils/logger');
2
2
 
3
3
  async function executeFunctionCall(assistant, call, metadata = {}) {
4
4
  const startTime = Date.now();
5
+ const name = call.name;
6
+ const args = call.arguments ? JSON.parse(call.arguments) : {};
7
+
8
+ let result, success = true;
5
9
  try {
6
- const name = call.name;
7
- const args = call.arguments ? JSON.parse(call.arguments) : {};
8
- const result = await assistant.executeTool(name, args);
9
- const executionTime = Date.now() - startTime;
10
-
11
- const toolData = {
12
- tool_name: name,
13
- tool_arguments: args,
14
- tool_output: result,
15
- execution_time_ms: executionTime,
16
- success: true,
17
- call_id: call.call_id,
18
- executed_at: new Date()
19
- };
20
-
21
- return {
22
- functionOutput: {
23
- type: 'function_call_output',
24
- call_id: call.call_id,
25
- output: typeof result === 'string' ? result : JSON.stringify(result)
26
- },
27
- toolData
28
- };
10
+ result = await assistant.executeTool(name, args);
29
11
  } catch (error) {
30
- const executionTime = Date.now() - startTime;
31
-
32
- const toolData = {
33
- tool_name: call.name,
34
- tool_arguments: call.arguments ? JSON.parse(call.arguments) : {},
35
- tool_output: { error: error?.message || 'Tool execution failed' },
36
- execution_time_ms: executionTime,
37
- success: false,
38
- call_id: call.call_id,
39
- executed_at: new Date()
40
- };
41
-
12
+ result = { error: error?.message || 'Tool execution failed' };
13
+ success = false;
42
14
  logger.error('[OpenAIResponsesProvider] Tool execution failed', error);
43
- return {
44
- functionOutput: {
45
- type: 'function_call_output',
46
- call_id: call.call_id,
47
- output: JSON.stringify({ success: false, error: error?.message || 'Tool execution failed' })
48
- },
49
- toolData
50
- };
51
15
  }
16
+
17
+ const toolData = {
18
+ tool_name: name,
19
+ tool_arguments: args,
20
+ tool_output: result,
21
+ execution_time_ms: Date.now() - startTime,
22
+ success,
23
+ call_id: call.call_id,
24
+ executed_at: new Date()
25
+ };
26
+
27
+ return {
28
+ functionOutput: {
29
+ type: 'function_call_output',
30
+ call_id: call.call_id,
31
+ output: success && typeof result === 'string' ? result : JSON.stringify(success ? result : { success: false, error: result.error })
32
+ },
33
+ toolData
34
+ };
52
35
  }
53
36
 
54
37
  /**
55
- * Handle pending function calls from conversation items
56
- * @param {Object} assistant - The assistant instance with executeTool method
57
- * @param {Array} conversationItems - Array of conversation items
58
- * @returns {Promise<Array>} Array of function call outputs
38
+ * Handle function calls from conversation items or run output
59
39
  */
60
- async function handlePendingFunctionCalls(assistant, conversationItems, metadata = {}) {
61
- const pendingFunctionCalls = conversationItems.filter(item => item.type === 'function_call');
62
- const functionOutputs = conversationItems.filter(item => item.type === 'function_call_output');
63
-
64
- const orphanedCalls = pendingFunctionCalls.filter(call =>
65
- !functionOutputs.some(output => output.call_id === call.call_id)
66
- );
67
-
68
- if (orphanedCalls.length === 0) {
40
+ async function handleFunctionCalls(functionCalls, assistant, metadata = {}) {
41
+ if (!functionCalls || functionCalls.length === 0) {
69
42
  return { outputs: [], toolsExecuted: [] };
70
43
  }
71
44
 
72
- logger.info(`[OpenAIResponsesProvider] Found ${orphanedCalls.length} pending function calls, handling them...`);
73
45
  const outputs = [];
74
46
  const toolsExecuted = [];
75
47
 
76
- for (const call of orphanedCalls) {
48
+ for (const call of functionCalls) {
77
49
  const result = await executeFunctionCall(assistant, call, metadata);
78
50
  outputs.push(result.functionOutput);
79
51
  toolsExecuted.push(result.toolData);
80
52
  }
81
-
53
+
82
54
  return { outputs, toolsExecuted };
83
55
  }
84
56
 
85
57
  /**
86
- * Handle function calls from a run output
87
- * @param {Object} assistant - The assistant instance with executeTool method
88
- * @param {Object} run - The run object with output array
89
- * @returns {Promise<Array>} Array of function call outputs
58
+ * Handle pending function calls from conversation items
90
59
  */
91
- async function handleRequiresAction(assistant, run, metadata = {}) {
92
- const functionCalls = run.output?.filter(item => item.type === 'function_call') || [];
93
- if (functionCalls.length === 0) {
94
- return { outputs: [], toolsExecuted: [] };
95
- }
96
-
97
- const outputs = [];
98
- const toolsExecuted = [];
60
+ async function handlePendingFunctionCalls(assistant, conversationItems, metadata = {}) {
61
+ const outputCallIds = new Set(
62
+ conversationItems.filter(item => item.type === 'function_call_output').map(output => output.call_id)
63
+ );
99
64
 
100
- for (const call of functionCalls) {
101
- const result = await executeFunctionCall(assistant, call, metadata);
102
- outputs.push(result.functionOutput);
103
- toolsExecuted.push(result.toolData);
65
+ const orphanedCalls = conversationItems.filter(item =>
66
+ item.type === 'function_call' && !outputCallIds.has(item.call_id)
67
+ );
68
+
69
+ if (orphanedCalls.length > 0) {
70
+ logger.info(`[OpenAIResponsesProvider] Found ${orphanedCalls.length} pending function calls, handling them...`);
104
71
  }
105
-
106
- return { outputs, toolsExecuted };
72
+
73
+ return handleFunctionCalls(orphanedCalls, assistant, metadata);
107
74
  }
108
75
 
109
76
  /**
110
77
  * Transform tools from Assistants API format to Responses API format
111
- * @param {string} variant - The API variant ('responses' or 'assistants')
112
- * @param {Array} tools - Array of tool definitions
113
- * @returns {Array|undefined} Transformed tools or undefined if empty
114
78
  */
115
79
  function transformToolsForResponsesAPI(variant, tools) {
116
- if (variant !== 'responses' || !Array.isArray(tools) || tools.length === 0) {
117
- return Array.isArray(tools) && tools.length > 0 ? tools : undefined;
118
- }
80
+ if (variant !== 'responses' || !tools?.length) return tools?.length ? tools : undefined;
119
81
 
120
82
  return tools.map(tool => {
121
- // If tool is in Assistants API format (type: 'function', function: {...})
122
83
  if (tool.type === 'function' && tool.function) {
123
- const transformed = {
124
- name: tool.function.name,
125
- type: 'function',
126
- description: tool.function.description,
127
- parameters: tool.function.parameters
128
- };
129
- if (tool.function.strict !== undefined) {
130
- transformed.strict = tool.function.strict;
131
- }
84
+ const { name, description, parameters, strict } = tool.function;
85
+ const transformed = { name, type: 'function', description, parameters };
86
+ if (strict !== undefined) transformed.strict = strict;
132
87
  return transformed;
133
88
  }
134
- // If already in Responses API format, return as-is
135
89
  return tool;
136
90
  });
137
91
  }
138
92
 
139
93
  module.exports = {
140
94
  executeFunctionCall,
95
+ handleFunctionCalls,
141
96
  handlePendingFunctionCalls,
142
- handleRequiresAction,
143
97
  transformToolsForResponsesAPI
144
98
  };
145
-
@@ -7,7 +7,7 @@ async function addRecord(baseID, tableName, fields) {
7
7
  if (!airtable) throw new Error('Airtable not configured. Set AIRTABLE_API_KEY');
8
8
  const base = airtable.base(baseID);
9
9
  const record = await base(tableName).create(fields);
10
- logger.info('Record added at', tableName);
10
+ logger.info('Record added at', { tableName });
11
11
  return record;
12
12
  } catch (error) {
13
13
  logger.error('Error adding record:', error);
@@ -11,25 +11,64 @@ const { getCurRow, runAssistantWithRetries } = require('../helpers/assistantHelp
11
11
  const { getThread } = require('../helpers/threadHelper.js');
12
12
  const { processThreadMessage } = require('../helpers/processHelper.js');
13
13
  const { getLastMessages, updateMessageRecord } = require('../helpers/messageHelper.js');
14
- const { withThreadRecovery } = require('../helpers/threadRecoveryHelper.js');
15
14
  const { combineImagesToPDF, cleanupFiles } = require('../helpers/filesHelper.js');
16
15
  const { getAssistantById } = require('./assistantResolver');
17
16
  const { logger } = require('../utils/logger');
18
17
 
19
- const createAssistantCore = async (code, assistant_id) => {
20
- const thread = await getThread(code);
21
- if (!thread) return null;
18
+ const createAssistantCore = async (code, assistant_id, messages = [], force = false) => {
19
+ const findThread = await Thread.findOne({ code: code });
20
+ logger.info('[createAssistantCore] findThread', findThread);
21
+
22
+ if (findThread && findThread.getConversationId() && !force) {
23
+ logger.info('[createAssistantCore] Thread already exists');
24
+ const updateFields = { active: true, stopped: false };
25
+ Thread.setAssistantId(updateFields, assistant_id);
26
+ await Thread.updateOne({ code: code }, { $set: updateFields });
27
+ return { success: true, assistant_id, thread: findThread };
28
+ }
22
29
 
23
- const assistant = getAssistantById(assistant_id, thread);
30
+ if (force && findThread?.getConversationId()) {
31
+ const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
32
+ await provider.deleteConversation(findThread.getConversationId());
33
+ logger.info('[createAssistantCore] Deleted old conversation, will create new one');
34
+ }
35
+
24
36
  const curRow = await getCurRow(Historial_Clinico_ID, code);
25
- const context = { curRow };
37
+ logger.info('[createAssistantCore] curRow', curRow[0]);
38
+ const nombre = curRow?.[0]?.['name'] || null;
39
+ const patientId = curRow?.[0]?.['record_id'] || null;
26
40
 
27
41
  try {
28
- await assistant.create(code, context);
29
- return { success: true, assistant_id };
42
+ const assistant = getAssistantById(assistant_id, null);
43
+ const initialThread = await assistant.create(code, curRow[0]);
44
+
45
+ const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
46
+ for (const message of messages) {
47
+ await provider.addMessage({
48
+ threadId: initialThread.id,
49
+ role: 'assistant',
50
+ content: message
51
+ });
52
+ }
53
+
54
+ const thread = {
55
+ code: code,
56
+ patient_id: patientId,
57
+ nombre: nombre,
58
+ active: true
59
+ };
60
+ Thread.setAssistantId(thread, assistant_id);
61
+ Thread.setConversationId(thread, initialThread.id);
62
+
63
+ const condition = { $or: [{ code: code }] };
64
+ const options = { new: true, upsert: true };
65
+ const updatedThread = await Thread.findOneAndUpdate(condition, { run_id: null, ...thread }, options);
66
+ logger.info('[createAssistantCore] Updated thread:', updatedThread);
67
+
68
+ return { success: true, assistant_id, thread: updatedThread };
30
69
  } catch (error) {
31
70
  logger.error('[createAssistantCore] Error creating assistant', { error: error.message, assistant_id, code });
32
- return { success: false, error: error.message };
71
+ throw error;
33
72
  }
34
73
  };
35
74
 
@@ -88,11 +127,7 @@ const addInstructionCore = async (code, instruction, role = 'user') => {
88
127
  const thread = await getThread(code);
89
128
  if (!thread) return null;
90
129
 
91
- const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
92
- const threadId = thread.getConversationId();
93
-
94
130
  try {
95
- await provider.addMessage({ threadId, messages: [{ role, content: instruction }] });
96
131
  const assistant = getAssistantById(thread.getAssistantId(), thread);
97
132
  const runResult = await runAssistantWithRetries(thread, assistant, {
98
133
  additionalInstructions: instruction,
@@ -134,14 +169,16 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
134
169
  const startTotal = Date.now();
135
170
 
136
171
  try {
172
+ const threadStart = Date.now();
137
173
  const thread = thread_ || await getThread(code);
138
- timings.get_thread_ms = 0;
174
+ timings.get_thread_ms = Date.now() - threadStart;
139
175
 
140
176
  if (!thread) return null;
141
177
  const finalThread = thread;
142
178
 
179
+ const messagesStart = Date.now();
143
180
  const patientReply = await getLastMessages(code);
144
- timings.get_messages_ms = 0;
181
+ timings.get_messages_ms = Date.now() - messagesStart;
145
182
 
146
183
  if (!patientReply) {
147
184
  logger.info('[replyAssistantCore] No relevant data found for this assistant.');
@@ -150,10 +187,11 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
150
187
 
151
188
  const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
152
189
  logger.info(`[replyAssistantCore] Processing ${patientReply.length} messages in parallel`);
190
+ const processStart = Date.now();
153
191
  const processResult = await processThreadMessage(code, patientReply, provider);
154
192
 
155
193
  const { results: processResults, timings: processTimings } = processResult;
156
- timings.process_messages_ms = 0;
194
+ timings.process_messages_ms = Date.now() - processStart;
157
195
 
158
196
  logger.debug('[replyAssistantCore] Process timings breakdown', { processTimings });
159
197
 
@@ -174,14 +212,8 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
174
212
 
175
213
  if (allMessagesToAdd.length > 0) {
176
214
  logger.info(`[replyAssistantCore] Adding ${allMessagesToAdd.length} messages to thread in batch`);
177
- await withThreadRecovery(
178
- async (thread = finalThread) => {
179
- const threadId = thread.getConversationId();
180
- await provider.addMessage({ threadId, messages: allMessagesToAdd });
181
- },
182
- finalThread,
183
- process.env.VARIANT || 'assistants'
184
- );
215
+ const threadId = finalThread.getConversationId();
216
+ await provider.addMessage({ threadId, messages: allMessagesToAdd });
185
217
  }
186
218
 
187
219
  await Promise.all(processResults.map(r => updateMessageRecord(r.reply, finalThread)));
@@ -189,8 +221,9 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
189
221
 
190
222
  if (urls.length > 0) {
191
223
  logger.info(`[replyAssistantCore] Processing ${urls.length} URLs for PDF combination`);
224
+ const pdfStart = Date.now();
192
225
  const pdfResult = await combineImagesToPDF({ code });
193
- timings.pdf_combination_ms = 0;
226
+ timings.pdf_combination_ms = Date.now() - pdfStart;
194
227
  const { pdfBuffer, processedFiles } = pdfResult;
195
228
  logger.info(`[replyAssistantCore] PDF combination complete: ${processedFiles?.length || 0} files processed`);
196
229
 
@@ -210,11 +243,12 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
210
243
  if (!patientMsg || finalThread.stopped) return null;
211
244
 
212
245
  const assistant = getAssistantById(finalThread.getAssistantId(), finalThread);
246
+ const runStart = Date.now();
213
247
  const runResult = await runAssistantWithRetries(finalThread, assistant, runOptions, patientReply);
214
- timings.run_assistant_ms = 0;
248
+ timings.run_assistant_ms = Date.now() - runStart;
215
249
  timings.total_ms = Date.now() - startTotal;
216
250
 
217
- const { run, output, completed, retries, predictionTimeMs, tools_executed } = runResult;
251
+ const { output, completed, retries, predictionTimeMs, tools_executed } = runResult;
218
252
 
219
253
  logger.info('[Assistant Reply Complete]', {
220
254
  code: code ? `${code.substring(0, 3)}***${code.slice(-4)}` : 'unknown',
@@ -265,11 +299,11 @@ const switchAssistantCore = async (code, assistant_id) => {
265
299
  }
266
300
 
267
301
  const updateFields = {
268
- assistant_id,
269
302
  stopped: false,
270
303
  updatedAt: new Date()
271
304
  };
272
-
305
+ Thread.setAssistantId(updateFields, assistant_id);
306
+
273
307
  await Thread.updateOne({ code }, { $set: updateFields });
274
308
  logger.info('[switchAssistantCore] Assistant switched', { code, assistant_id });
275
309
  return { success: true, assistant_id };
@@ -216,7 +216,7 @@ class Template {
216
216
  }
217
217
  return a.localeCompare(b);
218
218
  })
219
- .map(([key, value], index) => {
219
+ .map(([key, value]) => {
220
220
  if (typeof value === 'string') {
221
221
  return {
222
222
  name: `var_${key}`,
@@ -117,7 +117,8 @@ async function retryWithBackoff(operation, options = {}) {
117
117
  }
118
118
 
119
119
  try {
120
- return await operation();
120
+ const result = await operation();
121
+ return { result, retries: retryCount };
121
122
  } catch (error) {
122
123
  const isRateLimit = error?.status === 429 || error?.code === 'rate_limit_exceeded';
123
124
  const isRetryable = isRetryableError(error);
@@ -148,7 +149,7 @@ async function retryWithBackoff(operation, options = {}) {
148
149
 
149
150
  await sleep(waitTime * 1000);
150
151
 
151
- return retryWithBackoff(operation, { maxRetries, retryCount: retryCount + 1, providerName });
152
+ return await retryWithBackoff(operation, { maxRetries, retryCount: retryCount + 1, providerName });
152
153
  }
153
154
 
154
155
  throw error;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "2.5.5-message-tracking",
3
+ "version": "2.5.6-fix-switch",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",