@peopl-health/nexus 2.5.5-message-tracking → 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.
- package/lib/assistants/BaseAssistant.js +11 -5
- package/lib/controllers/assistantController.js +1 -4
- package/lib/controllers/conversationController.js +1 -2
- package/lib/helpers/assistantHelper.js +30 -117
- package/lib/providers/OpenAIAssistantsProvider.js +122 -121
- package/lib/providers/OpenAIResponsesProvider.js +168 -398
- package/lib/providers/OpenAIResponsesProviderTools.js +49 -96
- package/lib/services/airtableService.js +1 -1
- package/lib/services/assistantServiceCore.js +61 -27
- package/lib/templates/templateStructure.js +1 -1
- package/lib/utils/retryHelper.js +3 -2
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
61
|
-
|
|
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
|
|
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
|
|
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
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
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' || !
|
|
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
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
178
|
-
|
|
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 =
|
|
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 =
|
|
248
|
+
timings.run_assistant_ms = Date.now() - runStart;
|
|
215
249
|
timings.total_ms = Date.now() - startTotal;
|
|
216
250
|
|
|
217
|
-
const {
|
|
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',
|
package/lib/utils/retryHelper.js
CHANGED
|
@@ -117,7 +117,8 @@ async function retryWithBackoff(operation, options = {}) {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
try {
|
|
120
|
-
|
|
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;
|