@peopl-health/nexus 6.0.0-dev.702 → 6.0.0-dev.725

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.
@@ -2,6 +2,7 @@ const { sanitizeOutput } = require('../utils/formatUtils');
2
2
 
3
3
  const { getThread } = require('../helpers/threadHelper');
4
4
  const { runAssistantWithRetries } = require('./helpers/assistantHelper');
5
+ const { overlayCohortPreset } = require('./helpers/cohortHelper');
5
6
 
6
7
  const { forkShadowTurn } = require('./services/shadowService');
7
8
  const { diverge } = require('./services/divergenceService');
@@ -18,8 +19,9 @@ class AssistantProcessor {
18
19
  setSendMessage(fn) { this.sendMessage = fn; }
19
20
 
20
21
  async resolveThread(code) {
21
- const thread = await getThread(code);
22
- if (!thread) return null;
22
+ const stored = await getThread(code);
23
+ if (!stored) return null;
24
+ const thread = await overlayCohortPreset(stored);
23
25
  const assistant = getAssistantById(thread.getAssistantId(), thread);
24
26
  return { thread, assistant };
25
27
  }
@@ -0,0 +1,68 @@
1
+ const { Config_ID } = require('../../config/airtableConfig');
2
+ const runtimeConfig = require('../../config/runtimeConfig');
3
+
4
+ const { stripWhatsAppFormat } = require('../../helpers/twilioHelper');
5
+ const { getRecordByFilter } = require('../../services/airtableService');
6
+
7
+ const { logger } = require('../../utils/logger');
8
+ const MapCache = require('../../utils/MapCache');
9
+
10
+ const COHORT_TABLE = 'clinical_cohort';
11
+ const CACHE_TTL = 5 * 60 * 1000;
12
+ const CACHE_KEY = 'cohort';
13
+ const LANES = ['preset', 'bridge', 'fhir_write', 'fhir_read'];
14
+
15
+ const cache = new MapCache({ maxSize: 1, ttl: CACHE_TTL });
16
+ let inFlightRead = null;
17
+ let generation = 0;
18
+
19
+ function isCohortEnabled() {
20
+ return runtimeConfig.get('COHORT_ENABLED', 'false') === 'true';
21
+ }
22
+
23
+ async function readRegistry() {
24
+ const records = await getRecordByFilter(Config_ID, COHORT_TABLE, 'TRUE()');
25
+ if (!records) throw new Error('cohort registry could not be read from the Config base');
26
+
27
+ const lanesByCode = new Map();
28
+ for (const row of records) {
29
+ if (!row.whatsapp_id || row.status !== 'active') continue;
30
+ lanesByCode.set(stripWhatsAppFormat(row.whatsapp_id), new Set(row.lanes || []));
31
+ }
32
+ return lanesByCode;
33
+ }
34
+
35
+ async function load() {
36
+ const cached = cache.get(CACHE_KEY);
37
+ if (cached) return cached;
38
+ if (!inFlightRead) {
39
+ const readGeneration = generation;
40
+ inFlightRead = readRegistry()
41
+ .then((lanesByCode) => {
42
+ if (readGeneration === generation) cache.set(CACHE_KEY, lanesByCode);
43
+ return lanesByCode;
44
+ })
45
+ .finally(() => { inFlightRead = null; });
46
+ }
47
+ return inFlightRead;
48
+ }
49
+
50
+ async function isInCohort(code, lane) {
51
+ if (!LANES.includes(lane)) throw new Error(`unknown cohort lane: ${lane}`);
52
+ if (!code || !isCohortEnabled()) return false;
53
+ try {
54
+ const lanes = (await load()).get(stripWhatsAppFormat(code));
55
+ return Boolean(lanes && lanes.has(lane));
56
+ } catch (error) {
57
+ logger.error('[cohortConfig] registry read failed; gate fails closed', { code, lane, error: error.message });
58
+ return false;
59
+ }
60
+ }
61
+
62
+ function clearCohortCache() {
63
+ generation += 1;
64
+ cache.clear();
65
+ inFlightRead = null;
66
+ }
67
+
68
+ module.exports = { isInCohort, clearCohortCache };
@@ -0,0 +1,20 @@
1
+ const { isInCohort } = require('../config/cohortConfig');
2
+ const { Thread } = require('../../models/threadModel');
3
+
4
+ const runtimeConfig = require('../../config/runtimeConfig');
5
+ const { logger } = require('../../utils/logger');
6
+
7
+ async function overlayCohortPreset(thread) {
8
+ const enrolled = await isInCohort(thread.code, 'preset');
9
+ if (!enrolled) return thread;
10
+ try {
11
+ const base = thread.toObject();
12
+ delete base._id;
13
+ return new Thread({ ...base, preset_id: runtimeConfig.get('COHORT_PRESET_ID', 'clinical_agent'), preset_version: null });
14
+ } catch (error) {
15
+ logger.error('[cohortHelper] overlay failed; patient stays on the control path', { code: thread.code, error: error.message });
16
+ return thread;
17
+ }
18
+ }
19
+
20
+ module.exports = { overlayCohortPreset };
@@ -0,0 +1,16 @@
1
+ const { logger } = require('../utils/logger');
2
+
3
+ const { clearCohortCache } = require('../clinical/config/cohortConfig');
4
+
5
+ const clearCohortCacheController = async (req, res) => {
6
+ try {
7
+ clearCohortCache();
8
+ logger.info('[CohortController] Cohort cache cleared', { by: req.ip });
9
+ return res.status(200).json({ success: true, message: 'Cohort cache cleared' });
10
+ } catch (error) {
11
+ logger.error('[CohortController] Failed to clear cohort cache', { error: error.message });
12
+ return res.status(500).json({ success: false, error: 'Failed to clear cohort cache' });
13
+ }
14
+ };
15
+
16
+ module.exports = { clearCohortCacheController };
@@ -121,10 +121,18 @@ const ensureWhatsAppFormat = (code) => {
121
121
  return trimmed;
122
122
  };
123
123
 
124
+ const stripWhatsAppFormat = (code) => {
125
+ const raw = String(code).trim();
126
+ const unprefixed = raw.startsWith('whatsapp:') ? raw.slice('whatsapp:'.length) : raw;
127
+ const beforeAt = unprefixed.includes('@') ? unprefixed.slice(0, unprefixed.indexOf('@')) : unprefixed;
128
+ return beforeAt.replace(/^\+/, '');
129
+ };
130
+
124
131
  module.exports = {
125
132
  convertTwilioToInternalFormat,
126
133
  downloadMediaFromTwilio,
127
134
  getMediaTypeFromContentType,
128
135
  extractTitle,
129
- ensureWhatsAppFormat
136
+ ensureWhatsAppFormat,
137
+ stripWhatsAppFormat
130
138
  };
@@ -90,6 +90,10 @@ const presetRouteDefinitions = {
90
90
  'POST /cache/clear': 'clearPresetCacheController'
91
91
  };
92
92
 
93
+ const cohortRouteDefinitions = {
94
+ 'POST /cache/clear': 'clearCohortCacheController'
95
+ };
96
+
93
97
  const createRouter = (routeDefinitions, controllers) => {
94
98
  const router = express.Router();
95
99
 
@@ -115,6 +119,7 @@ const messageController = require('../controllers/messageController');
115
119
  const messageStatusController = require('../controllers/messageStatusController');
116
120
  const patientController = require('../controllers/patientController');
117
121
  const presetController = require('../controllers/presetController');
122
+ const cohortController = require('../controllers/cohortController');
118
123
  const qualityMessageController = require('../controllers/qualityMessageController');
119
124
  const templateController = require('../controllers/templateController');
120
125
  const templateFlowController = require('../controllers/templateFlowController');
@@ -181,6 +186,9 @@ const builtInControllers = {
181
186
  // Preset controllers
182
187
  clearPresetCacheController: presetController.clearPresetCacheController,
183
188
 
189
+ // Cohort controllers
190
+ clearCohortCacheController: cohortController.clearCohortCacheController,
191
+
184
192
  // Template controllers
185
193
  createTemplate: templateController.createTemplate,
186
194
  listTemplates: templateController.listTemplates,
@@ -223,6 +231,7 @@ const setupDefaultRoutes = (app) => {
223
231
  app.use('/api/message', createRouter(messageRouteDefinitions, builtInControllers));
224
232
  app.use('/api/patient', createRouter(patientRouteDefinitions, builtInControllers));
225
233
  app.use('/api/preset', createRouter(presetRouteDefinitions, builtInControllers));
234
+ app.use('/api/cohort', createRouter(cohortRouteDefinitions, builtInControllers));
226
235
  app.use('/api/template', createRouter(templateRouteDefinitions, builtInControllers));
227
236
  app.use('/api/prescription', createRouter(prescriptionRouteDefinitions, builtInControllers));
228
237
  app.use('/api/dashboard', createRouter(dashboardRouteDefinitions, builtInControllers));
@@ -235,6 +244,7 @@ module.exports = {
235
244
  messageRoutes: messageRouteDefinitions,
236
245
  patientRoutes: patientRouteDefinitions,
237
246
  presetRoutes: presetRouteDefinitions,
247
+ cohortRoutes: cohortRouteDefinitions,
238
248
  templateRoutes: templateRouteDefinitions,
239
249
  prescriptionRoutes: prescriptionRouteDefinitions,
240
250
  dashboardRoutes: dashboardRouteDefinitions,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "6.0.0-dev.702",
3
+ "version": "6.0.0-dev.725",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",