@peopl-health/nexus 2.2.8 → 2.2.9

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.
@@ -140,6 +140,10 @@ class BaseAssistant {
140
140
  }
141
141
  };
142
142
 
143
+ if (definition.strict !== undefined) {
144
+ toolSchema.function.strict = definition.strict;
145
+ }
146
+
143
147
  this.tools.set(name, {
144
148
  schema: toolSchema,
145
149
  execute: executor
@@ -1,5 +1,10 @@
1
1
  const { OpenAI } = require('openai');
2
2
  const { retryWithBackoff } = require('../utils/retryHelper');
3
+ const {
4
+ handlePendingFunctionCalls: handlePendingFunctionCallsUtil,
5
+ handleRequiresAction: handleRequiresActionUtil,
6
+ transformToolsForResponsesAPI: transformToolsForResponsesAPIUtil
7
+ } = require('./OpenAIResponsesProviderTools');
3
8
 
4
9
  const CONVERSATION_PREFIX = 'conv_';
5
10
  const RESPONSE_PREFIX = 'resp_';
@@ -265,11 +270,29 @@ class OpenAIResponsesProvider {
265
270
  truncationStrategy,
266
271
  tools = [],
267
272
  model,
273
+ assistant,
268
274
  } = {}) {
269
275
  try {
270
276
  const id = this._ensurethreadId(threadId);
271
277
  const messages = this._responseInput(additionalMessages) || [];
272
278
 
279
+ // Check for pending function calls in the conversation before creating a new response
280
+ if (assistant && toolOutputs.length === 0) {
281
+ try {
282
+ const conversationMessages = await this.listMessages({ threadId: id, order: 'desc', limit: 50 });
283
+ const items = conversationMessages?.data || [];
284
+ const pendingOutputs = await handlePendingFunctionCallsUtil(assistant, items);
285
+ if (pendingOutputs.length > 0) {
286
+ toolOutputs = pendingOutputs;
287
+ }
288
+ } catch (error) {
289
+ console.warn('[OpenAIResponsesProvider] Error checking for pending function calls:', error?.message);
290
+ }
291
+ }
292
+
293
+ // Transform tools to Responses API format (name at top level, not nested in function)
294
+ const transformedTools = transformToolsForResponsesAPIUtil(this.variant, tools);
295
+
273
296
  const payload = this._cleanObject({
274
297
  conversation: id,
275
298
  prompt: { id: assistantId },
@@ -281,7 +304,7 @@ class OpenAIResponsesProvider {
281
304
  temperature,
282
305
  max_output_tokens: maxOutputTokens,
283
306
  truncation_strategy: truncationStrategy,
284
- tools: Array.isArray(tools) && tools.length ? tools : undefined,
307
+ tools: transformedTools,
285
308
  });
286
309
 
287
310
  console.log('payload', payload);
@@ -390,34 +413,7 @@ class OpenAIResponsesProvider {
390
413
  }
391
414
 
392
415
  async handleRequiresAction(assistant, run, threadId) {
393
- const functionCalls = run.output?.filter(item => item.type === 'function_call') || [];
394
- if (functionCalls.length === 0) {
395
- return [];
396
- }
397
-
398
- const outputs = [];
399
-
400
- for (const call of functionCalls) {
401
- try {
402
- const name = call.name;
403
- const args = call.arguments ? JSON.parse(call.arguments) : {};
404
- const result = await assistant.executeTool(name, args);
405
- outputs.push({
406
- type: 'function_call_output',
407
- call_id: call.call_id,
408
- output: typeof result === 'string' ? result : JSON.stringify(result)
409
- });
410
- } catch (error) {
411
- console.error('[OpenAIResponsesProvider] Tool execution failed', error);
412
- outputs.push({
413
- type: 'function_call_output',
414
- call_id: call.call_id,
415
- output: JSON.stringify({ success: false, error: error?.message || 'Tool execution failed' })
416
- });
417
- }
418
- }
419
-
420
- return outputs;
416
+ return await handleRequiresActionUtil(assistant, run);
421
417
  }
422
418
 
423
419
  async checkRunStatus(assistant, thread_id, run_id, retryCount = 0, maxRetries = DEFAULT_MAX_RETRIES, actionHandled = false) {
@@ -425,33 +421,62 @@ class OpenAIResponsesProvider {
425
421
  let run = await this.getRun({ threadId: thread_id, runId: run_id });
426
422
  console.log(`Status: ${run.status} ${thread_id} ${run_id} (attempt ${retryCount + 1})`);
427
423
 
424
+ if (run.status === 'completed') {
425
+ return {run, completed: true};
426
+ }
427
+
428
+ if (run.status === 'failed' || run.status === 'cancelled' || run.status === 'expired') {
429
+ return {run, completed: false};
430
+ }
431
+
428
432
  const needsFunctionCall = run.output?.some(item => item.type === 'function_call');
429
433
  if (needsFunctionCall && !actionHandled) {
430
434
  if (retryCount >= maxRetries) {
435
+ console.warn('[OpenAIResponsesProvider] Max retries reached while handling function calls');
431
436
  return {run, completed: false};
432
437
  }
433
438
 
434
- const outputs = await this.handleRequiresAction(assistant, run, thread_id);
439
+ const outputs = await handleRequiresActionUtil(assistant, run);
435
440
  console.log('[OpenAIResponsesProvider] Function call outputs:', outputs);
436
441
 
437
442
  if (outputs.length > 0) {
438
- run = await this.runConversation({
439
- threadId: thread_id,
440
- assistantId: assistant.assistantId || assistant.promptId,
441
- toolOutputs: outputs
442
- });
443
-
444
- return this.checkRunStatus(assistant, thread_id, run.id, retryCount + 1, maxRetries, true);
443
+ try {
444
+ await this.submitToolOutputs({
445
+ threadId: thread_id,
446
+ runId: run_id,
447
+ toolOutputs: outputs
448
+ });
449
+
450
+ await new Promise(resolve => setTimeout(resolve, 1000));
451
+
452
+ return this.checkRunStatus(assistant, thread_id, run_id, retryCount + 1, maxRetries, true);
453
+ } catch (submitError) {
454
+ console.error('[OpenAIResponsesProvider] Error submitting tool outputs:', submitError);
455
+ if (retryCount < maxRetries) {
456
+ await new Promise(resolve => setTimeout(resolve, 2000));
457
+ return this.checkRunStatus(assistant, thread_id, run_id, retryCount + 1, maxRetries, false);
458
+ }
459
+ return {run, completed: false};
460
+ }
461
+ } else {
462
+ console.warn('[OpenAIResponsesProvider] Function calls detected but no outputs generated');
463
+ return {run, completed: false};
445
464
  }
446
465
  }
447
466
 
448
- return {run, completed: true};
467
+ if (retryCount < maxRetries) {
468
+ await new Promise(resolve => setTimeout(resolve, 1000));
469
+ return this.checkRunStatus(assistant, thread_id, run_id, retryCount + 1, maxRetries, actionHandled);
470
+ }
471
+
472
+ return {run, completed: false};
449
473
  } catch (error) {
450
- console.error('Error checking run status:', error);
474
+ console.error('[OpenAIResponsesProvider] Error checking run status:', error);
451
475
  return {run: null, completed: false};
452
476
  }
453
477
  }
454
478
 
479
+
455
480
  /**
456
481
  * Internal helpers
457
482
  */
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Tool and function call handling utilities for OpenAIResponsesProvider
3
+ */
4
+
5
+ /**
6
+ * Execute a function call and return the output format
7
+ * @param {Object} assistant - The assistant instance with executeTool method
8
+ * @param {Object} call - The function call object with name, arguments, and call_id
9
+ * @returns {Promise<Object>} Function call output in Responses API format
10
+ */
11
+ async function executeFunctionCall(assistant, call) {
12
+ try {
13
+ const name = call.name;
14
+ const args = call.arguments ? JSON.parse(call.arguments) : {};
15
+ const result = await assistant.executeTool(name, args);
16
+ return {
17
+ type: 'function_call_output',
18
+ call_id: call.call_id,
19
+ output: typeof result === 'string' ? result : JSON.stringify(result)
20
+ };
21
+ } catch (error) {
22
+ console.error('[OpenAIResponsesProvider] Tool execution failed', error);
23
+ return {
24
+ type: 'function_call_output',
25
+ call_id: call.call_id,
26
+ output: JSON.stringify({ success: false, error: error?.message || 'Tool execution failed' })
27
+ };
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Handle pending function calls from conversation items
33
+ * @param {Object} assistant - The assistant instance with executeTool method
34
+ * @param {Array} conversationItems - Array of conversation items
35
+ * @returns {Promise<Array>} Array of function call outputs
36
+ */
37
+ async function handlePendingFunctionCalls(assistant, conversationItems) {
38
+ const pendingFunctionCalls = conversationItems.filter(item => item.type === 'function_call');
39
+ const functionOutputs = conversationItems.filter(item => item.type === 'function_call_output');
40
+
41
+ const orphanedCalls = pendingFunctionCalls.filter(call =>
42
+ !functionOutputs.some(output => output.call_id === call.call_id)
43
+ );
44
+
45
+ if (orphanedCalls.length === 0) {
46
+ return [];
47
+ }
48
+
49
+ console.log(`[OpenAIResponsesProvider] Found ${orphanedCalls.length} pending function calls, handling them...`);
50
+ const outputs = [];
51
+ for (const call of orphanedCalls) {
52
+ outputs.push(await executeFunctionCall(assistant, call));
53
+ }
54
+ return outputs;
55
+ }
56
+
57
+ /**
58
+ * Handle function calls from a run output
59
+ * @param {Object} assistant - The assistant instance with executeTool method
60
+ * @param {Object} run - The run object with output array
61
+ * @returns {Promise<Array>} Array of function call outputs
62
+ */
63
+ async function handleRequiresAction(assistant, run) {
64
+ const functionCalls = run.output?.filter(item => item.type === 'function_call') || [];
65
+ if (functionCalls.length === 0) {
66
+ return [];
67
+ }
68
+
69
+ const outputs = [];
70
+ for (const call of functionCalls) {
71
+ outputs.push(await executeFunctionCall(assistant, call));
72
+ }
73
+
74
+ return outputs;
75
+ }
76
+
77
+ /**
78
+ * Transform tools from Assistants API format to Responses API format
79
+ * @param {string} variant - The API variant ('responses' or 'assistants')
80
+ * @param {Array} tools - Array of tool definitions
81
+ * @returns {Array|undefined} Transformed tools or undefined if empty
82
+ */
83
+ function transformToolsForResponsesAPI(variant, tools) {
84
+ if (variant !== 'responses' || !Array.isArray(tools) || tools.length === 0) {
85
+ return Array.isArray(tools) && tools.length > 0 ? tools : undefined;
86
+ }
87
+
88
+ return tools.map(tool => {
89
+ // If tool is in Assistants API format (type: 'function', function: {...})
90
+ if (tool.type === 'function' && tool.function) {
91
+ const transformed = {
92
+ name: tool.function.name,
93
+ type: 'function',
94
+ description: tool.function.description,
95
+ parameters: tool.function.parameters
96
+ };
97
+ if (tool.function.strict !== undefined) {
98
+ transformed.strict = tool.function.strict;
99
+ }
100
+ return transformed;
101
+ }
102
+ // If already in Responses API format, return as-is
103
+ return tool;
104
+ });
105
+ }
106
+
107
+ module.exports = {
108
+ executeFunctionCall,
109
+ handlePendingFunctionCalls,
110
+ handleRequiresAction,
111
+ transformToolsForResponsesAPI
112
+ };
113
+
@@ -41,12 +41,22 @@ const runAssistantAndWait = async ({
41
41
  }
42
42
 
43
43
  const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
44
- const { polling, ...conversationConfig } = runConfig || {};
44
+ const { polling, tools: configTools, ...conversationConfig } = runConfig || {};
45
+ const variant = provider.getVariant ? provider.getVariant() : (process.env.VARIANT || 'assistants');
46
+
47
+ // Get tool schemas from assistant if available, fallback to config tools
48
+ const tools = assistant.getToolSchemas ? assistant.getToolSchemas() : (configTools || []);
49
+
50
+ // Only pass assistant for Responses API (needed to handle pending function calls)
51
+ const runConfigWithAssistant = variant === 'responses'
52
+ ? { ...conversationConfig, assistant }
53
+ : conversationConfig;
45
54
 
46
55
  let run = await provider.runConversation({
47
56
  threadId: thread.getConversationId(),
48
57
  assistantId: thread.getAssistantId(),
49
- ...conversationConfig,
58
+ tools: tools.length > 0 ? tools : undefined,
59
+ ...runConfigWithAssistant,
50
60
  });
51
61
 
52
62
  const filter = thread.code ? { code: thread.code, active: true } : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "2.2.8",
3
+ "version": "2.2.9",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",