@peopl-health/nexus 5.0.0-dev.453 → 5.0.0-dev.458
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/clinical/helpers/assistantHelper.js +1 -6
- package/lib/clinical/index.js +2 -2
- package/lib/clinical/providers/BaseLLMProvider.js +5 -16
- package/lib/clinical/providers/GatewayProvider.js +84 -0
- package/lib/clinical/providers/OpenAIResponsesProvider.js +2 -81
- package/lib/clinical/providers/OpenRouterProvider.js +143 -9
- package/lib/clinical/providers/createLLMProvider.js +2 -0
- package/lib/controllers/conversationController.js +20 -40
- package/lib/index.d.ts +19 -4
- package/lib/index.js +2 -0
- package/package.json +1 -1
- package/lib/clinical/helpers/threadRecoveryHelper.js +0 -50
|
@@ -5,7 +5,6 @@ const { withTracing } = require('../../utils/tracingDecorator');
|
|
|
5
5
|
const { logger } = require('../../utils/logger');
|
|
6
6
|
|
|
7
7
|
const { getRecordByFilter } = require('../../services/airtableService.js');
|
|
8
|
-
const { withThreadRecovery } = require('./threadRecoveryHelper');
|
|
9
8
|
|
|
10
9
|
function getCurRow(baseID, code) {
|
|
11
10
|
if (code.endsWith('@g.us')) {
|
|
@@ -29,11 +28,7 @@ const runAssistantAndWait = async ({ thread, assistant, message = null, runConfi
|
|
|
29
28
|
const { polling, tools: configTools, ...config } = runConfig;
|
|
30
29
|
const tools = assistant.getToolSchemas?.() || configTools || [];
|
|
31
30
|
|
|
32
|
-
return
|
|
33
|
-
(currentThread = thread) => provider.executeRun({ thread: currentThread, message, assistant, tools, config, polling }),
|
|
34
|
-
thread,
|
|
35
|
-
variant
|
|
36
|
-
);
|
|
31
|
+
return provider.executeRun({ thread, message, assistant, tools, config, polling });
|
|
37
32
|
};
|
|
38
33
|
|
|
39
34
|
const runAssistantWithRetries = async (thread, assistant, runConfig, patientReply = null) => {
|
package/lib/clinical/index.js
CHANGED
|
@@ -3,6 +3,7 @@ const { OpenAIResponsesProvider } = require('./providers/OpenAIResponsesProvider
|
|
|
3
3
|
const { handleFunctionCalls } = require('./providers/OpenAIResponsesProviderTools');
|
|
4
4
|
const { OpenRouterProvider } = require('./providers/OpenRouterProvider');
|
|
5
5
|
const { AnthropicProvider } = require('./providers/AnthropicProvider');
|
|
6
|
+
const { GatewayProvider } = require('./providers/GatewayProvider');
|
|
6
7
|
|
|
7
8
|
const { composePrompt, resolveTools, clearCache } = require('./services/promptComposerService');
|
|
8
9
|
const { createAssistant, preProcessMessages, switchAssistant } = require('./services/assistantService');
|
|
@@ -15,7 +16,6 @@ const { BaseAssistant } = require('./assistants/BaseAssistant');
|
|
|
15
16
|
const { AssistantProcessor } = require('./AssistantProcessor');
|
|
16
17
|
|
|
17
18
|
const { getCurRow, runAssistantAndWait, runAssistantWithRetries } = require('./helpers/assistantHelper');
|
|
18
|
-
const { withThreadRecovery } = require('./helpers/threadRecoveryHelper');
|
|
19
19
|
const { analyzeImage, buildPrompt } = require('./helpers/llmsHelper');
|
|
20
20
|
|
|
21
21
|
const { getProviderVariant, getAnthropicClient, setOpenAIProvider, getOpenAIProvider, requireOpenAIProvider, configureOpenAIProvider } = require('./config/llmConfig');
|
|
@@ -30,6 +30,7 @@ module.exports = {
|
|
|
30
30
|
handleFunctionCalls,
|
|
31
31
|
OpenRouterProvider,
|
|
32
32
|
AnthropicProvider,
|
|
33
|
+
GatewayProvider,
|
|
33
34
|
composePrompt,
|
|
34
35
|
resolveTools,
|
|
35
36
|
clearCache,
|
|
@@ -47,7 +48,6 @@ module.exports = {
|
|
|
47
48
|
getCurRow,
|
|
48
49
|
runAssistantAndWait,
|
|
49
50
|
runAssistantWithRetries,
|
|
50
|
-
withThreadRecovery,
|
|
51
51
|
analyzeImage,
|
|
52
52
|
buildPrompt,
|
|
53
53
|
getProviderVariant,
|
|
@@ -51,7 +51,7 @@ class BaseLLMProvider {
|
|
|
51
51
|
|
|
52
52
|
const toolSchemas = this._resolveToolSchemas({ assistant, toolIds, filtered, toolDescriptions });
|
|
53
53
|
const devContent = this._buildInstructions({ resolvedPrompt, toolSchemas, prePromptResult, additionalInstructions });
|
|
54
|
-
const input = this._buildInput({ context, prePromptResult, additionalInstructions, promptVariables });
|
|
54
|
+
const input = this._buildInput({ context, prePromptResult, additionalInstructions, promptVariables, modelConfig });
|
|
55
55
|
|
|
56
56
|
trace?.setPrompt({ resolved: devContent, resolvedPresetId, resolvedPresetVersion });
|
|
57
57
|
|
|
@@ -137,13 +137,13 @@ class BaseLLMProvider {
|
|
|
137
137
|
return { role: 'user', content: CONVERSATION_CONTINUATION };
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
_buildInput({ context, prePromptResult, additionalInstructions, promptVariables }) {
|
|
140
|
+
_buildInput({ context, prePromptResult, additionalInstructions, promptVariables, modelConfig = null }) {
|
|
141
141
|
let messages = this._convertItemsToApiFormat(context || []);
|
|
142
142
|
if (Array.isArray(prePromptResult?.additionalMessages) && prePromptResult.additionalMessages.length > 0) {
|
|
143
143
|
messages = [...messages, ...this._convertItemsToApiFormat(prePromptResult.additionalMessages)];
|
|
144
144
|
}
|
|
145
145
|
if (additionalInstructions) {
|
|
146
|
-
messages = [...messages, this._operatorInstructionTurn(additionalInstructions)];
|
|
146
|
+
messages = [...messages, this._operatorInstructionTurn(additionalInstructions, modelConfig)];
|
|
147
147
|
}
|
|
148
148
|
return this._sanitizeInput([...this._buildMemoryMessage(promptVariables), ...messages]);
|
|
149
149
|
}
|
|
@@ -160,12 +160,8 @@ class BaseLLMProvider {
|
|
|
160
160
|
throw new Error(`${this.constructor.name} must implement _invokeModel()`);
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
async listMessages() {
|
|
168
|
-
throw new Error(`listMessages is not supported by ${this.constructor.name}`);
|
|
163
|
+
_mapModelConfig(modelConfig) {
|
|
164
|
+
return modelConfig;
|
|
169
165
|
}
|
|
170
166
|
|
|
171
167
|
async transcribeAudio() {
|
|
@@ -420,13 +416,6 @@ class BaseLLMProvider {
|
|
|
420
416
|
if (segments.length > 1) logger.debug(`[${this.constructor.name}] Concatenated transcript detected, using last segment`, { segmentCount: segments.length });
|
|
421
417
|
return segments[segments.length - 1].trim();
|
|
422
418
|
}
|
|
423
|
-
|
|
424
|
-
_ensureId(value) {
|
|
425
|
-
if (!value) throw new Error('Identifier value is required');
|
|
426
|
-
if (typeof value === 'string') return value;
|
|
427
|
-
if (value?.id) return value.id;
|
|
428
|
-
throw new Error('Unable to resolve identifier value');
|
|
429
|
-
}
|
|
430
419
|
}
|
|
431
420
|
|
|
432
421
|
module.exports = {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const { composePrompt } = require('../services/promptComposerService');
|
|
2
|
+
|
|
3
|
+
const { BaseLLMProvider } = require('./BaseLLMProvider');
|
|
4
|
+
|
|
5
|
+
const VENDOR_VARIANT = {
|
|
6
|
+
openai: 'responses',
|
|
7
|
+
anthropic: 'anthropic',
|
|
8
|
+
openrouter: 'openrouter',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
class GatewayProvider extends BaseLLMProvider {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
super(options);
|
|
14
|
+
this.variant = 'gateway';
|
|
15
|
+
this.supportsPromptRegistry = true;
|
|
16
|
+
this._subs = {};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
_defaultSub() {
|
|
20
|
+
return this._sub('openai');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
getClient() {
|
|
24
|
+
return this._defaultSub().getClient();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
transcribeAudio(options) {
|
|
28
|
+
return this._defaultSub().transcribeAudio(options);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_operatorInstructionTurn(additionalInstructions, modelConfig = null) {
|
|
32
|
+
const { vendor } = this._route(modelConfig?.model);
|
|
33
|
+
return this._sub(vendor)._operatorInstructionTurn(additionalInstructions);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
_route(model) {
|
|
37
|
+
if (!model) return { vendor: 'openai', model: undefined };
|
|
38
|
+
const slash = model.indexOf('/');
|
|
39
|
+
if (slash === -1) return { vendor: 'openai', model };
|
|
40
|
+
const vendor = model.slice(0, slash);
|
|
41
|
+
if (vendor === 'openai' || vendor === 'anthropic' || vendor === 'openrouter') {
|
|
42
|
+
return { vendor, model: model.slice(slash + 1) };
|
|
43
|
+
}
|
|
44
|
+
return { vendor: 'openrouter', model };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_sub(vendor) {
|
|
48
|
+
if (this._subs[vendor]) return this._subs[vendor];
|
|
49
|
+
const variant = VENDOR_VARIANT[vendor];
|
|
50
|
+
const { createLLMProvider } = require('./createLLMProvider');
|
|
51
|
+
try {
|
|
52
|
+
this._subs[vendor] = createLLMProvider({
|
|
53
|
+
variant,
|
|
54
|
+
conversationManager: this.conversationManager,
|
|
55
|
+
sessionManager: this.sessionManager,
|
|
56
|
+
});
|
|
57
|
+
} catch (err) {
|
|
58
|
+
throw new Error(`Gateway routed a model to '${vendor}' but its provider could not be created: ${err.message}`);
|
|
59
|
+
}
|
|
60
|
+
return this._subs[vendor];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async _invokeModel(request, options = {}) {
|
|
64
|
+
const { vendor, model } = this._route(request?.modelConfig?.model);
|
|
65
|
+
const modelConfig = { ...request.modelConfig, model };
|
|
66
|
+
return this._sub(vendor)._invokeModel({ ...request, modelConfig }, options);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async runStructured(params = {}) {
|
|
70
|
+
const { promptId = null, presetId = null, variables = null } = params;
|
|
71
|
+
let model = null;
|
|
72
|
+
if (promptId || presetId) {
|
|
73
|
+
const composed = await composePrompt({ promptId, presetId, variables });
|
|
74
|
+
model = composed.modelConfig?.model || null;
|
|
75
|
+
}
|
|
76
|
+
const { vendor, model: routedModel } = this._route(model);
|
|
77
|
+
if (vendor !== 'openai') {
|
|
78
|
+
throw new Error(`runStructured is only supported for OpenAI models; routed model '${model}' resolved to '${vendor}'`);
|
|
79
|
+
}
|
|
80
|
+
return this._sub('openai').runStructured({ ...params, model: routedModel });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = { GatewayProvider };
|
|
@@ -10,9 +10,6 @@ const { logger } = require('../../utils/logger');
|
|
|
10
10
|
const { BaseLLMProvider } = require('./BaseLLMProvider');
|
|
11
11
|
const { handleFunctionCalls } = require('./OpenAIResponsesProviderTools');
|
|
12
12
|
|
|
13
|
-
const CONVERSATION_PREFIX = 'conv_';
|
|
14
|
-
const MAX_ITEMS_ON_CREATE = 20;
|
|
15
|
-
const MAX_ITEMS_PER_BATCH = 20;
|
|
16
13
|
const DEFAULT_MAX_FUNCTION_ROUNDS = 5;
|
|
17
14
|
|
|
18
15
|
class OpenAIResponsesProvider extends BaseLLMProvider {
|
|
@@ -46,76 +43,12 @@ class OpenAIResponsesProvider extends BaseLLMProvider {
|
|
|
46
43
|
this.supportsPromptRegistry = true;
|
|
47
44
|
|
|
48
45
|
this.responses = this.client.responses;
|
|
49
|
-
this.conversations = this.client.conversations;
|
|
50
46
|
}
|
|
51
47
|
|
|
52
48
|
_operatorInstructionTurn(additionalInstructions) {
|
|
53
49
|
return { role: 'developer', content: `[Instrucción del operador para tu próxima respuesta al paciente]: ${additionalInstructions}` };
|
|
54
50
|
}
|
|
55
51
|
|
|
56
|
-
async createConversation({ metadata, messages = [], toolResources } = {}) {
|
|
57
|
-
const maxHistoricalMessages = this.conversationManager.maxHistoricalMessages;
|
|
58
|
-
const capped = messages.length > maxHistoricalMessages;
|
|
59
|
-
const messagesToProcess = capped ? messages.slice(-maxHistoricalMessages) : messages;
|
|
60
|
-
if (capped) {
|
|
61
|
-
logger.warn(`[createConversation] Capped ${messages.length} → ${maxHistoricalMessages} messages`);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const allItems = this._convertItemsToApiFormat(messagesToProcess);
|
|
65
|
-
const initialItems = allItems.slice(0, MAX_ITEMS_ON_CREATE);
|
|
66
|
-
const remainingItems = allItems.slice(MAX_ITEMS_ON_CREATE);
|
|
67
|
-
|
|
68
|
-
if (remainingItems.length) {
|
|
69
|
-
logger.info(`[createConversation] Batching: ${initialItems.length} initial + ${remainingItems.length} remaining`);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const conversation = await this._createConversationWithItems(metadata, initialItems, toolResources);
|
|
73
|
-
|
|
74
|
-
if (remainingItems.length) {
|
|
75
|
-
try {
|
|
76
|
-
await this._addItemsInBatches(conversation.id, remainingItems);
|
|
77
|
-
} catch (error) {
|
|
78
|
-
logger.error('[createConversation] Failed to add remaining messages', { error: error.message });
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return conversation;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async _createConversationWithItems(metadata, items, toolResources) {
|
|
86
|
-
const payload = {
|
|
87
|
-
metadata,
|
|
88
|
-
items: items.length ? items : undefined,
|
|
89
|
-
tool_resources: toolResources
|
|
90
|
-
};
|
|
91
|
-
const { result } = await retryWithBackoff(
|
|
92
|
-
() => this.conversations.create(payload),
|
|
93
|
-
{ providerName: this.constructor.name }
|
|
94
|
-
);
|
|
95
|
-
return result;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async _addItemsInBatches(threadId, items, batchSize = MAX_ITEMS_PER_BATCH) {
|
|
99
|
-
if (!items?.length) return;
|
|
100
|
-
|
|
101
|
-
const id = this._ensurethreadId(threadId);
|
|
102
|
-
for (let i = 0; i < items.length; i += batchSize) {
|
|
103
|
-
const batch = this._convertItemsToApiFormat(items.slice(i, i + batchSize));
|
|
104
|
-
await retryWithBackoff(
|
|
105
|
-
() => this.conversations.items.create(id, { items: batch }),
|
|
106
|
-
{ providerName: this.constructor.name }
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
logger.info(`[_addItemsInBatches] Added ${items.length} messages in ${Math.ceil(items.length / batchSize)} batches`);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async listMessages({ threadId, order = 'desc', limit } = {}) {
|
|
113
|
-
return this.client.conversations.items.list(
|
|
114
|
-
this._ensurethreadId(threadId),
|
|
115
|
-
{ order, limit }
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
52
|
async _invokeModel({
|
|
120
53
|
instructions, input, toolSchemas = [], toolChoice = 'auto', followUpToolChoice = 'auto',
|
|
121
54
|
modelConfig, metadata = {}, assistant, phiProcessor = null,
|
|
@@ -200,7 +133,7 @@ class OpenAIResponsesProvider extends BaseLLMProvider {
|
|
|
200
133
|
return { response: finalResponse, toolCalls: allToolCalls, toolsExecuted: allToolsExecuted, retries: totalRetries, usage: accumulatedUsage };
|
|
201
134
|
}
|
|
202
135
|
|
|
203
|
-
async runStructured({ promptId = null, presetId = null, instructions = null, variables = null, input = [], text = null, metadata = null } = {}) {
|
|
136
|
+
async runStructured({ promptId = null, presetId = null, instructions = null, variables = null, input = [], text = null, metadata = null, model = null } = {}) {
|
|
204
137
|
let modelConfig = null;
|
|
205
138
|
let resolvedInstructions = instructions;
|
|
206
139
|
if (promptId || presetId) {
|
|
@@ -213,6 +146,7 @@ class OpenAIResponsesProvider extends BaseLLMProvider {
|
|
|
213
146
|
...this._mapModelConfig(modelConfig || {}),
|
|
214
147
|
instructions: resolvedInstructions || '',
|
|
215
148
|
};
|
|
149
|
+
if (model) apiConfig.model = model;
|
|
216
150
|
if (!apiConfig.model) apiConfig.model = this.defaults.responseModel;
|
|
217
151
|
if (text) apiConfig.text = text;
|
|
218
152
|
if (metadata) {
|
|
@@ -227,25 +161,12 @@ class OpenAIResponsesProvider extends BaseLLMProvider {
|
|
|
227
161
|
return result;
|
|
228
162
|
}
|
|
229
163
|
|
|
230
|
-
_mapModelConfig(modelConfig) {
|
|
231
|
-
return modelConfig;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
164
|
async transcribeAudio({ file, model, language, responseFormat, temperature, prompt } = {}) {
|
|
235
165
|
return this.client.audio.transcriptions.create({
|
|
236
166
|
model: model || this.defaults.transcriptionModel,
|
|
237
167
|
file, language, response_format: responseFormat, temperature, prompt,
|
|
238
168
|
});
|
|
239
169
|
}
|
|
240
|
-
|
|
241
|
-
_ensurethreadId(value) {
|
|
242
|
-
const id = this._ensureId(value);
|
|
243
|
-
if (!id.startsWith(CONVERSATION_PREFIX)) {
|
|
244
|
-
throw new Error(`Expected conversation id to start with '${CONVERSATION_PREFIX}'`);
|
|
245
|
-
}
|
|
246
|
-
return id;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
170
|
}
|
|
250
171
|
|
|
251
172
|
module.exports = {
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
const { OpenAI } = require('openai');
|
|
2
2
|
|
|
3
|
+
const { retryWithBackoff } = require('../../utils/retryUtils');
|
|
4
|
+
const { withTiming } = require('../../utils/tracingDecorator');
|
|
5
|
+
const { safeParse } = require('../../utils/jsonUtils');
|
|
6
|
+
|
|
3
7
|
const { OpenAIResponsesProvider } = require('./OpenAIResponsesProvider');
|
|
8
|
+
const { handleFunctionCalls } = require('./OpenAIResponsesProviderTools');
|
|
4
9
|
|
|
5
10
|
const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';
|
|
6
11
|
const DEFAULT_MODEL_VENDOR = 'openai';
|
|
@@ -30,9 +35,146 @@ class OpenRouterProvider extends OpenAIResponsesProvider {
|
|
|
30
35
|
this.transcriptionClient = null;
|
|
31
36
|
}
|
|
32
37
|
|
|
38
|
+
async _invokeModel({
|
|
39
|
+
instructions, input, toolSchemas = [], toolChoice = 'auto', followUpToolChoice = 'auto',
|
|
40
|
+
modelConfig, assistant, phiProcessor = null, trace = null
|
|
41
|
+
}, options = {}) {
|
|
42
|
+
const executeTools = options.toolExecutor || handleFunctionCalls;
|
|
43
|
+
let totalRetries = 0;
|
|
44
|
+
const allToolsExecuted = [];
|
|
45
|
+
const allToolCalls = [];
|
|
46
|
+
const accumulatedUsage = { input_tokens: 0, output_tokens: 0, total_tokens: 0 };
|
|
47
|
+
const addUsage = (usage) => {
|
|
48
|
+
if (!usage) return;
|
|
49
|
+
accumulatedUsage.input_tokens += usage.prompt_tokens || 0;
|
|
50
|
+
accumulatedUsage.output_tokens += usage.completion_tokens || 0;
|
|
51
|
+
accumulatedUsage.total_tokens += usage.total_tokens || 0;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const messages = this._toChatMessages(instructions, input);
|
|
55
|
+
const apiCallConfig = { ...this._mapModelConfig(modelConfig) };
|
|
56
|
+
if (toolSchemas.length > 0) {
|
|
57
|
+
apiCallConfig.tools = toolSchemas;
|
|
58
|
+
apiCallConfig.tool_choice = toolChoice;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const makeAPICall = (msgs) => retryWithBackoff(
|
|
62
|
+
() => this.client.chat.completions.create({ messages: msgs, ...apiCallConfig }),
|
|
63
|
+
{ providerName: this.constructor.name }
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const { result: { result: response, retries }, durationMs } = await withTiming(() => makeAPICall(messages));
|
|
67
|
+
totalRetries += retries;
|
|
68
|
+
addUsage(response.usage);
|
|
69
|
+
let producingIteration = trace?.addIteration({ response, durationMs, apiCallConfig }) || null;
|
|
70
|
+
let finalResponse = response;
|
|
71
|
+
const currentMessages = [...messages];
|
|
72
|
+
|
|
73
|
+
if (assistant && toolSchemas.length > 0) {
|
|
74
|
+
apiCallConfig.tool_choice = followUpToolChoice;
|
|
75
|
+
|
|
76
|
+
for (let round = 1; round <= this.maxFunctionRounds; round++) {
|
|
77
|
+
const message = finalResponse.choices?.[0]?.message;
|
|
78
|
+
const toolCalls = message?.tool_calls || [];
|
|
79
|
+
if (!toolCalls.length) break;
|
|
80
|
+
|
|
81
|
+
const functionCalls = toolCalls.map(tc => ({
|
|
82
|
+
name: tc.function.name,
|
|
83
|
+
arguments: tc.function.arguments,
|
|
84
|
+
call_id: tc.id,
|
|
85
|
+
}));
|
|
86
|
+
allToolCalls.push(...toolCalls.map(tc => ({ name: tc.function.name, arguments: safeParse(tc.function.arguments), call_id: tc.id })));
|
|
87
|
+
const { outputs, toolsExecuted } = await executeTools(functionCalls, assistant, phiProcessor, producingIteration);
|
|
88
|
+
|
|
89
|
+
currentMessages.push(
|
|
90
|
+
message,
|
|
91
|
+
...outputs.map(o => ({ role: 'tool', tool_call_id: o.call_id, content: this._toToolContent(o.output) }))
|
|
92
|
+
);
|
|
93
|
+
allToolsExecuted.push(...toolsExecuted);
|
|
94
|
+
|
|
95
|
+
const { result: { result: followUp, retries: followUpRetries }, durationMs: followUpDurationMs } = await withTiming(() => makeAPICall(currentMessages));
|
|
96
|
+
totalRetries += followUpRetries;
|
|
97
|
+
addUsage(followUp.usage);
|
|
98
|
+
producingIteration = trace?.addIteration({ response: followUp, durationMs: followUpDurationMs, apiCallConfig }) || null;
|
|
99
|
+
finalResponse = followUp;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { response: this._normalizeResponse(finalResponse), toolCalls: allToolCalls, toolsExecuted: allToolsExecuted, retries: totalRetries, usage: accumulatedUsage };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
_toChatMessages(instructions, input) {
|
|
107
|
+
const systemParts = [];
|
|
108
|
+
if (instructions) systemParts.push(instructions);
|
|
109
|
+
|
|
110
|
+
const outputsByCallId = new Map();
|
|
111
|
+
for (const item of input || []) {
|
|
112
|
+
if (item.type === 'function_call_output') outputsByCallId.set(item.call_id, item.output);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const messages = [];
|
|
116
|
+
for (const item of input || []) {
|
|
117
|
+
const type = item.type || 'message';
|
|
118
|
+
if (type === 'function_call') {
|
|
119
|
+
if (!outputsByCallId.has(item.call_id)) continue;
|
|
120
|
+
messages.push({
|
|
121
|
+
role: 'assistant',
|
|
122
|
+
content: null,
|
|
123
|
+
tool_calls: [{ id: item.call_id, type: 'function', function: { name: item.name, arguments: item.arguments || '{}' } }],
|
|
124
|
+
});
|
|
125
|
+
messages.push({ role: 'tool', tool_call_id: item.call_id, content: this._toToolContent(outputsByCallId.get(item.call_id)) });
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (type === 'function_call_output') continue;
|
|
129
|
+
const content = item.content;
|
|
130
|
+
if (item.role === 'developer' || item.role === 'system') {
|
|
131
|
+
if (content) systemParts.push(content);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
messages.push({ role: item.role === 'assistant' ? 'assistant' : 'user', content });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const system = systemParts.join('\n\n');
|
|
138
|
+
return system ? [{ role: 'system', content: system }, ...messages] : messages;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
_toToolContent(output) {
|
|
142
|
+
return typeof output === 'string' ? output : JSON.stringify(output ?? '');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
_normalizeResponse(response) {
|
|
146
|
+
const choice = response.choices?.[0];
|
|
147
|
+
const outputText = (choice?.message?.content || '').trim();
|
|
148
|
+
const output = outputText
|
|
149
|
+
? [{ type: 'message', role: 'assistant', content: [{ type: 'output_text', text: outputText }] }]
|
|
150
|
+
: [];
|
|
151
|
+
const usage = response.usage
|
|
152
|
+
? {
|
|
153
|
+
input_tokens: response.usage.prompt_tokens || 0,
|
|
154
|
+
output_tokens: response.usage.completion_tokens || 0,
|
|
155
|
+
total_tokens: response.usage.total_tokens || 0,
|
|
156
|
+
}
|
|
157
|
+
: undefined;
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
id: response.id,
|
|
161
|
+
object: 'response',
|
|
162
|
+
model: response.model,
|
|
163
|
+
status: choice?.finish_reason === 'content_filter' ? 'refused' : 'completed',
|
|
164
|
+
stop_reason: choice?.finish_reason,
|
|
165
|
+
output,
|
|
166
|
+
output_text: outputText,
|
|
167
|
+
usage,
|
|
168
|
+
raw: response,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
33
172
|
_mapModelConfig(modelConfig) {
|
|
34
|
-
const mapped = { ...modelConfig };
|
|
173
|
+
const mapped = { ...(modelConfig || {}) };
|
|
174
|
+
if (mapped.max_tokens == null && mapped.max_output_tokens != null) mapped.max_tokens = mapped.max_output_tokens;
|
|
35
175
|
delete mapped.store;
|
|
176
|
+
delete mapped.text;
|
|
177
|
+
delete mapped.max_output_tokens;
|
|
36
178
|
mapped.model = this._toOpenRouterModel(mapped.model || this.defaults.responseModel);
|
|
37
179
|
return mapped;
|
|
38
180
|
}
|
|
@@ -54,14 +196,6 @@ class OpenRouterProvider extends OpenAIResponsesProvider {
|
|
|
54
196
|
file, language, response_format: responseFormat, temperature, prompt,
|
|
55
197
|
});
|
|
56
198
|
}
|
|
57
|
-
|
|
58
|
-
async createConversation() {
|
|
59
|
-
throw new Error('createConversation is not supported by OpenRouterProvider');
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async listMessages() {
|
|
63
|
-
throw new Error('listMessages is not supported by OpenRouterProvider');
|
|
64
|
-
}
|
|
65
199
|
}
|
|
66
200
|
|
|
67
201
|
module.exports = {
|
|
@@ -6,11 +6,13 @@ const { logger } = require('../../utils/logger');
|
|
|
6
6
|
const { OpenAIResponsesProvider } = require('./OpenAIResponsesProvider');
|
|
7
7
|
const { OpenRouterProvider } = require('./OpenRouterProvider');
|
|
8
8
|
const { AnthropicProvider } = require('./AnthropicProvider');
|
|
9
|
+
const { GatewayProvider } = require('./GatewayProvider');
|
|
9
10
|
|
|
10
11
|
const PROVIDER_VARIANTS = {
|
|
11
12
|
responses: OpenAIResponsesProvider,
|
|
12
13
|
openrouter: OpenRouterProvider,
|
|
13
14
|
anthropic: AnthropicProvider,
|
|
15
|
+
gateway: GatewayProvider,
|
|
14
16
|
};
|
|
15
17
|
|
|
16
18
|
const VARIANT_API_KEY = {
|
|
@@ -14,7 +14,7 @@ const { fetchConversationData, processConversations, startConversation } = requi
|
|
|
14
14
|
const { getMessages, countMessages, aggregateMessages } = require('../services/messageService');
|
|
15
15
|
|
|
16
16
|
const { sendMessage } = require('../core/NexusMessaging');
|
|
17
|
-
const {
|
|
17
|
+
const { getProviderVariant } = require('../clinical');
|
|
18
18
|
|
|
19
19
|
const Message = mongoose.models.Message;
|
|
20
20
|
|
|
@@ -470,65 +470,45 @@ const markMessagesAsReadController = async (req, res) => {
|
|
|
470
470
|
const getOpenAIThreadMessagesController = async (req, res) => {
|
|
471
471
|
try {
|
|
472
472
|
const { code } = req.params;
|
|
473
|
-
const {
|
|
474
|
-
|
|
475
|
-
|
|
473
|
+
const { order = 'desc' } = req.query;
|
|
474
|
+
|
|
476
475
|
if (!code) {
|
|
477
476
|
return res.status(400).json({
|
|
478
477
|
success: false,
|
|
479
478
|
error: 'Phone number is required'
|
|
480
479
|
});
|
|
481
480
|
}
|
|
482
|
-
|
|
483
|
-
|
|
481
|
+
|
|
482
|
+
const thread = await Thread.findOne({ code: code }).sort({ createdAt: -1 });
|
|
484
483
|
if (!thread) {
|
|
485
484
|
return res.status(404).json({
|
|
486
485
|
success: false,
|
|
487
|
-
error: 'No active
|
|
486
|
+
error: 'No active thread found for this phone number'
|
|
488
487
|
});
|
|
489
488
|
}
|
|
490
|
-
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
threadId: thread.conversation_id,
|
|
496
|
-
order,
|
|
497
|
-
limit: parseInt(limit),
|
|
498
|
-
...(runId && { runId })
|
|
499
|
-
};
|
|
500
|
-
let threadRecreated = false;
|
|
501
|
-
|
|
502
|
-
const messages = await withThreadRecovery(
|
|
503
|
-
async (currentThread = thread) => {
|
|
504
|
-
if (currentThread !== thread) {
|
|
505
|
-
thread = currentThread;
|
|
506
|
-
queryParams.threadId = currentThread.getConversationId();
|
|
507
|
-
threadRecreated = true;
|
|
508
|
-
}
|
|
509
|
-
return await provider.listMessages(queryParams);
|
|
510
|
-
},
|
|
511
|
-
thread,
|
|
512
|
-
variant
|
|
489
|
+
|
|
490
|
+
const parsedLimit = Math.min(Math.max(parseInt(req.query.limit) || 50, 1), 100);
|
|
491
|
+
const messages = await getMessages(
|
|
492
|
+
{ numero: code, group_id: null },
|
|
493
|
+
{ sort: { createdAt: order === 'asc' ? 1 : -1 }, limit: parsedLimit }
|
|
513
494
|
);
|
|
514
|
-
|
|
495
|
+
messages.forEach(msg => { msg.body = msg.plainBody || msg.body; });
|
|
496
|
+
|
|
515
497
|
res.status(200).json({
|
|
516
498
|
success: true,
|
|
517
|
-
variant,
|
|
499
|
+
variant: getProviderVariant(),
|
|
518
500
|
phoneNumber: code,
|
|
519
501
|
code,
|
|
520
502
|
threadId: thread.conversation_id,
|
|
521
503
|
assistantId: thread.assistant_id,
|
|
522
|
-
messages
|
|
523
|
-
|
|
524
|
-
threadRecreated,
|
|
525
|
-
pagination: { limit: parseInt(limit), order }
|
|
504
|
+
messages,
|
|
505
|
+
pagination: { limit: parsedLimit, order }
|
|
526
506
|
});
|
|
527
507
|
} catch (error) {
|
|
528
|
-
logger.error('Error fetching
|
|
529
|
-
res.status(500).json({
|
|
530
|
-
success: false,
|
|
531
|
-
error: error.message || 'Failed to fetch
|
|
508
|
+
logger.error('Error fetching thread messages:', { error: error.message });
|
|
509
|
+
res.status(500).json({
|
|
510
|
+
success: false,
|
|
511
|
+
error: error.message || 'Failed to fetch thread messages'
|
|
532
512
|
});
|
|
533
513
|
}
|
|
534
514
|
};
|
package/lib/index.d.ts
CHANGED
|
@@ -273,14 +273,29 @@ declare module '@peopl-health/nexus' {
|
|
|
273
273
|
constructor(options?: { apiKey?: string; organization?: string; client?: any; defaultModels?: Record<string, string>; conversationManager?: any });
|
|
274
274
|
getVariant(): string;
|
|
275
275
|
getClient(): any;
|
|
276
|
-
createConversation(options?: any): Promise<any>;
|
|
277
|
-
listMessages(options?: any): Promise<any>;
|
|
278
276
|
executeRun(options: { thread: any; assistant: any; message?: string; tools?: any[]; config?: any }): Promise<any>;
|
|
279
277
|
runConversation(config?: any): Promise<any>;
|
|
280
278
|
transcribeAudio(options?: any): Promise<any>;
|
|
281
279
|
}
|
|
282
280
|
|
|
283
|
-
export
|
|
281
|
+
export class GatewayProvider {
|
|
282
|
+
constructor(options?: { conversationManager?: any; sessionManager?: any; [key: string]: any });
|
|
283
|
+
getVariant(): string;
|
|
284
|
+
executeRun(options: { thread: any; assistant: any; message?: string; tools?: any[]; config?: any }): Promise<any>;
|
|
285
|
+
runConversation(config?: any): Promise<any>;
|
|
286
|
+
runStructured(params?: { promptId?: string; presetId?: string; variables?: any; input?: any[]; text?: any; metadata?: any }): Promise<any>;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export interface LLMProvider {
|
|
290
|
+
getVariant(): string;
|
|
291
|
+
getClient?(): any;
|
|
292
|
+
executeRun(options: { thread: any; assistant: any; message?: string; tools?: any[]; config?: any }): Promise<any>;
|
|
293
|
+
runConversation(config?: any): Promise<any>;
|
|
294
|
+
runStructured?(params?: { promptId?: string; presetId?: string; variables?: any; input?: any[]; text?: any; metadata?: any }): Promise<any>;
|
|
295
|
+
transcribeAudio?(options?: any): Promise<any>;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function createLLMProvider(config?: { variant?: string; [key: string]: any }): LLMProvider;
|
|
284
299
|
|
|
285
300
|
// Main Nexus Class
|
|
286
301
|
export interface NexusConfig {
|
|
@@ -324,7 +339,7 @@ declare module '@peopl-health/nexus' {
|
|
|
324
339
|
getMessaging(): NexusMessaging;
|
|
325
340
|
getStorage(): MongoStorage | null;
|
|
326
341
|
getMessageParser(): MessageParser | null;
|
|
327
|
-
getLLMProvider():
|
|
342
|
+
getLLMProvider(): LLMProvider | null;
|
|
328
343
|
}
|
|
329
344
|
|
|
330
345
|
// Utility Functions
|
package/lib/index.js
CHANGED
|
@@ -7,6 +7,7 @@ const {
|
|
|
7
7
|
OpenAIResponsesProvider,
|
|
8
8
|
OpenRouterProvider,
|
|
9
9
|
AnthropicProvider,
|
|
10
|
+
GatewayProvider,
|
|
10
11
|
configureOpenAIProvider,
|
|
11
12
|
BaseAssistant,
|
|
12
13
|
registerAssistant,
|
|
@@ -201,6 +202,7 @@ module.exports = {
|
|
|
201
202
|
OpenAIResponsesProvider,
|
|
202
203
|
OpenRouterProvider,
|
|
203
204
|
AnthropicProvider,
|
|
205
|
+
GatewayProvider,
|
|
204
206
|
BaseAssistant,
|
|
205
207
|
registerAssistant,
|
|
206
208
|
overrideGetAssistantById,
|
package/package.json
CHANGED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
const { createLLMProvider } = require('../providers/createLLMProvider');
|
|
2
|
-
const { logger } = require('../../utils/logger');
|
|
3
|
-
|
|
4
|
-
const { Thread } = require('../../models/threadModel');
|
|
5
|
-
|
|
6
|
-
const THREAD_ERROR_CODES = ['thread_not_found', 'invalid_thread_id'];
|
|
7
|
-
const THREAD_ERROR_MESSAGES = ['No thread found', 'thread does not exist', 'Invalid thread'];
|
|
8
|
-
|
|
9
|
-
const isThreadNotFoundError = (error) =>
|
|
10
|
-
error?.status === 404 ||
|
|
11
|
-
THREAD_ERROR_CODES.includes(error?.code) ||
|
|
12
|
-
THREAD_ERROR_MESSAGES.some(msg => error?.message?.includes(msg));
|
|
13
|
-
|
|
14
|
-
const recreateThread = async (thread, variant) => {
|
|
15
|
-
const provider = createLLMProvider({ variant });
|
|
16
|
-
const newConversation = await provider.createConversation({
|
|
17
|
-
metadata: { code: thread.code }
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
await Thread.updateOne(
|
|
21
|
-
{ code: thread.code },
|
|
22
|
-
{ $set: { conversation_id: newConversation.id } }
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
logger.info('[threadRecovery] Recreated', {
|
|
26
|
-
oldId: thread.conversation_id,
|
|
27
|
-
newId: newConversation.id,
|
|
28
|
-
code: thread.code
|
|
29
|
-
});
|
|
30
|
-
return Thread.findOne({ code: thread.code });
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const withThreadRecovery = async (operation, thread, variant) => {
|
|
34
|
-
try {
|
|
35
|
-
return await operation();
|
|
36
|
-
} catch (error) {
|
|
37
|
-
if (isThreadNotFoundError(error) && thread) {
|
|
38
|
-
logger.warn('[threadRecovery] Thread not found, recreating', { code: thread.code });
|
|
39
|
-
const recoveredThread = await recreateThread(thread, variant);
|
|
40
|
-
return operation(recoveredThread);
|
|
41
|
-
}
|
|
42
|
-
throw error;
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
module.exports = {
|
|
47
|
-
isThreadNotFoundError,
|
|
48
|
-
recreateThread,
|
|
49
|
-
withThreadRecovery
|
|
50
|
-
};
|