@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 null;
50
+ return degraded('no_preset');
40
51
  }
41
52
  const profile = await readClinicalProfile(code);
42
53
  if (!profile) {
43
- logger.warn('[clinicalPatternFloor] The clinical profile is missing or unreadable; the check is skipped', { turnId, profile: profile === undefined ? 'unreadable' : 'missing_or_unparseable' });
44
- return null;
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') throw new Error(`pattern floor incomplete: status=${result.status}`);
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 null;
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 == null && parsed.pattern == null) return null;
58
- if (!FLOOR_LEVELS.includes(parsed.min_level) || !PATTERNS.includes(parsed.pattern)) {
59
- logger.warn('[clinicalPatternFloor] The pattern check returned an unsupported level or pattern', { turnId, minLevel: parsed.min_level, pattern: parsed.pattern });
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 { level: parsed.min_level, pattern: parsed.pattern, evidence: cleanText(parsed.evidence), reason: cleanText(parsed.reason) };
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 null;
99
+ return degraded(timedOut ? 'timeout' : 'error');
77
100
  }
78
101
  }
79
102
 
80
- module.exports = { clinicalPatternFloor, buildPatternInput, PATTERNS };
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 FLOOR_LEVEL = 'asap';
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 null;
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 null;
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 null;
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
- level: FLOOR_LEVEL,
33
- runId: latest.runId,
34
- triageRecordId: latest.triageRecordId,
35
- escalationRoute: latest.escalationRoute,
36
- maxActionTimeHours: hours,
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 gateFloor = triageGateFloorLive() && runtime.patientCode ? await activeTriageGateFloor(runtime.patientCode) : null;
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 patternFloor = clinicalPatternFloorLive() && runtime.patientCode && (URGENCY_RANK[flooredLevel] || 0) < URGENCY_RANK.stat
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
- if (agentLevel !== 'routine') {
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: agentLevel,
410
+ urgencyLevel: netLevel,
406
411
  symptom: details.symptom || '',
407
412
  suspectedCondition: details.suspected_condition || null,
408
413
  checkIn: args?.patient_check_in || null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "5.66.0",
3
+ "version": "5.67.0-dev.11670",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",