@peopl-health/nexus 5.66.0 → 5.67.0-dev.11670
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.
|
@@ -10,8 +10,19 @@ const { logger } = require('../../utils/logger');
|
|
|
10
10
|
const { redactDirectIdentifiers } = require('../../utils/sanitizerUtils');
|
|
11
11
|
const { withTimeout } = require('../../utils/timeoutUtils');
|
|
12
12
|
|
|
13
|
-
const PATTERNS = ['posquirurgico_abdominal', 'toxicidad_gi_quimioterapia', 'toxicidad_gi_inmunoterapia', 'fiebre_quimioterapia'];
|
|
14
13
|
const FLOOR_LEVELS = ['asap', 'stat'];
|
|
14
|
+
const PATTERN_NAME = /^[a-z0-9_]{1,64}$/;
|
|
15
|
+
const UNLABELLED_PATTERN = 'sin_etiqueta';
|
|
16
|
+
const MAX_TEXT_LENGTH = 500;
|
|
17
|
+
|
|
18
|
+
const degraded = (reason) => ({ floor: null, degraded: reason });
|
|
19
|
+
|
|
20
|
+
function normalizePattern(raw) {
|
|
21
|
+
const name = typeof raw === 'string' ? raw.trim().toLowerCase() : '';
|
|
22
|
+
return PATTERN_NAME.test(name) ? name : UNLABELLED_PATTERN;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const clippedText = (value) => cleanText(value)?.slice(0, MAX_TEXT_LENGTH) || null;
|
|
15
26
|
|
|
16
27
|
function buildPatternInput({ profile, details, justification, today }) {
|
|
17
28
|
const indicators = Array.isArray(details.indicators) ? details.indicators.filter((item) => typeof item === 'string' && item.trim()) : [];
|
|
@@ -36,30 +47,41 @@ async function evaluatePatternFloor({ code, turnId, details, justification, prov
|
|
|
36
47
|
const { presetId } = await getEscalationPatternFloor();
|
|
37
48
|
if (!presetId) {
|
|
38
49
|
logger.info('[clinicalPatternFloor] No preset is configured; the check is skipped', { turnId });
|
|
39
|
-
return
|
|
50
|
+
return degraded('no_preset');
|
|
40
51
|
}
|
|
41
52
|
const profile = await readClinicalProfile(code);
|
|
42
53
|
if (!profile) {
|
|
43
|
-
|
|
44
|
-
|
|
54
|
+
const reason = profile === undefined ? 'profile_unreadable' : 'profile_missing';
|
|
55
|
+
logger.warn('[clinicalPatternFloor] The clinical profile is missing or unreadable; the check is skipped', { turnId, profile: reason });
|
|
56
|
+
return degraded(reason);
|
|
45
57
|
}
|
|
46
58
|
const result = await (provider || requireGatewayProvider()).runStructured({
|
|
47
59
|
presetId,
|
|
48
60
|
input: [{ role: 'user', content: buildPatternInput({ profile, details, justification, today: now.toISOString().slice(0, 10) }) }],
|
|
49
61
|
metadata: { subAgent: 'escalation-pattern-floor', turnId },
|
|
50
62
|
});
|
|
51
|
-
if (result?.status && result.status !== 'completed')
|
|
63
|
+
if (result?.status && result.status !== 'completed') {
|
|
64
|
+
logger.warn('[clinicalPatternFloor] The pattern check did not complete', { turnId, status: result.status });
|
|
65
|
+
return degraded('incomplete');
|
|
66
|
+
}
|
|
52
67
|
const parsed = safeParse(readStructuredText(result), null);
|
|
53
68
|
if (!isPlainObject(parsed)) {
|
|
54
69
|
logger.warn('[clinicalPatternFloor] The pattern check did not return a JSON object', { turnId });
|
|
55
|
-
return
|
|
70
|
+
return degraded('malformed_output');
|
|
71
|
+
}
|
|
72
|
+
if (parsed.min_level == null) {
|
|
73
|
+
if (parsed.pattern == null) return { floor: null, degraded: null };
|
|
74
|
+
logger.warn('[clinicalPatternFloor] The pattern check named a pattern without a level', { turnId, pattern: parsed.pattern });
|
|
75
|
+
return degraded('missing_level');
|
|
56
76
|
}
|
|
57
|
-
if (parsed.min_level
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return null;
|
|
77
|
+
if (!FLOOR_LEVELS.includes(parsed.min_level)) {
|
|
78
|
+
logger.warn('[clinicalPatternFloor] The pattern check returned an unsupported level', { turnId, minLevel: parsed.min_level, pattern: parsed.pattern });
|
|
79
|
+
return degraded('unsupported_level');
|
|
61
80
|
}
|
|
62
|
-
return {
|
|
81
|
+
return {
|
|
82
|
+
floor: { level: parsed.min_level, pattern: normalizePattern(parsed.pattern), evidence: clippedText(parsed.evidence), reason: clippedText(parsed.reason) },
|
|
83
|
+
degraded: null,
|
|
84
|
+
};
|
|
63
85
|
}
|
|
64
86
|
|
|
65
87
|
async function clinicalPatternFloor({ code, turnId = null, details = {}, justification = null, provider = null, now = new Date() }) {
|
|
@@ -70,11 +92,12 @@ async function clinicalPatternFloor({ code, turnId = null, details = {}, justifi
|
|
|
70
92
|
'clinical pattern floor',
|
|
71
93
|
);
|
|
72
94
|
} catch (error) {
|
|
95
|
+
const timedOut = /timed out after/.test(error?.message || '');
|
|
73
96
|
logger.error('[clinicalPatternFloor] The clinical pattern check failed', {
|
|
74
97
|
turnId, error: error.message, stack: redactDirectIdentifiers(String(error?.stack || '')),
|
|
75
98
|
});
|
|
76
|
-
return
|
|
99
|
+
return degraded(timedOut ? 'timeout' : 'error');
|
|
77
100
|
}
|
|
78
101
|
}
|
|
79
102
|
|
|
80
|
-
module.exports = { clinicalPatternFloor, buildPatternInput
|
|
103
|
+
module.exports = { clinicalPatternFloor, buildPatternInput };
|
|
@@ -5,8 +5,10 @@ const { redactDirectIdentifiers } = require('../../utils/sanitizerUtils');
|
|
|
5
5
|
|
|
6
6
|
const HOUR_MS = 60 * 60 * 1000;
|
|
7
7
|
const LOOKBACK_MS = 24 * HOUR_MS;
|
|
8
|
-
const
|
|
8
|
+
const ROUTE_FLOOR = { emergencia: 'stat', urgencias: 'asap', equipo_tratante_hoy: 'asap' };
|
|
9
|
+
const DEFAULT_FLOOR = 'asap';
|
|
9
10
|
const PROMPT_ROUTES = ['urgencias', 'emergencia'];
|
|
11
|
+
const NO_FLOOR = { floor: null, degraded: null };
|
|
10
12
|
|
|
11
13
|
const settledAt = (event) => new Date(event.createdAt).getTime();
|
|
12
14
|
const gateTime = (event) => Math.min(new Date(event.triageAuthoredAt || event.createdAt).getTime(), settledAt(event));
|
|
@@ -19,25 +21,28 @@ async function activeTriageGateFloor(code, now = new Date()) {
|
|
|
19
21
|
.lean();
|
|
20
22
|
const latest = gates.reduce((best, event) => (!best || gateTime(event) > gateTime(best)
|
|
21
23
|
|| (gateTime(event) === gateTime(best) && settledAt(event) > settledAt(best)) ? event : best), null);
|
|
22
|
-
if (!latest?.urgentUnder24h) return
|
|
24
|
+
if (!latest?.urgentUnder24h) return NO_FLOOR;
|
|
23
25
|
|
|
24
26
|
const hours = Number.isFinite(latest.maxActionTimeHours) ? latest.maxActionTimeHours : null;
|
|
25
27
|
const promptRoute = PROMPT_ROUTES.includes(latest.escalationRoute);
|
|
26
|
-
if (!promptRoute && (hours === null || hours > triageGateFloorMaxHours())) return
|
|
28
|
+
if (!promptRoute && (hours === null || hours > triageGateFloorMaxHours())) return NO_FLOOR;
|
|
27
29
|
|
|
28
|
-
const windowMs = (hours ?? 24) * HOUR_MS;
|
|
29
|
-
if (now.getTime() - gateTime(latest) > windowMs) return
|
|
30
|
+
const windowMs = Math.max(hours ?? 24, 1) * HOUR_MS;
|
|
31
|
+
if (now.getTime() - gateTime(latest) > windowMs) return NO_FLOOR;
|
|
30
32
|
|
|
31
33
|
return {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
floor: {
|
|
35
|
+
level: ROUTE_FLOOR[latest.escalationRoute] || DEFAULT_FLOOR,
|
|
36
|
+
runId: latest.runId,
|
|
37
|
+
triageRecordId: latest.triageRecordId,
|
|
38
|
+
escalationRoute: latest.escalationRoute,
|
|
39
|
+
maxActionTimeHours: hours,
|
|
40
|
+
},
|
|
41
|
+
degraded: null,
|
|
37
42
|
};
|
|
38
43
|
} catch (error) {
|
|
39
44
|
logger.error('[triageGateFloor] The latest triage gate could not be read', { error: error.message, stack: redactDirectIdentifiers(String(error?.stack || '')) });
|
|
40
|
-
return null;
|
|
45
|
+
return { floor: null, degraded: 'read_failed' };
|
|
41
46
|
}
|
|
42
47
|
}
|
|
43
48
|
|
|
@@ -313,12 +313,14 @@ async function handler(args, context = {}) {
|
|
|
313
313
|
const anchor = updateMode && review ? escalationAnchorOf(review, alarmId) : null;
|
|
314
314
|
const keptOpenLevel = Boolean(anchor) && (URGENCY_RANK[anchor.urgencyLevel] || 0) > (URGENCY_RANK[requestedLevel] || 0);
|
|
315
315
|
const agentLevel = keptOpenLevel ? anchor.urgencyLevel : requestedLevel;
|
|
316
|
-
const
|
|
316
|
+
const gateCheck = triageGateFloorLive() && runtime.patientCode ? await activeTriageGateFloor(runtime.patientCode) : null;
|
|
317
|
+
const gateFloor = gateCheck?.floor || null;
|
|
317
318
|
const raisedByGate = Boolean(gateFloor) && (URGENCY_RANK[gateFloor.level] || 0) > (URGENCY_RANK[agentLevel] || 0);
|
|
318
319
|
const flooredLevel = raisedByGate ? gateFloor.level : agentLevel;
|
|
319
|
-
const
|
|
320
|
+
const patternCheck = clinicalPatternFloorLive() && runtime.patientCode && (URGENCY_RANK[flooredLevel] || 0) < URGENCY_RANK.stat
|
|
320
321
|
? await clinicalPatternFloor({ code: runtime.patientCode, turnId: runtime.turnId, details, justification: args?.urgency_justification })
|
|
321
322
|
: null;
|
|
323
|
+
const patternFloor = patternCheck?.floor || null;
|
|
322
324
|
const raisedByPattern = Boolean(patternFloor) && (URGENCY_RANK[patternFloor.level] || 0) > (URGENCY_RANK[flooredLevel] || 0);
|
|
323
325
|
const urgencyLevel = raisedByPattern ? patternFloor.level : flooredLevel;
|
|
324
326
|
const raisesOpen = !anchor || (URGENCY_RANK[urgencyLevel] || 0) > (URGENCY_RANK[anchor.urgencyLevel] || 0);
|
|
@@ -345,6 +347,8 @@ async function handler(args, context = {}) {
|
|
|
345
347
|
...(clinicalAssessment ? { clinicalAssessment } : {}),
|
|
346
348
|
...(gateFloor ? { triageGateFloor: { ...gateFloor, raised: raisedByGate } } : {}),
|
|
347
349
|
...(patternFloor ? { clinicalPatternFloor: { ...patternFloor, raised: raisedByPattern } } : {}),
|
|
350
|
+
...(gateCheck?.degraded ? { triageGateFloorDegraded: gateCheck.degraded } : {}),
|
|
351
|
+
...(patternCheck?.degraded ? { clinicalPatternFloorDegraded: patternCheck.degraded } : {}),
|
|
348
352
|
},
|
|
349
353
|
});
|
|
350
354
|
|
|
@@ -398,11 +402,12 @@ async function handler(args, context = {}) {
|
|
|
398
402
|
onRecorded: (recordId) => { emergencyRecordId = recordId; },
|
|
399
403
|
});
|
|
400
404
|
if (review && emergencyRecordId) await attachEmergencyRow({ reviewId: review._id, alarmId, emergencyRecordId, appliedLevel: urgencyLevel });
|
|
401
|
-
|
|
405
|
+
const netLevel = raisedByGate || raisedByPattern ? urgencyLevel : agentLevel;
|
|
406
|
+
if (netLevel !== 'routine') {
|
|
402
407
|
try {
|
|
403
408
|
const net = queued ? await armEscalationNet({
|
|
404
409
|
patientCode: runtime.patientCode,
|
|
405
|
-
urgencyLevel:
|
|
410
|
+
urgencyLevel: netLevel,
|
|
406
411
|
symptom: details.symptom || '',
|
|
407
412
|
suspectedCondition: details.suspected_condition || null,
|
|
408
413
|
checkIn: args?.patient_check_in || null,
|