@peopl-health/nexus 2.5.5 → 2.5.6

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,29 +11,68 @@ 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
 
36
- const addMsgAssistantCore = async (code, message, role = 'user', reply = false) => {
75
+ const addMsgAssistantCore = async (code, inMessages, role = 'user', reply = false, skipSystemMessage = false) => {
37
76
  const thread = await getThread(code);
38
77
  if (!thread) return null;
39
78
 
@@ -41,16 +80,43 @@ const addMsgAssistantCore = async (code, message, role = 'user', reply = false)
41
80
  const threadId = thread.getConversationId();
42
81
 
43
82
  try {
44
- await provider.addMessage({ threadId, messages: [{ role, content: message }] });
45
- await insertMessage({ code, message, role });
46
-
47
- if (reply) {
48
- const assistant = getAssistantById(thread.getAssistantId(), thread);
49
- const runResult = await runAssistantWithRetries(thread, assistant, {});
50
- return runResult?.output || null;
83
+ const messages = Array.isArray(inMessages) ? inMessages : [inMessages];
84
+
85
+ await provider.addMessage({
86
+ threadId,
87
+ messages: messages.map(message => ({ role, content: message }))
88
+ });
89
+
90
+ // Save system messages to database for frontend visibility
91
+ if (!skipSystemMessage) {
92
+ for (let i = 0; i < messages.length; i++) {
93
+ const message = messages[i];
94
+ try {
95
+ const message_id = `system_${Date.now()}_${i}_${Math.random().toString(36).substring(7)}`;
96
+ await insertMessage({
97
+ nombre_whatsapp: 'System',
98
+ numero: code,
99
+ body: message,
100
+ message_id: message_id,
101
+ is_group: false,
102
+ is_media: false,
103
+ from_me: true,
104
+ processed: true,
105
+ origin: 'system',
106
+ assistant_id: thread.getAssistantId(),
107
+ raw: { role: role }
108
+ });
109
+ } catch (err) {
110
+ logger.error('[addMsgAssistantCore] Error saving system message:', err);
111
+ }
112
+ }
51
113
  }
52
-
53
- return null;
114
+
115
+ if (!reply) return null;
116
+
117
+ const assistant = getAssistantById(thread.getAssistantId(), thread);
118
+ const runResult = await runAssistantWithRetries(thread, assistant, {});
119
+ return runResult?.output || null;
54
120
  } catch (error) {
55
121
  logger.error('[addMsgAssistantCore] Error adding message', { error: error.message, code, role });
56
122
  return null;
@@ -61,13 +127,36 @@ const addInstructionCore = async (code, instruction, role = 'user') => {
61
127
  const thread = await getThread(code);
62
128
  if (!thread) return null;
63
129
 
64
- const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
65
- const threadId = thread.getConversationId();
66
-
67
130
  try {
68
- await provider.addMessage({ threadId, messages: [{ role, content: instruction }] });
69
131
  const assistant = getAssistantById(thread.getAssistantId(), thread);
70
- const runResult = await runAssistantWithRetries(thread, assistant, {});
132
+ const runResult = await runAssistantWithRetries(thread, assistant, {
133
+ additionalInstructions: instruction,
134
+ additionalMessages: [
135
+ { role: role, content: instruction }
136
+ ]
137
+ });
138
+
139
+ // Save instruction message to database for frontend visibility
140
+ try {
141
+ const message_id = `instruction_${Date.now()}_${Math.random().toString(36).substring(7)}`;
142
+ await insertMessage({
143
+ nombre_whatsapp: 'Instruction',
144
+ numero: code,
145
+ body: instruction,
146
+ message_id: message_id,
147
+ is_group: false,
148
+ is_media: false,
149
+ from_me: true,
150
+ processed: true,
151
+ origin: 'instruction',
152
+ assistant_id: thread.getAssistantId(),
153
+ raw: { role: role }
154
+ });
155
+ } catch (err) {
156
+ logger.error('[addInstructionCore] Error saving instruction message:', err);
157
+ }
158
+
159
+ logger.info('[addInstructionCore] Run response', { output: runResult?.output });
71
160
  return runResult?.output || null;
72
161
  } catch (error) {
73
162
  logger.error('[addInstructionCore] Error adding instruction', { error: error.message, code, role });
@@ -80,14 +169,16 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
80
169
  const startTotal = Date.now();
81
170
 
82
171
  try {
172
+ const threadStart = Date.now();
83
173
  const thread = thread_ || await getThread(code);
84
- timings.get_thread_ms = 0;
174
+ timings.get_thread_ms = Date.now() - threadStart;
85
175
 
86
176
  if (!thread) return null;
87
177
  const finalThread = thread;
88
178
 
179
+ const messagesStart = Date.now();
89
180
  const patientReply = await getLastMessages(code);
90
- timings.get_messages_ms = 0;
181
+ timings.get_messages_ms = Date.now() - messagesStart;
91
182
 
92
183
  if (!patientReply) {
93
184
  logger.info('[replyAssistantCore] No relevant data found for this assistant.');
@@ -96,10 +187,11 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
96
187
 
97
188
  const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
98
189
  logger.info(`[replyAssistantCore] Processing ${patientReply.length} messages in parallel`);
190
+ const processStart = Date.now();
99
191
  const processResult = await processThreadMessage(code, patientReply, provider);
100
192
 
101
193
  const { results: processResults, timings: processTimings } = processResult;
102
- timings.process_messages_ms = 0;
194
+ timings.process_messages_ms = Date.now() - processStart;
103
195
 
104
196
  logger.debug('[replyAssistantCore] Process timings breakdown', { processTimings });
105
197
 
@@ -120,14 +212,8 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
120
212
 
121
213
  if (allMessagesToAdd.length > 0) {
122
214
  logger.info(`[replyAssistantCore] Adding ${allMessagesToAdd.length} messages to thread in batch`);
123
- await withThreadRecovery(
124
- async (thread = finalThread) => {
125
- const threadId = thread.getConversationId();
126
- await provider.addMessage({ threadId, messages: allMessagesToAdd });
127
- },
128
- finalThread,
129
- process.env.VARIANT || 'assistants'
130
- );
215
+ const threadId = finalThread.getConversationId();
216
+ await provider.addMessage({ threadId, messages: allMessagesToAdd });
131
217
  }
132
218
 
133
219
  await Promise.all(processResults.map(r => updateMessageRecord(r.reply, finalThread)));
@@ -135,8 +221,9 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
135
221
 
136
222
  if (urls.length > 0) {
137
223
  logger.info(`[replyAssistantCore] Processing ${urls.length} URLs for PDF combination`);
224
+ const pdfStart = Date.now();
138
225
  const pdfResult = await combineImagesToPDF({ code });
139
- timings.pdf_combination_ms = 0;
226
+ timings.pdf_combination_ms = Date.now() - pdfStart;
140
227
  const { pdfBuffer, processedFiles } = pdfResult;
141
228
  logger.info(`[replyAssistantCore] PDF combination complete: ${processedFiles?.length || 0} files processed`);
142
229
 
@@ -156,11 +243,12 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
156
243
  if (!patientMsg || finalThread.stopped) return null;
157
244
 
158
245
  const assistant = getAssistantById(finalThread.getAssistantId(), finalThread);
246
+ const runStart = Date.now();
159
247
  const runResult = await runAssistantWithRetries(finalThread, assistant, runOptions, patientReply);
160
- timings.run_assistant_ms = 0;
248
+ timings.run_assistant_ms = Date.now() - runStart;
161
249
  timings.total_ms = Date.now() - startTotal;
162
250
 
163
- const { run, output, completed, retries, predictionTimeMs, tools_executed } = runResult;
251
+ const { output, completed, retries, predictionTimeMs, tools_executed } = runResult;
164
252
 
165
253
  logger.info('[Assistant Reply Complete]', {
166
254
  code: code ? `${code.substring(0, 3)}***${code.slice(-4)}` : 'unknown',
@@ -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",
3
+ "version": "2.5.6",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",