@peopl-health/nexus 6.0.0-dev.702 → 6.0.0-dev.718
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.
- package/lib/clinical/AssistantProcessor.js +4 -2
- package/lib/clinical/config/cohortConfig.js +68 -0
- package/lib/clinical/helpers/cohortHelper.js +20 -0
- package/lib/controllers/cohortController.js +16 -0
- package/lib/helpers/twilioHelper.js +9 -1
- package/lib/routes/index.js +10 -0
- package/package.json +1 -1
|
@@ -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
|
|
22
|
-
if (!
|
|
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
|
};
|
package/lib/routes/index.js
CHANGED
|
@@ -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,
|