@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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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:
|
|
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();
|
package/lib/utils/retryUtils.js
CHANGED
|
@@ -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;
|