@peopl-health/nexus 4.5.22 → 4.5.24
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/adapters/TwilioProvider.js +8 -0
- package/lib/controllers/templateController.js +0 -2
- package/lib/core/AssistantProcessor.js +6 -6
- package/lib/core/NexusMessaging.js +1 -10
- package/lib/helpers/assistantHelper.js +5 -2
- package/lib/index.d.ts +2 -2
- package/lib/models/templateModel.js +0 -1
- package/lib/providers/OpenAIResponsesProvider.js +23 -13
- package/lib/services/templateService.js +0 -1
- package/lib/utils/formatUtils.js +17 -4
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@ const { generatePresignedUrl } = require('../config/awsConfig');
|
|
|
9
9
|
const { sanitizeMediaFilename } = require('../utils/sanitizerUtils');
|
|
10
10
|
const { validateMedia, getMediaType, MEDIA_LIMITS, STICKER_DIMENSIONS } = require('../utils/mediaValidator');
|
|
11
11
|
const { logger } = require('../utils/logger');
|
|
12
|
+
const { isJsonBlob } = require('../utils/formatUtils');
|
|
12
13
|
const { calculateDelay } = require('../utils/scheduleUtils');
|
|
13
14
|
const { isBenchMode } = require('../utils/benchModeHelper');
|
|
14
15
|
|
|
@@ -107,6 +108,13 @@ class TwilioProvider extends MessageProvider {
|
|
|
107
108
|
messageParams.contentVariables = JSON.stringify(formattedVariables);
|
|
108
109
|
}
|
|
109
110
|
} else if (body) {
|
|
111
|
+
if (isJsonBlob(body)) {
|
|
112
|
+
logger.error('[TwilioProvider] Blocked raw JSON body from being sent to patient', {
|
|
113
|
+
code: formattedCode,
|
|
114
|
+
preview: body.slice(0, 120)
|
|
115
|
+
});
|
|
116
|
+
throw new Error('Refusing to send raw JSON as message body');
|
|
117
|
+
}
|
|
110
118
|
messageParams.body = body;
|
|
111
119
|
}
|
|
112
120
|
|
|
@@ -232,7 +232,6 @@ const submitForApproval = async (req, res) => {
|
|
|
232
232
|
status: 'PENDING',
|
|
233
233
|
approvalRequest: {
|
|
234
234
|
sid: response.sid,
|
|
235
|
-
status: response.status || 'PENDING',
|
|
236
235
|
dateSubmitted: validSubmittedDate,
|
|
237
236
|
dateUpdated: validUpdatedDate,
|
|
238
237
|
rejectionReason: response.rejection_reason || ''
|
|
@@ -282,7 +281,6 @@ const checkApprovalStatus = async (req, res) => {
|
|
|
282
281
|
);
|
|
283
282
|
dbTemplate.approvalRequest = {
|
|
284
283
|
sid: status.approvalRequest.sid,
|
|
285
|
-
status: status.approvalRequest.status,
|
|
286
284
|
dateSubmitted,
|
|
287
285
|
dateUpdated: parseDate(
|
|
288
286
|
status.approvalRequest.date_updated || status.approvalRequest.dateUpdated || status.content.dateUpdated,
|
|
@@ -20,9 +20,9 @@ class AssistantProcessor {
|
|
|
20
20
|
return { thread, assistant };
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
async executeLLM(thread, assistant, runOptions = {},
|
|
23
|
+
async executeLLM(thread, assistant, runOptions = {}, message = null) {
|
|
24
24
|
const startTime = Date.now();
|
|
25
|
-
const runResult = await runAssistantWithRetries(thread, assistant, { ...runOptions, phiProcessor: this.phiProcessor },
|
|
25
|
+
const runResult = await runAssistantWithRetries(thread, assistant, { ...runOptions, phiProcessor: this.phiProcessor }, message);
|
|
26
26
|
const predictionTimeMs = Date.now() - startTime;
|
|
27
27
|
|
|
28
28
|
const output = sanitizeOutput(runResult?.output);
|
|
@@ -41,19 +41,19 @@ class AssistantProcessor {
|
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
async process({ code, runOptions = {},
|
|
44
|
+
async process({ code, runOptions = {}, message = null }) {
|
|
45
45
|
if (!code) throw new Error('code is required for assistant processing');
|
|
46
46
|
|
|
47
47
|
return (this.mode === 'queue')
|
|
48
48
|
? await this._executeViaQueue({ code, runOptions })
|
|
49
|
-
: await this._executeLocal({ code, runOptions,
|
|
49
|
+
: await this._executeLocal({ code, runOptions, message });
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
async _executeLocal({ code, runOptions = {},
|
|
52
|
+
async _executeLocal({ code, runOptions = {}, message = null }) {
|
|
53
53
|
const resolved = await this.resolveThread(code);
|
|
54
54
|
if (!resolved) return null;
|
|
55
55
|
|
|
56
|
-
const result = await this.executeLLM(resolved.thread, resolved.assistant, runOptions,
|
|
56
|
+
const result = await this.executeLLM(resolved.thread, resolved.assistant, runOptions, message);
|
|
57
57
|
if (this.storeRunMetrics) await this.storeRunMetrics(code, resolved.thread, result);
|
|
58
58
|
return result;
|
|
59
59
|
}
|
|
@@ -507,7 +507,7 @@ class NexusMessaging {
|
|
|
507
507
|
const result = await this.assistantProcessor.executeLLM(
|
|
508
508
|
resolved.thread, resolved.assistant,
|
|
509
509
|
{ prePromptResult: preProcessResult },
|
|
510
|
-
preProcessed.messages
|
|
510
|
+
preProcessed.messages[0]
|
|
511
511
|
);
|
|
512
512
|
|
|
513
513
|
if (storeRunMetrics) await storeRunMetrics(chatId, resolved.thread, result, preProcessed.timings);
|
|
@@ -518,7 +518,6 @@ class NexusMessaging {
|
|
|
518
518
|
}
|
|
519
519
|
|
|
520
520
|
async processInstruction(code, instruction, role = 'developer', { triggeredBy } = {}) {
|
|
521
|
-
const assistantId = await this._getThreadAssistantId(code);
|
|
522
521
|
const messageData = {
|
|
523
522
|
pushName: 'Instruction',
|
|
524
523
|
code,
|
|
@@ -527,7 +526,6 @@ class NexusMessaging {
|
|
|
527
526
|
fromMe: true,
|
|
528
527
|
processed: true,
|
|
529
528
|
origin: 'instruction',
|
|
530
|
-
assistantId,
|
|
531
529
|
raw: { role },
|
|
532
530
|
triggeredBy: triggeredBy || null,
|
|
533
531
|
silent: true,
|
|
@@ -556,7 +554,6 @@ class NexusMessaging {
|
|
|
556
554
|
const thread = await Thread.findOne({ code }).lean();
|
|
557
555
|
if (!thread) return null;
|
|
558
556
|
|
|
559
|
-
const assistantId = await this._getThreadAssistantId(code);
|
|
560
557
|
const normalizedMessages = Array.isArray(messages) ? messages : [messages];
|
|
561
558
|
|
|
562
559
|
for (let i = 0; i < normalizedMessages.length; i++) {
|
|
@@ -569,7 +566,6 @@ class NexusMessaging {
|
|
|
569
566
|
fromMe: true,
|
|
570
567
|
processed: true,
|
|
571
568
|
origin: 'system',
|
|
572
|
-
assistantId,
|
|
573
569
|
raw: { role },
|
|
574
570
|
triggeredBy: triggeredBy || null,
|
|
575
571
|
silent: true,
|
|
@@ -597,11 +593,6 @@ class NexusMessaging {
|
|
|
597
593
|
return result?.output || null;
|
|
598
594
|
}
|
|
599
595
|
|
|
600
|
-
async _getThreadAssistantId(code) {
|
|
601
|
-
const thread = await Thread.findOne({ code }).select('assistant_id prompt_id').lean();
|
|
602
|
-
return thread?.prompt_id || thread?.assistant_id || null;
|
|
603
|
-
}
|
|
604
|
-
|
|
605
596
|
async _processMessages(chatId, processingFn, shouldFinalize = () => true) {
|
|
606
597
|
const query = { numero: chatId, from_me: false, processed: false };
|
|
607
598
|
const unprocessed = await getMessages(query, { select: '_id' });
|
|
@@ -22,14 +22,17 @@ const runAssistantAndWait = async ({ thread, assistant, message = null, runConfi
|
|
|
22
22
|
throw new Error('runAssistantAndWait requires thread and assistant');
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
if (Array.isArray(message)) {
|
|
26
|
+
throw new Error('runAssistantAndWait requires message to be a single element, not an array');
|
|
27
|
+
}
|
|
28
|
+
|
|
25
29
|
const variant = getProviderVariant();
|
|
26
30
|
const provider = createLLMProvider({ variant });
|
|
27
31
|
const { polling, tools: configTools, ...config } = runConfig;
|
|
28
32
|
const tools = assistant.getToolSchemas?.() || configTools || [];
|
|
29
|
-
const messageForRun = Array.isArray(message) && message.length > 0 ? message[0] : message;
|
|
30
33
|
|
|
31
34
|
return withThreadRecovery(
|
|
32
|
-
(currentThread = thread) => provider.executeRun({ thread: currentThread, message
|
|
35
|
+
(currentThread = thread) => provider.executeRun({ thread: currentThread, message, assistant, tools, config, polling }),
|
|
33
36
|
thread,
|
|
34
37
|
variant
|
|
35
38
|
);
|
package/lib/index.d.ts
CHANGED
|
@@ -595,7 +595,7 @@ declare module '@peopl-health/nexus' {
|
|
|
595
595
|
export interface ProcessInput {
|
|
596
596
|
code: string;
|
|
597
597
|
runOptions?: Record<string, any>;
|
|
598
|
-
|
|
598
|
+
message?: any | null;
|
|
599
599
|
}
|
|
600
600
|
|
|
601
601
|
export interface LLMResult {
|
|
@@ -614,7 +614,7 @@ declare module '@peopl-health/nexus' {
|
|
|
614
614
|
constructor(config: AssistantProcessorConfig);
|
|
615
615
|
setSendMessage(fn: AssistantProcessorConfig['sendMessage']): void;
|
|
616
616
|
resolveThread(code: string): Promise<{ thread: any; assistant: any } | null>;
|
|
617
|
-
executeLLM(thread: any, assistant: any, runOptions?: Record<string, any>,
|
|
617
|
+
executeLLM(thread: any, assistant: any, runOptions?: Record<string, any>, message?: any): Promise<LLMResult>;
|
|
618
618
|
process(input: ProcessInput): Promise<LLMResult | null>;
|
|
619
619
|
sendResponse(code: string, result: any): Promise<string | null>;
|
|
620
620
|
}
|
|
@@ -4,6 +4,7 @@ const runtimeConfig = require('../config/runtimeConfig');
|
|
|
4
4
|
const { retryWithBackoff } = require('../utils/retryUtils');
|
|
5
5
|
const { logger } = require('../utils/logger');
|
|
6
6
|
const { getCurrentMexicoDateTime } = require('../utils/dateUtils');
|
|
7
|
+
const { isJsonBlob } = require('../utils/formatUtils');
|
|
7
8
|
|
|
8
9
|
const { DefaultMemoryManager } = require('../memory/DefaultMemoryManager');
|
|
9
10
|
|
|
@@ -167,16 +168,31 @@ class OpenAIResponsesProvider {
|
|
|
167
168
|
return typeof part.text === 'string' ? part.text : (typeof part.text?.value === 'string' ? part.text.value : '');
|
|
168
169
|
}
|
|
169
170
|
|
|
171
|
+
_messageItemToText(item) {
|
|
172
|
+
if (!item) return '';
|
|
173
|
+
if (Array.isArray(item.content)) {
|
|
174
|
+
return item.content.map(c => this._contentPartToText(c)).filter(Boolean).join('').trim();
|
|
175
|
+
}
|
|
176
|
+
if (typeof item.content === 'string') return item.content.trim();
|
|
177
|
+
return '';
|
|
178
|
+
}
|
|
179
|
+
|
|
170
180
|
_extractMessageOutput(result) {
|
|
171
181
|
if (result == null || typeof result !== 'object') return '';
|
|
172
182
|
|
|
173
183
|
if (result.output && Array.isArray(result.output)) {
|
|
174
184
|
const messageItems = result.output.filter(item => item && item.type === 'message');
|
|
175
185
|
if (messageItems.length > 0) {
|
|
176
|
-
|
|
186
|
+
const texts = messageItems.map(item => this._messageItemToText(item)).filter(Boolean);
|
|
187
|
+
const proseTexts = texts.filter(text => !isJsonBlob(text));
|
|
188
|
+
const jsonDiscarded = texts.length - proseTexts.length;
|
|
189
|
+
const keptOne = proseTexts.length > 0 ? 1 : 0;
|
|
190
|
+
|
|
191
|
+
if (messageItems.length > 1 || jsonDiscarded > 0) {
|
|
177
192
|
const hasFunctionCalls = result.output.some(item => item?.type === 'function_call');
|
|
178
|
-
logger.warn('[OpenAIResponsesProvider] Multiple message items in response; keeping the first (likely OpenAI multi-output bug)', {
|
|
179
|
-
discarded: messageItems.length -
|
|
193
|
+
logger.warn('[OpenAIResponsesProvider] Multiple/structured message items in response; keeping the first natural-language reply (likely OpenAI multi-output bug)', {
|
|
194
|
+
discarded: messageItems.length - keptOne,
|
|
195
|
+
jsonDiscarded,
|
|
180
196
|
responseId: result.id,
|
|
181
197
|
model: result.model,
|
|
182
198
|
hasFunctionCalls
|
|
@@ -186,11 +202,12 @@ class OpenAIResponsesProvider {
|
|
|
186
202
|
description: [
|
|
187
203
|
'OpenAI Responses API returned multiple message items in a single response.',
|
|
188
204
|
'Suspected upstream bug (model failed to emit stop-of-message token).',
|
|
189
|
-
`Discarded ${messageItems.length -
|
|
205
|
+
`Discarded ${messageItems.length - keptOne} extra message(s); kept the first natural-language reply as the canonical reply.`,
|
|
206
|
+
jsonDiscarded > 0 ? `${jsonDiscarded} discarded item(s) were raw JSON/structured output that must never reach the patient.` : null,
|
|
190
207
|
`Response ID: ${result.id || 'unknown'}`,
|
|
191
208
|
`Model: ${result.model || 'unknown'}`,
|
|
192
209
|
`Function calls present: ${hasFunctionCalls ? 'yes' : 'no'}`
|
|
193
|
-
].join('\n'),
|
|
210
|
+
].filter(Boolean).join('\n'),
|
|
194
211
|
severity: 'medium',
|
|
195
212
|
status: 'Open',
|
|
196
213
|
clasificacion: 'alucinaciones',
|
|
@@ -201,14 +218,7 @@ class OpenAIResponsesProvider {
|
|
|
201
218
|
}).catch((err) => logger.warn('[OpenAIResponsesProvider] Bug report logger failed', { error: err.message }));
|
|
202
219
|
}
|
|
203
220
|
|
|
204
|
-
|
|
205
|
-
let text = '';
|
|
206
|
-
if (firstMessage.content && Array.isArray(firstMessage.content)) {
|
|
207
|
-
text = firstMessage.content.map(c => this._contentPartToText(c)).filter(Boolean).join('');
|
|
208
|
-
} else if (typeof firstMessage.content === 'string') {
|
|
209
|
-
text = firstMessage.content;
|
|
210
|
-
}
|
|
211
|
-
if (text.trim()) return text.trim();
|
|
221
|
+
if (proseTexts.length > 0) return proseTexts[0];
|
|
212
222
|
}
|
|
213
223
|
}
|
|
214
224
|
if (result.output_text) return this._extractMessageFromConcatenatedOutput(result.output_text);
|
|
@@ -16,7 +16,6 @@ const refreshApprovalStatuses = async (templates) => {
|
|
|
16
16
|
const updateFields = {
|
|
17
17
|
approvalRequest: {
|
|
18
18
|
sid: reqData.sid,
|
|
19
|
-
status: reqData.status,
|
|
20
19
|
dateSubmitted: reqData.dateCreated ? new Date(reqData.dateCreated) : new Date(),
|
|
21
20
|
dateUpdated: reqData.dateUpdated ? new Date(reqData.dateUpdated) : new Date(),
|
|
22
21
|
rejectionReason: reqData.rejectionReason || ''
|
package/lib/utils/formatUtils.js
CHANGED
|
@@ -26,14 +26,27 @@ function formatForWhatsApp(text) {
|
|
|
26
26
|
|
|
27
27
|
function sanitizeOutput(text) {
|
|
28
28
|
if (!text || typeof text !== 'string') return text;
|
|
29
|
-
|
|
29
|
+
|
|
30
30
|
let sanitized = text;
|
|
31
31
|
sanitized = removeBracketContent(sanitized);
|
|
32
32
|
sanitized = formatForWhatsApp(sanitized);
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
return sanitized;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
function isJsonBlob(text) {
|
|
38
|
+
if (!text || typeof text !== 'string') return false;
|
|
39
|
+
const trimmed = text.trim();
|
|
40
|
+
if (!(trimmed.startsWith('{') || trimmed.startsWith('['))) return false;
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(trimmed);
|
|
43
|
+
return parsed !== null && typeof parsed === 'object';
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = {
|
|
50
|
+
sanitizeOutput,
|
|
51
|
+
isJsonBlob
|
|
39
52
|
};
|