@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.
@@ -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 = {}, messages = null) {
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 }, messages);
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 = {}, messages = null }) {
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, messages });
49
+ : await this._executeLocal({ code, runOptions, message });
50
50
  }
51
51
 
52
- async _executeLocal({ code, runOptions = {}, messages = null }) {
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, messages);
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: messageForRun, assistant, tools, config, polling }),
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
- messages?: any[] | null;
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>, messages?: any[]): Promise<LLMResult>;
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
  }
@@ -37,7 +37,6 @@ const TemplateSchema = new mongoose.Schema({
37
37
  ],
38
38
  approvalRequest: {
39
39
  sid: String,
40
- status: String,
41
40
  dateSubmitted: Date,
42
41
  dateUpdated: Date,
43
42
  rejectionReason: String
@@ -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
- if (messageItems.length > 1) {
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 - 1,
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 - 1} extra message(s); kept the first as the canonical reply.`,
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
- const firstMessage = messageItems[0];
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 || ''
@@ -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
- module.exports = {
38
- sanitizeOutput
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "4.5.22",
3
+ "version": "4.5.24",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",