@peopl-health/nexus 5.71.9 → 5.71.11

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.
@@ -378,7 +378,8 @@ class BaseLLMProvider {
378
378
  '',
379
379
  'Si escribes, que sea para ELLA: lo que te contó, lo que quedó abierto, lo que quieres saber ahora.',
380
380
  'Tu texto llega dentro de una plantilla que ya empieza con «Hola {su nombre},»: no saludes ni escribas su nombre, entra directo a lo que quieres decirle, breve (menos de 500 caracteres) y con un emoji como máximo. Es un contacto sobre su cuidado: no le sumes invitaciones a charlas, sesiones ni actividades.',
381
- 'LÍMITE UTILITY: escribe sólo cuando puedas anclar el contacto en algo concreto que la paciente contó, preguntó o acordó, o en una interacción de cuidado todavía vigente. La inactividad por sí sola no basta. Si escribes, registra esa ancla en utility_basis; si sólo queda un saludo general para generar engagement, registra hold con hold_reason no_utility_anchor y utility_basis null.',
381
+ 'LÍMITE UTILITY: escribe sólo cuando puedas anclar el contacto en algo concreto que la paciente contó, preguntó o acordó, o en una interacción de cuidado todavía vigente. La inactividad por sí sola no basta. Si escribes, registra esa ancla en utility_basis; si sólo queda un saludo general para generar engagement, registra hold con hold_reason no_utility_anchor y utility_basis null. El mensaje tiene que nombrar esa ancla; preguntarle cómo se ha sentido o invitarla a escribir si tiene dudas no es un ancla.',
382
+ 'Debe entenderse por sí mismo: si mencionas fechas, un documento o algo que le enviaste, di cuáles. No enumeres señales de alarma; si vigilas algo concreto, menciona sólo esa señal y pídele que te escriba en cuanto aparezca.',
382
383
  'No repitas un mensaje que ya le enviaste, y no menciones automatizaciones, listas ni seguimientos programados.',
383
384
  );
384
385
 
@@ -78,11 +78,18 @@ async function extractClinicalMentions({ text, turnId, code, provider, detectedT
78
78
  const { datosClinicos, contextGaps } = await loadPatientContext(code);
79
79
  const vagueWords = await getVagueEntityWords();
80
80
 
81
- const { result, durationMs } = await withTiming(() => provider.runStructured({
82
- presetId,
83
- input: [{ role: 'user', content: buildExtractorInput(text, detectedTags, curatedRows, template, { priorTurns, patientContext: datosClinicos }) }],
84
- metadata: { extractor: 'clinical-mention', turnId },
85
- }));
81
+ let timed;
82
+ try {
83
+ timed = await withTiming(() => provider.runStructured({
84
+ presetId,
85
+ input: [{ role: 'user', content: buildExtractorInput(text, detectedTags, curatedRows, template, { priorTurns, patientContext: datosClinicos }) }],
86
+ metadata: { extractor: 'clinical-mention', turnId },
87
+ }));
88
+ } catch (err) {
89
+ logger.error('[clinicalExtraction] extractor provider unavailable; returning deterministic safety flags only', { turnId, error: err?.message });
90
+ return { completed: false, unavailable: Boolean(patterns?.length), clinicalMentions: [], safetyFlags: [...flags], safetyFlagSources: sources() };
91
+ }
92
+ const { result, durationMs } = timed;
86
93
  trace?.addSubCall({ response: result, durationMs, presetId });
87
94
 
88
95
  if (result?.status && result.status !== 'completed') throw new Error(`clinical extraction incomplete: status=${result.status}`);
@@ -38,6 +38,8 @@ function nonClinicalRequestSchema() {
38
38
  }
39
39
 
40
40
  const MAX_CONTEXT_TURNS = 6;
41
+ const EXTRACTOR_UNAVAILABLE = 'extractor_unavailable';
42
+ const ESCALATION_WHILE_UNAVAILABLE = 'described in the patient\'s words and graded on the symptom itself; never mention the extractor or a technical failure, to the patient or the team.';
41
43
 
42
44
  const definition = {
43
45
  name: 'extractClinicalInfo',
@@ -239,13 +241,19 @@ async function handler(args, context = {}) {
239
241
  });
240
242
  }
241
243
 
244
+ if (!intakeCompleted && extraction.unavailable && !safetyFlags.length && !detectedTags.includes('symptom')) {
245
+ return degradedIntake(trace);
246
+ }
247
+
242
248
  if (!intakeCompleted) {
243
249
  return JSON.stringify({
244
250
  success: false,
245
251
  error: 'clinical_intake_incomplete',
246
252
  data: {
247
253
  safety_flags_received: safetyFlags,
248
- recovery: 'Retry extractClinicalInfo with the complete patient message and all applicable supported tags.',
254
+ recovery: extraction.unavailable
255
+ ? `The clinical extractor is unavailable and a retry will not succeed. Send what the message reports to the team with reportMedicalEscalation (reportCrisis for a crisis), ${ESCALATION_WHILE_UNAVAILABLE}`
256
+ : 'Retry extractClinicalInfo with the complete patient message and all applicable supported tags.',
249
257
  },
250
258
  });
251
259
  }
@@ -312,6 +320,10 @@ async function declareNonClinical({ declaredRequest, rawMessage, intakeSource, p
312
320
  });
313
321
  }
314
322
 
323
+ if (check?.unavailable && !safetyFlags.length) {
324
+ return degradedIntake(trace, { non_clinical_request: declaredRequest });
325
+ }
326
+
315
327
  trace?.setSignals?.({ clinicalIntakeCompleted: false, clinicalMentions: [], safetyFlags });
316
328
  const supported = [...SUPPORTED_TAGS].sort();
317
329
  const reason = found
@@ -325,6 +337,20 @@ async function declareNonClinical({ declaredRequest, rawMessage, intakeSource, p
325
337
  });
326
338
  }
327
339
 
340
+ function degradedIntake(trace, data = {}) {
341
+ trace?.setSignals?.({ clinicalIntakeCompleted: true, clinicalIntakeDegraded: EXTRACTOR_UNAVAILABLE, clinicalMentions: [], safetyFlags: [] });
342
+ return JSON.stringify({
343
+ success: true,
344
+ data: {
345
+ ...data,
346
+ degraded: true,
347
+ degraded_reasons: [EXTRACTOR_UNAVAILABLE],
348
+ clinical_facts: [],
349
+ agent_hint: `The clinical extractor is unavailable, so nothing from this message was recorded as clinical information. Do not call extractClinicalInfo again this turn. A new or worsening symptom goes to the team with reportMedicalEscalation, ${ESCALATION_WHILE_UNAVAILABLE} Anything else gets a reply only.`,
350
+ },
351
+ });
352
+ }
353
+
328
354
  async function scanSafetyFlags(text) {
329
355
  try {
330
356
  const patterns = await getSafetyFlagPatterns();
@@ -53,6 +53,7 @@ function isAbortError(error) {
53
53
  function isRetryableError(error) {
54
54
  if (!error) return false;
55
55
  if (isAbortError(error)) return false;
56
+ if (error.code === 'insufficient_quota') return false;
56
57
 
57
58
  const status = error?.status;
58
59
  const code = error?.code;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "5.71.9",
3
+ "version": "5.71.11",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",