@peopl-health/nexus 5.45.0-dev.7136 → 5.45.0-dev.7144

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.
@@ -6,6 +6,7 @@ const { isAllowedUrl, redactDirectIdentifiers } = require('../utils/sanitizerUti
6
6
  const { getPatientInformation, updatePatientInformation, logPatientChange } = require('../services/patientService');
7
7
  const { getTriageHistory } = require('../services/triageHistoryService');
8
8
  const { getCarePlanHistory } = require('../services/carePlanHistoryService');
9
+ const { getEscalationContext } = require('../services/escalationContextService');
9
10
  const { getPatientTasks, updatePatientTask } = require('../services/patientTaskService');
10
11
  const {
11
12
  getRecommendationCarousels,
@@ -241,10 +242,25 @@ const logPatientChangeController = async (req, res) => {
241
242
  }
242
243
  };
243
244
 
245
+ const getPatientEscalationContextController = async (req, res) => {
246
+ try {
247
+ const context = await getEscalationContext({ code: req.params.code });
248
+ res.status(200).json({ success: true, context });
249
+ } catch (error) {
250
+ logger.error('Error fetching patient escalation context:', {
251
+ error: redactDirectIdentifiers(String(error.message || '')),
252
+ stack: redactDirectIdentifiers(String(error.stack || '')),
253
+ code: redactDirectIdentifiers(String(req.params?.code || '')),
254
+ });
255
+ res.status(error.statusCode || 500).json({ success: false, error: error.message });
256
+ }
257
+ };
258
+
244
259
  module.exports = {
245
260
  getPatientInfoController,
246
261
  getPatientTriageInfoController,
247
262
  getPatientCarePlanController,
263
+ getPatientEscalationContextController,
248
264
  getPatientTasksController,
249
265
  getPatientRecommendationCarouselsController,
250
266
  getPatientRecommendationCarouselImageController,
@@ -70,6 +70,7 @@ const patientRouteDefinitions = {
70
70
  'GET /:code': 'getPatientInfoController',
71
71
  'GET /triage/:code': 'getPatientTriageInfoController',
72
72
  'GET /careplan/:code': 'getPatientCarePlanController',
73
+ 'GET /escalation-context/:code': 'getPatientEscalationContextController',
73
74
  'GET /tasks/:code': 'getPatientTasksController',
74
75
  'PATCH /:code': 'updatePatientController',
75
76
  'PATCH /tasks/:code/:kind/:recordId': 'updatePatientTaskController',
@@ -214,6 +215,7 @@ const builtInControllers = {
214
215
  getPatientInfoController: patientController.getPatientInfoController,
215
216
  getPatientTriageInfoController: patientController.getPatientTriageInfoController,
216
217
  getPatientCarePlanController: patientController.getPatientCarePlanController,
218
+ getPatientEscalationContextController: patientController.getPatientEscalationContextController,
217
219
  getPatientTasksController: patientController.getPatientTasksController,
218
220
  getPatientRecommendationCarouselsController: patientController.getPatientRecommendationCarouselsController,
219
221
  getPatientRecommendationCarouselImageController: patientController.getPatientRecommendationCarouselImageController,
@@ -0,0 +1,81 @@
1
+ const { isoDay } = require('../utils/dateUtils');
2
+ const { logger } = require('../utils/logger');
3
+ const { PATIENT_CODE, redactDirectIdentifiers } = require('../utils/sanitizerUtils');
4
+ const { withTimeout } = require('../utils/timeoutUtils');
5
+ const { ensureWhatsAppFormat } = require('../helpers/twilioHelper');
6
+ const { assembleHistory } = require('../clinical/services/patientHistoryService');
7
+ const { DefaultMemoryManager } = require('../clinical/memory/DefaultMemoryManager');
8
+
9
+ const { getCarePlanHistory } = require('./carePlanHistoryService');
10
+ const { getPatientInformation } = require('./patientService');
11
+ const { getTriageHistory } = require('./triageHistoryService');
12
+
13
+ const SECTION_TIMEOUT_MS = 10000;
14
+ const TRIAGE_WINDOW_DAYS = 30;
15
+ const CARE_PLAN_LIMIT = 5;
16
+ const HISTORY_SCOPE = ['closed_cases_90d', 'recurrence_patterns', 'recent_medications', 'patient_reported_procedures'];
17
+
18
+ const httpError = (statusCode, message) => Object.assign(new Error(message), { statusCode });
19
+
20
+ function sectionReaders(code, now) {
21
+ const memoryManager = new DefaultMemoryManager();
22
+ const triageStart = isoDay(new Date(now - TRIAGE_WINDOW_DAYS * 86400000).toISOString());
23
+ return {
24
+ agentContext: () => memoryManager.getClinicalData(code),
25
+ conversation: () => memoryManager.buildContext({ thread: { code } }),
26
+ triages: () => getTriageHistory({ code, startDate: triageStart }),
27
+ carePlan: () => getCarePlanHistory({ code, limit: CARE_PLAN_LIMIT }),
28
+ history: () => assembleHistory(code, { scope: HISTORY_SCOPE }),
29
+ };
30
+ }
31
+
32
+ async function getEscalationContext({ code, now = Date.now(), timeoutMs = SECTION_TIMEOUT_MS } = {}) {
33
+ const patientCode = ensureWhatsAppFormat(String(code ?? '').trim());
34
+ if (!PATIENT_CODE.test(patientCode)) throw httpError(400, 'A valid WhatsApp ID is required');
35
+
36
+ const patient = await withTimeout(getPatientInformation(patientCode), timeoutMs, 'patient lookup');
37
+ if (!patient) throw httpError(404, 'Patient not found');
38
+
39
+ const readers = sectionReaders(patientCode, now);
40
+ const names = Object.keys(readers);
41
+ const outcomes = await Promise.allSettled(names.map((name) => withTimeout(readers[name](), timeoutMs, name)));
42
+
43
+ const context = { patientCode, asOf: new Date(now).toISOString(), degradedSections: [] };
44
+ outcomes.forEach((outcome, index) => {
45
+ const name = names[index];
46
+ if (outcome.status === 'fulfilled' && outcome.value != null) {
47
+ context[name] = outcome.value;
48
+ return;
49
+ }
50
+ context[name] = null;
51
+ context.degradedSections.push(name);
52
+ if (outcome.status === 'rejected') {
53
+ logger.error('[escalationContextService] Section reader failed', {
54
+ code: patientCode,
55
+ section: name,
56
+ error: redactDirectIdentifiers(String(outcome.reason?.message ?? outcome.reason)),
57
+ stack: redactDirectIdentifiers(String(outcome.reason?.stack ?? '')),
58
+ });
59
+ }
60
+ });
61
+
62
+ if (context.conversation) {
63
+ context.conversation = context.conversation
64
+ .filter((item) => item.type === 'message')
65
+ .map(({ role, content }) => ({ role, content }));
66
+ }
67
+ if (context.triages) {
68
+ if (context.triages.pagination?.hasNext) context.degradedSections.push('triages');
69
+ context.triages = context.triages.records;
70
+ }
71
+ if (context.carePlan) {
72
+ const { records, pagination = null, currentState = null } = context.carePlan;
73
+ const { degradedSections: carePlanDegraded = [], ...state } = currentState || {};
74
+ context.carePlan = { records, pagination, currentState: currentState ? state : null };
75
+ for (const section of carePlanDegraded) context.degradedSections.push(`carePlan.${section}`);
76
+ }
77
+
78
+ return context;
79
+ }
80
+
81
+ module.exports = { getEscalationContext };
@@ -149,6 +149,7 @@ function redactDirectIdentifiers(value) {
149
149
  }
150
150
 
151
151
  module.exports = {
152
+ PATIENT_CODE,
152
153
  decodeUnicodeEscapes,
153
154
  decodeUnicodeEscapesDeep,
154
155
  collectStringsDeep,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "5.45.0-dev.7136",
3
+ "version": "5.45.0-dev.7144",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",