@peopl-health/nexus 4.7.0-dev.451 → 5.0.0-dev.453
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 +6 -1
- package/lib/clinical/helpers/threadRecoveryHelper.js +50 -0
- package/lib/clinical/index.js +2 -0
- package/lib/clinical/providers/BaseLLMProvider.js +15 -0
- package/lib/clinical/providers/OpenAIResponsesProvider.js +76 -0
- package/lib/clinical/providers/OpenRouterProvider.js +9 -143
- package/lib/controllers/conversationController.js +40 -20
- package/lib/index.d.ts +2 -0
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@ 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');
|
|
8
9
|
|
|
9
10
|
function getCurRow(baseID, code) {
|
|
10
11
|
if (code.endsWith('@g.us')) {
|
|
@@ -28,7 +29,11 @@ const runAssistantAndWait = async ({ thread, assistant, message = null, runConfi
|
|
|
28
29
|
const { polling, tools: configTools, ...config } = runConfig;
|
|
29
30
|
const tools = assistant.getToolSchemas?.() || configTools || [];
|
|
30
31
|
|
|
31
|
-
return
|
|
32
|
+
return withThreadRecovery(
|
|
33
|
+
(currentThread = thread) => provider.executeRun({ thread: currentThread, message, assistant, tools, config, polling }),
|
|
34
|
+
thread,
|
|
35
|
+
variant
|
|
36
|
+
);
|
|
32
37
|
};
|
|
33
38
|
|
|
34
39
|
const runAssistantWithRetries = async (thread, assistant, runConfig, patientReply = null) => {
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
};
|
package/lib/clinical/index.js
CHANGED
|
@@ -15,6 +15,7 @@ const { BaseAssistant } = require('./assistants/BaseAssistant');
|
|
|
15
15
|
const { AssistantProcessor } = require('./AssistantProcessor');
|
|
16
16
|
|
|
17
17
|
const { getCurRow, runAssistantAndWait, runAssistantWithRetries } = require('./helpers/assistantHelper');
|
|
18
|
+
const { withThreadRecovery } = require('./helpers/threadRecoveryHelper');
|
|
18
19
|
const { analyzeImage, buildPrompt } = require('./helpers/llmsHelper');
|
|
19
20
|
|
|
20
21
|
const { getProviderVariant, getAnthropicClient, setOpenAIProvider, getOpenAIProvider, requireOpenAIProvider, configureOpenAIProvider } = require('./config/llmConfig');
|
|
@@ -46,6 +47,7 @@ module.exports = {
|
|
|
46
47
|
getCurRow,
|
|
47
48
|
runAssistantAndWait,
|
|
48
49
|
runAssistantWithRetries,
|
|
50
|
+
withThreadRecovery,
|
|
49
51
|
analyzeImage,
|
|
50
52
|
buildPrompt,
|
|
51
53
|
getProviderVariant,
|
|
@@ -160,6 +160,14 @@ class BaseLLMProvider {
|
|
|
160
160
|
throw new Error(`${this.constructor.name} must implement _invokeModel()`);
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
async createConversation() {
|
|
164
|
+
throw new Error(`createConversation is not supported by ${this.constructor.name}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async listMessages() {
|
|
168
|
+
throw new Error(`listMessages is not supported by ${this.constructor.name}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
163
171
|
async transcribeAudio() {
|
|
164
172
|
throw new Error(`transcribeAudio is not supported by ${this.constructor.name}`);
|
|
165
173
|
}
|
|
@@ -412,6 +420,13 @@ class BaseLLMProvider {
|
|
|
412
420
|
if (segments.length > 1) logger.debug(`[${this.constructor.name}] Concatenated transcript detected, using last segment`, { segmentCount: segments.length });
|
|
413
421
|
return segments[segments.length - 1].trim();
|
|
414
422
|
}
|
|
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
|
+
}
|
|
415
430
|
}
|
|
416
431
|
|
|
417
432
|
module.exports = {
|
|
@@ -10,6 +10,9 @@ 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;
|
|
13
16
|
const DEFAULT_MAX_FUNCTION_ROUNDS = 5;
|
|
14
17
|
|
|
15
18
|
class OpenAIResponsesProvider extends BaseLLMProvider {
|
|
@@ -43,12 +46,76 @@ class OpenAIResponsesProvider extends BaseLLMProvider {
|
|
|
43
46
|
this.supportsPromptRegistry = true;
|
|
44
47
|
|
|
45
48
|
this.responses = this.client.responses;
|
|
49
|
+
this.conversations = this.client.conversations;
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
_operatorInstructionTurn(additionalInstructions) {
|
|
49
53
|
return { role: 'developer', content: `[Instrucción del operador para tu próxima respuesta al paciente]: ${additionalInstructions}` };
|
|
50
54
|
}
|
|
51
55
|
|
|
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
|
+
|
|
52
119
|
async _invokeModel({
|
|
53
120
|
instructions, input, toolSchemas = [], toolChoice = 'auto', followUpToolChoice = 'auto',
|
|
54
121
|
modelConfig, metadata = {}, assistant, phiProcessor = null,
|
|
@@ -170,6 +237,15 @@ class OpenAIResponsesProvider extends BaseLLMProvider {
|
|
|
170
237
|
file, language, response_format: responseFormat, temperature, prompt,
|
|
171
238
|
});
|
|
172
239
|
}
|
|
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
|
+
|
|
173
249
|
}
|
|
174
250
|
|
|
175
251
|
module.exports = {
|
|
@@ -1,11 +1,6 @@
|
|
|
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
|
-
|
|
7
3
|
const { OpenAIResponsesProvider } = require('./OpenAIResponsesProvider');
|
|
8
|
-
const { handleFunctionCalls } = require('./OpenAIResponsesProviderTools');
|
|
9
4
|
|
|
10
5
|
const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';
|
|
11
6
|
const DEFAULT_MODEL_VENDOR = 'openai';
|
|
@@ -35,146 +30,9 @@ class OpenRouterProvider extends OpenAIResponsesProvider {
|
|
|
35
30
|
this.transcriptionClient = null;
|
|
36
31
|
}
|
|
37
32
|
|
|
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
|
-
|
|
172
33
|
_mapModelConfig(modelConfig) {
|
|
173
|
-
const mapped = { ...
|
|
174
|
-
if (mapped.max_tokens == null && mapped.max_output_tokens != null) mapped.max_tokens = mapped.max_output_tokens;
|
|
34
|
+
const mapped = { ...modelConfig };
|
|
175
35
|
delete mapped.store;
|
|
176
|
-
delete mapped.text;
|
|
177
|
-
delete mapped.max_output_tokens;
|
|
178
36
|
mapped.model = this._toOpenRouterModel(mapped.model || this.defaults.responseModel);
|
|
179
37
|
return mapped;
|
|
180
38
|
}
|
|
@@ -196,6 +54,14 @@ class OpenRouterProvider extends OpenAIResponsesProvider {
|
|
|
196
54
|
file, language, response_format: responseFormat, temperature, prompt,
|
|
197
55
|
});
|
|
198
56
|
}
|
|
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
|
+
}
|
|
199
65
|
}
|
|
200
66
|
|
|
201
67
|
module.exports = {
|
|
@@ -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 { getProviderVariant } = require('../clinical');
|
|
17
|
+
const { withThreadRecovery, getProviderVariant, getOpenAIProvider } = require('../clinical');
|
|
18
18
|
|
|
19
19
|
const Message = mongoose.models.Message;
|
|
20
20
|
|
|
@@ -470,45 +470,65 @@ const markMessagesAsReadController = async (req, res) => {
|
|
|
470
470
|
const getOpenAIThreadMessagesController = async (req, res) => {
|
|
471
471
|
try {
|
|
472
472
|
const { code } = req.params;
|
|
473
|
-
const { order = 'desc' } = req.query;
|
|
474
|
-
|
|
473
|
+
const { limit = 50, order = 'desc', runId } = req.query;
|
|
474
|
+
const variant = getProviderVariant();
|
|
475
|
+
|
|
475
476
|
if (!code) {
|
|
476
477
|
return res.status(400).json({
|
|
477
478
|
success: false,
|
|
478
479
|
error: 'Phone number is required'
|
|
479
480
|
});
|
|
480
481
|
}
|
|
481
|
-
|
|
482
|
-
|
|
482
|
+
|
|
483
|
+
let thread = await Thread.findOne({ code: code }).sort({ createdAt: -1 });
|
|
483
484
|
if (!thread) {
|
|
484
485
|
return res.status(404).json({
|
|
485
486
|
success: false,
|
|
486
|
-
error: 'No active thread found for this phone number'
|
|
487
|
+
error: 'No active OpenAI thread found for this phone number'
|
|
487
488
|
});
|
|
488
489
|
}
|
|
489
|
-
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
490
|
+
|
|
491
|
+
const provider = getOpenAIProvider({ instantiate: true, variant });
|
|
492
|
+
if (!provider) throw new Error('OpenAI provider not initialized');
|
|
493
|
+
|
|
494
|
+
const queryParams = {
|
|
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
|
|
494
513
|
);
|
|
495
|
-
|
|
496
|
-
|
|
514
|
+
|
|
497
515
|
res.status(200).json({
|
|
498
516
|
success: true,
|
|
499
|
-
variant
|
|
517
|
+
variant,
|
|
500
518
|
phoneNumber: code,
|
|
501
519
|
code,
|
|
502
520
|
threadId: thread.conversation_id,
|
|
503
521
|
assistantId: thread.assistant_id,
|
|
504
|
-
messages,
|
|
505
|
-
|
|
522
|
+
messages: messages.data || messages,
|
|
523
|
+
hasMore: messages.has_more || false,
|
|
524
|
+
threadRecreated,
|
|
525
|
+
pagination: { limit: parseInt(limit), order }
|
|
506
526
|
});
|
|
507
527
|
} catch (error) {
|
|
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'
|
|
528
|
+
logger.error('Error fetching OpenAI thread messages:', { error: error.message });
|
|
529
|
+
res.status(500).json({
|
|
530
|
+
success: false,
|
|
531
|
+
error: error.message || 'Failed to fetch OpenAI thread messages'
|
|
512
532
|
});
|
|
513
533
|
}
|
|
514
534
|
};
|
package/lib/index.d.ts
CHANGED
|
@@ -273,6 +273,8 @@ 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>;
|
|
276
278
|
executeRun(options: { thread: any; assistant: any; message?: string; tools?: any[]; config?: any }): Promise<any>;
|
|
277
279
|
runConversation(config?: any): Promise<any>;
|
|
278
280
|
transcribeAudio(options?: any): Promise<any>;
|