@peopl-health/nexus 5.9.0-dev.921 → 6.0.0-dev.611
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 +5 -34
- package/lib/clinical/config/divergenceConfig.js +1 -18
- package/lib/clinical/config/llmConfig.js +1 -9
- package/lib/clinical/context/ToolRuntimeContext.js +2 -1
- package/lib/clinical/helpers/divergenceHelper.js +0 -12
- package/lib/clinical/index.js +0 -2
- package/lib/clinical/memory/DefaultMemoryManager.js +9 -19
- package/lib/clinical/models/turnTraceModel.js +5 -18
- package/lib/clinical/providers/AnthropicProvider.js +29 -291
- package/lib/clinical/providers/BaseLLMProvider.js +24 -113
- package/lib/clinical/providers/GatewayProvider.js +3 -3
- package/lib/clinical/providers/OpenAIResponsesProvider.js +4 -46
- package/lib/clinical/services/divergenceService.js +5 -22
- package/lib/clinical/services/promptComposerService.js +15 -46
- package/lib/clinical/services/shadowMessageService.js +12 -28
- package/lib/clinical/services/shadowService.js +3 -12
- package/lib/clinical/stores/CtcaeCatalog.js +0 -20
- package/lib/config/airtableConfig.js +0 -3
- package/lib/core/BatchingManager.js +3 -10
- package/lib/core/NexusMessaging.js +13 -31
- package/lib/eval/EvalProvider.js +2 -3
- package/lib/fhir/config/fhirConfig.js +3 -34
- package/lib/fhir/constants/projectionSlugs.js +1 -41
- package/lib/fhir/helpers/elementHelper.js +0 -21
- package/lib/fhir/helpers/fhirHelper.js +2 -5
- package/lib/fhir/index.js +0 -24
- package/lib/fhir/models/envelopeModel.js +0 -5
- package/lib/fhir/projections/registerProjectors.js +0 -16
- package/lib/fhir/resources/Condition.js +2 -107
- package/lib/fhir/resources/Observation.js +1 -73
- package/lib/fhir/resources/Procedure.js +2 -34
- package/lib/fhir/resources/Provenance.js +2 -93
- package/lib/fhir/services/fhirService.js +2 -2
- package/lib/helpers/twilioHelper.js +1 -9
- package/lib/index.d.ts +0 -16
- package/lib/index.js +0 -9
- package/lib/models/messageModel.js +0 -6
- package/lib/routes/index.js +1 -21
- package/lib/services/airtableService.js +1 -2
- package/lib/shared/dtos/ContingencySafetyNet.js +3 -10
- package/lib/shared/dtos/DispatchedEscalation.js +8 -3
- package/lib/shared/dtos/Reminder.js +2 -2
- package/lib/storage/MongoStorage.js +0 -2
- package/package.json +1 -1
- package/lib/clinical/config/cohortConfig.js +0 -68
- package/lib/clinical/config/composerConfig.js +0 -42
- package/lib/clinical/config/subAgentsConfig.js +0 -44
- package/lib/clinical/flags/bridgeConfig.js +0 -14
- package/lib/clinical/flags/composerFlags.js +0 -14
- package/lib/clinical/helpers/clinicalFactsHelper.js +0 -108
- package/lib/clinical/helpers/clinicalMentionHelper.js +0 -68
- package/lib/clinical/helpers/cohortHelper.js +0 -22
- package/lib/clinical/helpers/gradeEstimateHelper.js +0 -29
- package/lib/clinical/helpers/resultsHelper.js +0 -34
- package/lib/clinical/helpers/safetyFlagsHelper.js +0 -10
- package/lib/clinical/services/bridgeService.js +0 -55
- package/lib/clinical/services/clinicalAirtableService.js +0 -162
- package/lib/clinical/services/clinicalExtractionService.js +0 -112
- package/lib/clinical/services/composerService.js +0 -59
- package/lib/clinical/services/dispatcherService.js +0 -137
- package/lib/clinical/services/reminderDispatchService.js +0 -36
- package/lib/clinical/services/skillService.js +0 -44
- package/lib/clinical/tools/analyzeSymptomPatternsTool.js +0 -279
- package/lib/clinical/tools/deliverPatientMessageTool.js +0 -202
- package/lib/clinical/tools/extractClinicalInfoTool.js +0 -167
- package/lib/clinical/tools/getActiveSymptomLandscapeTool.js +0 -141
- package/lib/clinical/tools/getPatientHistoryTool.js +0 -235
- package/lib/clinical/tools/getPatientRiskProfileTool.js +0 -71
- package/lib/clinical/tools/getRouterContextBundleTool.js +0 -93
- package/lib/clinical/tools/lookupCtcaeEntryTool.js +0 -35
- package/lib/clinical/tools/openClusterHypothesisTool.js +0 -160
- package/lib/clinical/tools/openConditionTool.js +0 -136
- package/lib/clinical/tools/recordClinicalImpressionTool.js +0 -251
- package/lib/clinical/tools/recordInterventionTool.js +0 -140
- package/lib/clinical/tools/registerClinicalTools.js +0 -39
- package/lib/clinical/tools/reportCrisisTool.js +0 -211
- package/lib/clinical/tools/reportMedicalEscalationTool.js +0 -187
- package/lib/clinical/tools/reportResultsReceivedTool.js +0 -153
- package/lib/clinical/tools/reportUnresolvedRequestv2Tool.js +0 -177
- package/lib/clinical/tools/schedulePatientReminderTool.js +0 -149
- package/lib/clinical/tools/setContingencyPlanTool.js +0 -176
- package/lib/clinical/tools/skillTool.js +0 -67
- package/lib/clinical/tools/submitRoutingDecisionTool.js +0 -291
- package/lib/clinical/tools/updateClusterHypothesisTool.js +0 -159
- package/lib/controllers/cohortController.js +0 -16
- package/lib/controllers/traceController.js +0 -26
- package/lib/core/workflowRunner.js +0 -35
- package/lib/fhir/config/fhirTablesConfig.js +0 -9
- package/lib/fhir/constants/severityPriority.js +0 -8
- package/lib/fhir/helpers/compositeHelper.js +0 -88
- package/lib/fhir/helpers/fhirReadHelper.js +0 -42
- package/lib/fhir/helpers/terminologyHelper.js +0 -260
- package/lib/fhir/projections/clusterProjection.js +0 -24
- package/lib/fhir/projections/contingencyProjection.js +0 -36
- package/lib/fhir/projections/dispatchedEscalationProjection.js +0 -25
- package/lib/fhir/projections/interventionProjection.js +0 -42
- package/lib/fhir/projections/proctcaeProjection.js +0 -179
- package/lib/fhir/projections/reminderProjection.js +0 -20
- package/lib/fhir/projections/riskProjection.js +0 -16
- package/lib/fhir/projections/routingProjection.js +0 -48
- package/lib/fhir/projections/symptomCaseProjection.js +0 -65
- package/lib/fhir/resources/CarePlan.js +0 -93
- package/lib/fhir/resources/ClinicalImpression.js +0 -118
- package/lib/fhir/resources/Communication.js +0 -65
- package/lib/fhir/resources/CommunicationRequest.js +0 -182
- package/lib/fhir/resources/Device.js +0 -24
- package/lib/fhir/resources/Patient.js +0 -18
- package/lib/fhir/resources/Questionnaire.js +0 -23
- package/lib/fhir/resources/QuestionnaireResponse.js +0 -29
- package/lib/fhir/resources/RequestOrchestration.js +0 -46
- package/lib/fhir/resources/RiskAssessment.js +0 -99
- package/lib/fhir/resources/Task.js +0 -106
- package/lib/fhir/resources/interventionExtensions.js +0 -28
- package/lib/fhir/services/clusterService.js +0 -77
- package/lib/fhir/services/contingencyService.js +0 -117
- package/lib/fhir/services/interventionService.js +0 -149
- package/lib/fhir/services/resultObservationService.js +0 -52
- package/lib/fhir/services/riskService.js +0 -101
- package/lib/fhir/services/routingService.js +0 -147
- package/lib/fhir/services/snapshotService.js +0 -28
- package/lib/fhir/services/symptomCaseService.js +0 -139
- package/lib/fhir/services/terminologyService.js +0 -58
- package/lib/fhir/services/triageService.js +0 -57
- package/lib/fhir/utils/normalizeUtils.js +0 -33
- package/lib/shared/dtos/GradedSymptom.js +0 -23
- package/lib/shared/dtos/TriageResponse.js +0 -35
|
@@ -15,28 +15,22 @@ async function runShadowTurn({
|
|
|
15
15
|
resolveAssistant,
|
|
16
16
|
executeLLM,
|
|
17
17
|
}) {
|
|
18
|
-
if (!shouldContinue())
|
|
19
|
-
logger.debug('[shadow] superseded pre-run', { code: thread?.code, runId });
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
18
|
+
if (!shouldContinue()) return null;
|
|
22
19
|
if (!runId) throw new Error('runShadowTurn requires a runId');
|
|
23
20
|
|
|
24
21
|
const code = thread.code;
|
|
25
|
-
logger.debug('[shadow] start', { code, runId, sessionId });
|
|
26
22
|
const shadowThread = cloneShadowThread(thread);
|
|
27
23
|
const shadowAssistant = resolveAssistant(shadowThread);
|
|
28
24
|
|
|
29
|
-
const context = new ToolRuntimeContext({ patientCode: code, turnId: runId, sessionId });
|
|
25
|
+
const context = new ToolRuntimeContext({ patientCode: code, turnId: runId, sessionId, thread: shadowThread });
|
|
30
26
|
|
|
31
27
|
const runOptions = {
|
|
32
28
|
toolRuntimeContext: context, shadow: true, shouldContinue, additionalInstructions,
|
|
33
29
|
};
|
|
34
30
|
|
|
35
|
-
const startedAt = Date.now();
|
|
36
31
|
const result = await runInShadowScope(() =>
|
|
37
32
|
executeLLM(shadowThread, shadowAssistant, runOptions, message),
|
|
38
33
|
);
|
|
39
|
-
logger.debug('[shadow] done', { code, runId, completed: !!result?.completed, steps: context.trace?.steps?.length || 0, durationMs: Date.now() - startedAt });
|
|
40
34
|
|
|
41
35
|
return { turnId: runId, context, result };
|
|
42
36
|
}
|
|
@@ -46,10 +40,7 @@ function forkShadowTurn({
|
|
|
46
40
|
}) {
|
|
47
41
|
Promise.resolve()
|
|
48
42
|
.then(() => {
|
|
49
|
-
if (!isShadowEnabled())
|
|
50
|
-
logger.debug('[shadow] disabled', { code: thread?.code });
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
43
|
+
if (!isShadowEnabled()) return null;
|
|
53
44
|
return runShadowTurn({
|
|
54
45
|
thread,
|
|
55
46
|
message,
|
|
@@ -32,26 +32,6 @@ class CtcaeCatalog {
|
|
|
32
32
|
return grades && Object.keys(grades).length ? grades : null;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
lookupEntry(ctcaeTerm) {
|
|
36
|
-
this._assertLoaded();
|
|
37
|
-
const key = (ctcaeTerm || '').trim().toLowerCase();
|
|
38
|
-
const symptom = key ? this.symptomsByTerm.get(key) : null;
|
|
39
|
-
if (symptom) {
|
|
40
|
-
return {
|
|
41
|
-
entry: {
|
|
42
|
-
ctcae_term: symptom.ctcaeTerm,
|
|
43
|
-
spanish_label: symptom.spanishLabel,
|
|
44
|
-
patient_phrases_validated: symptom.patientPhrasesValidated,
|
|
45
|
-
grades: symptom.grades,
|
|
46
|
-
},
|
|
47
|
-
found: true,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
const nearestTerms = [...this.symptomsByTerm.keys()]
|
|
51
|
-
.filter((k) => key && (key.includes(k) || k.includes(key) || k.split('_')[0] === key.split('_')[0]));
|
|
52
|
-
return { entry: {}, found: false, nearest_terms: nearestTerms };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
35
|
_assertLoaded() {
|
|
56
36
|
if (this.symptomsByTerm === null) throw new Error('ctcae catalog not loaded: call initialize() at startup');
|
|
57
37
|
}
|
|
@@ -13,7 +13,6 @@ const Logging_ID = runtimeConfig.get('AIRTABLE_LOGGING_ID') || 'appQ7YhzfebRDbSP
|
|
|
13
13
|
const Monitoreo_ID = runtimeConfig.get('AIRTABLE_MONITOREO_ID') || 'appdvraKSdp0XVn5n';
|
|
14
14
|
const Programa_Juntas_ID = runtimeConfig.get('AIRTABLE_PROGRAMA_JUNTAS_ID') || 'appKFWzkcDEWlrXBE';
|
|
15
15
|
const Symptoms_ID = runtimeConfig.get('AIRTABLE_SYMPTOMS_ID') || 'appQRhZlQ9tMfYZWJ';
|
|
16
|
-
const Ctcae_ID = runtimeConfig.get('AIRTABLE_CTCAE_ID') || 'appEr4ATOs0Xc8ymP';
|
|
17
16
|
const Follow_Up_ID = runtimeConfig.get('AIRTABLE_FOLLOW_UP_ID') || 'appBjKw1Ub0KkbZf0';
|
|
18
17
|
const Webinars_Leads_ID = runtimeConfig.get('AIRTABLE_WEBINARS_LEADS_ID') || 'appzjpVXTI0TgqGPq';
|
|
19
18
|
const Product_ID = runtimeConfig.get('AIRTABLE_PRODUCT_ID') || 'appu2YDW2pKDYLL5H';
|
|
@@ -33,7 +32,6 @@ const BASE_MAP = {
|
|
|
33
32
|
monitoreo: Monitoreo_ID,
|
|
34
33
|
programa: Programa_Juntas_ID,
|
|
35
34
|
symptoms: Symptoms_ID,
|
|
36
|
-
ctcae: Ctcae_ID,
|
|
37
35
|
followup: Follow_Up_ID,
|
|
38
36
|
webinars: Webinars_Leads_ID,
|
|
39
37
|
product: Product_ID,
|
|
@@ -51,7 +49,6 @@ module.exports = {
|
|
|
51
49
|
Monitoreo_ID,
|
|
52
50
|
Programa_Juntas_ID,
|
|
53
51
|
Symptoms_ID,
|
|
54
|
-
Ctcae_ID,
|
|
55
52
|
Follow_Up_ID,
|
|
56
53
|
Webinars_Leads_ID,
|
|
57
54
|
Product_ID,
|
|
@@ -53,10 +53,10 @@ class BatchingManager {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
async enqueueProcessing(chatId, processingFn, sendResponseFn) {
|
|
56
|
-
await this._runWithLock(chatId, processingFn, sendResponseFn
|
|
56
|
+
await this._runWithLock(chatId, processingFn, sendResponseFn);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
async _runWithLock(chatId, processingFn, sendResponseFn
|
|
59
|
+
async _runWithLock(chatId, processingFn, sendResponseFn) {
|
|
60
60
|
const prev = this.chatQueues.get(chatId) || Promise.resolve();
|
|
61
61
|
let resolveGate;
|
|
62
62
|
const gate = new Promise(r => { resolveGate = r; });
|
|
@@ -74,16 +74,9 @@ class BatchingManager {
|
|
|
74
74
|
const result = await processingFn(runId);
|
|
75
75
|
if (this._checkAbandoned(runId)) return;
|
|
76
76
|
|
|
77
|
-
if (sendResponseFn && result)
|
|
78
|
-
try {
|
|
79
|
-
await sendResponseFn(result);
|
|
80
|
-
} catch (error) {
|
|
81
|
-
logger.error('[BatchingManager] Error sending response', { chatId, error: error.message });
|
|
82
|
-
}
|
|
83
|
-
}
|
|
77
|
+
if (sendResponseFn && result) await sendResponseFn(result);
|
|
84
78
|
} catch (error) {
|
|
85
79
|
logger.error('[BatchingManager] Error processing messages', { chatId, error: error.message });
|
|
86
|
-
if (rethrow) throw error;
|
|
87
80
|
} finally {
|
|
88
81
|
if (this.activeRequests.get(chatId) === runId) {
|
|
89
82
|
this._clearProcessingState(chatId);
|
|
@@ -525,7 +525,7 @@ class NexusMessaging {
|
|
|
525
525
|
body: instruction,
|
|
526
526
|
messageId: `instruction_${Date.now()}_${Math.random().toString(36).substring(7)}`,
|
|
527
527
|
fromMe: true,
|
|
528
|
-
processed:
|
|
528
|
+
processed: true,
|
|
529
529
|
origin: 'instruction',
|
|
530
530
|
raw: { role },
|
|
531
531
|
triggeredBy: triggeredBy || null,
|
|
@@ -535,27 +535,17 @@ class NexusMessaging {
|
|
|
535
535
|
const result = await this.phiProcessor.inData(
|
|
536
536
|
messageData,
|
|
537
537
|
this._saveMessageFn,
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
runId,
|
|
547
|
-
}
|
|
548
|
-
}),
|
|
549
|
-
);
|
|
550
|
-
if (_id) {
|
|
551
|
-
try {
|
|
552
|
-
await updateMessage({ _id }, { $set: { processed: true } });
|
|
553
|
-
} catch (err) {
|
|
554
|
-
logger.error('[processInstruction] Could not mark instruction processed', { error: err.message, code });
|
|
538
|
+
({ body }) => this._executeWithPipeline(code, 'instruction', 'queue',
|
|
539
|
+
async (preProcessResult, _shouldContinue, runId) => this.assistantProcessor.process({
|
|
540
|
+
code,
|
|
541
|
+
runOptions: {
|
|
542
|
+
prePromptResult: preProcessResult,
|
|
543
|
+
additionalInstructions: body,
|
|
544
|
+
toolChoice: 'none',
|
|
545
|
+
runId,
|
|
555
546
|
}
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
},
|
|
547
|
+
}),
|
|
548
|
+
),
|
|
559
549
|
);
|
|
560
550
|
|
|
561
551
|
return result?.output || null;
|
|
@@ -565,15 +555,7 @@ class NexusMessaging {
|
|
|
565
555
|
const thread = await Thread.findOne({ code }).lean();
|
|
566
556
|
if (!thread) return null;
|
|
567
557
|
|
|
568
|
-
|
|
569
|
-
const requestedMessages = Array.isArray(messages) ? messages : [messages];
|
|
570
|
-
const normalizedMessages = requestedMessages.filter(body => typeof body === 'string' && body.trim());
|
|
571
|
-
if (normalizedMessages.length !== requestedMessages.length) {
|
|
572
|
-
logger.warn('[processSystemMessage] Ignoring blank message entries', {
|
|
573
|
-
code,
|
|
574
|
-
ignored: requestedMessages.length - normalizedMessages.length,
|
|
575
|
-
});
|
|
576
|
-
}
|
|
558
|
+
const normalizedMessages = Array.isArray(messages) ? messages : [messages];
|
|
577
559
|
|
|
578
560
|
for (let i = 0; i < normalizedMessages.length; i++) {
|
|
579
561
|
try {
|
|
@@ -587,7 +569,7 @@ class NexusMessaging {
|
|
|
587
569
|
origin: 'system',
|
|
588
570
|
raw: { role },
|
|
589
571
|
triggeredBy: triggeredBy || null,
|
|
590
|
-
|
|
572
|
+
silent: true,
|
|
591
573
|
};
|
|
592
574
|
await this.phiProcessor.inData(messageData, this._saveMessageFn, () => {});
|
|
593
575
|
} catch (err) {
|
package/lib/eval/EvalProvider.js
CHANGED
|
@@ -163,9 +163,8 @@ class EvalProvider {
|
|
|
163
163
|
devContent += `\n\nYou only have access to these tools: ${activeToolSchemas.map(s => s.function?.name).join(', ')}. Do not call or reference any tools not listed here.`;
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
|
-
} catch
|
|
167
|
-
|
|
168
|
-
logger.error('[NexusEvalProvider] Failed to resolve assistant — running with no tools', { assistantId, error: error.message });
|
|
166
|
+
} catch {
|
|
167
|
+
logger.warn('[NexusEvalProvider] Failed to resolve assistant', { assistantId });
|
|
169
168
|
}
|
|
170
169
|
}
|
|
171
170
|
|
|
@@ -2,32 +2,12 @@ const { Config_ID } = require('../../config/airtableConfig');
|
|
|
2
2
|
|
|
3
3
|
const { getRecordByFilter } = require('../../services/airtableService');
|
|
4
4
|
|
|
5
|
-
const { safeParse
|
|
5
|
+
const { safeParse } = require('../../utils/jsonUtils');
|
|
6
6
|
|
|
7
7
|
const FHIR_CONFIG_TABLE = 'fhir';
|
|
8
|
-
const REQUIRED_KEYS = [
|
|
9
|
-
'IDENT_BASE',
|
|
10
|
-
'EXT_BASE',
|
|
11
|
-
'CODESYSTEM_BASE',
|
|
12
|
-
'SYSTEMS',
|
|
13
|
-
'SUSPECTED_GRADE_DISPLAYS',
|
|
14
|
-
'QUESTIONNAIRE_CANONICAL_URL',
|
|
15
|
-
'PROCTCAE_VERSION',
|
|
16
|
-
'DEVICE_ID',
|
|
17
|
-
'ORGANIZATION_ID',
|
|
18
|
-
];
|
|
8
|
+
const REQUIRED_KEYS = ['IDENT_BASE', 'EXT_BASE', 'SYSTEMS', 'DEVICE_ID', 'ORGANIZATION_ID'];
|
|
19
9
|
|
|
20
|
-
const state = {
|
|
21
|
-
identBase: null,
|
|
22
|
-
extBase: null,
|
|
23
|
-
codesystemBase: null,
|
|
24
|
-
systems: null,
|
|
25
|
-
suspectedGradeDisplays: null,
|
|
26
|
-
questionnaireCanonicalUrl: null,
|
|
27
|
-
proctcaeVersion: null,
|
|
28
|
-
deviceId: null,
|
|
29
|
-
organizationId: null,
|
|
30
|
-
};
|
|
10
|
+
const state = { identBase: null, extBase: null, systems: null, deviceId: null, organizationId: null };
|
|
31
11
|
|
|
32
12
|
async function initialize() {
|
|
33
13
|
const records = await getRecordByFilter(Config_ID, FHIR_CONFIG_TABLE, 'TRUE()');
|
|
@@ -40,16 +20,9 @@ async function initialize() {
|
|
|
40
20
|
const systems = safeParse(byKey.SYSTEMS, null);
|
|
41
21
|
if (!systems) throw new Error('fhir config SYSTEMS is not valid JSON');
|
|
42
22
|
|
|
43
|
-
const suspectedGradeDisplays = safeParseArray(byKey.SUSPECTED_GRADE_DISPLAYS, null);
|
|
44
|
-
if (!suspectedGradeDisplays) throw new Error('fhir config SUSPECTED_GRADE_DISPLAYS is not a JSON array');
|
|
45
|
-
|
|
46
23
|
state.identBase = byKey.IDENT_BASE;
|
|
47
24
|
state.extBase = byKey.EXT_BASE;
|
|
48
|
-
state.codesystemBase = byKey.CODESYSTEM_BASE;
|
|
49
25
|
state.systems = systems;
|
|
50
|
-
state.suspectedGradeDisplays = suspectedGradeDisplays;
|
|
51
|
-
state.questionnaireCanonicalUrl = byKey.QUESTIONNAIRE_CANONICAL_URL;
|
|
52
|
-
state.proctcaeVersion = byKey.PROCTCAE_VERSION;
|
|
53
26
|
state.deviceId = byKey.DEVICE_ID;
|
|
54
27
|
state.organizationId = byKey.ORGANIZATION_ID;
|
|
55
28
|
}
|
|
@@ -62,11 +35,7 @@ module.exports = {
|
|
|
62
35
|
initialize,
|
|
63
36
|
getIdentBase: () => { assertLoaded(); return state.identBase; },
|
|
64
37
|
getExtBase: () => { assertLoaded(); return state.extBase; },
|
|
65
|
-
getCodesystemBase: () => { assertLoaded(); return state.codesystemBase; },
|
|
66
38
|
getSystems: () => { assertLoaded(); return state.systems; },
|
|
67
|
-
getSuspectedGradeDisplays: () => { assertLoaded(); return state.suspectedGradeDisplays; },
|
|
68
|
-
getQuestionnaireCanonicalUrl: () => { assertLoaded(); return state.questionnaireCanonicalUrl; },
|
|
69
|
-
getProctcaeVersion: () => { assertLoaded(); return state.proctcaeVersion; },
|
|
70
39
|
getDeviceId: () => { assertLoaded(); return state.deviceId; },
|
|
71
40
|
getOrganizationId: () => { assertLoaded(); return state.organizationId; },
|
|
72
41
|
};
|
|
@@ -1,43 +1,3 @@
|
|
|
1
1
|
const MENTION_SLUG = 'clinical-mention';
|
|
2
|
-
const RESULT_SLUG = 'patient-reported-result';
|
|
3
|
-
const ESCALATION_SLUG = 'dispatched-escalation';
|
|
4
|
-
const ESCALATION_KIND_SLUG = 'escalation-kind';
|
|
5
|
-
const REMINDER_SLUG = 'reminder';
|
|
6
|
-
const REMINDER_KIND_SLUG = 'reminder-kind';
|
|
7
|
-
const COMMUNICATION_CHANNEL_SLUG = 'communication-channel';
|
|
8
|
-
const SYMPTOM_CASE_SLUG = 'symptom-condition';
|
|
9
|
-
const SYMPTOM_ASSESSMENT_SLUG = 'symptom-assessment';
|
|
10
|
-
const SYMPTOM_GRADE_SLUG = 'symptom-grade';
|
|
11
|
-
const PATIENT_RISK_SLUG = 'patient-risk-assessment';
|
|
12
|
-
const CONTINGENCY_PLAN_SLUG = 'contingency-plan';
|
|
13
|
-
const CONTINGENCY_STEP_SLUG = 'contingency-step';
|
|
14
|
-
const INTERVENTION_SLUG = 'intervention';
|
|
15
|
-
const INTERVENTION_OUTCOME_SLUG = 'intervention-outcome';
|
|
16
|
-
const INTERVENTION_KIND_SLUG = 'intervention-kind';
|
|
17
|
-
const CLUSTER_IMPRESSION_SLUG = 'cluster-impression';
|
|
18
|
-
const CLUSTER_LABEL_SLUG = 'cluster-label';
|
|
19
|
-
const ROUTING_ASSESSMENT_SLUG = 'routing-assessment';
|
|
20
|
-
const ROUTING_DISPOSITION_SLUG = 'routing-disposition';
|
|
21
2
|
|
|
22
|
-
module.exports = {
|
|
23
|
-
MENTION_SLUG,
|
|
24
|
-
RESULT_SLUG,
|
|
25
|
-
ESCALATION_SLUG,
|
|
26
|
-
ESCALATION_KIND_SLUG,
|
|
27
|
-
REMINDER_SLUG,
|
|
28
|
-
REMINDER_KIND_SLUG,
|
|
29
|
-
COMMUNICATION_CHANNEL_SLUG,
|
|
30
|
-
SYMPTOM_CASE_SLUG,
|
|
31
|
-
SYMPTOM_ASSESSMENT_SLUG,
|
|
32
|
-
SYMPTOM_GRADE_SLUG,
|
|
33
|
-
PATIENT_RISK_SLUG,
|
|
34
|
-
CONTINGENCY_PLAN_SLUG,
|
|
35
|
-
CONTINGENCY_STEP_SLUG,
|
|
36
|
-
INTERVENTION_SLUG,
|
|
37
|
-
INTERVENTION_OUTCOME_SLUG,
|
|
38
|
-
INTERVENTION_KIND_SLUG,
|
|
39
|
-
CLUSTER_IMPRESSION_SLUG,
|
|
40
|
-
CLUSTER_LABEL_SLUG,
|
|
41
|
-
ROUTING_ASSESSMENT_SLUG,
|
|
42
|
-
ROUTING_DISPOSITION_SLUG,
|
|
43
|
-
};
|
|
3
|
+
module.exports = { MENTION_SLUG };
|
|
@@ -12,11 +12,6 @@ function patientReference(patientId) {
|
|
|
12
12
|
return reference('Patient', patientId);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
function recipientReference({ patientId, recipientKind = 'patient', recipientRefId = null }) {
|
|
16
|
-
if (recipientKind === 'related_person' && recipientRefId) return reference('RelatedPerson', recipientRefId);
|
|
17
|
-
return reference('Patient', patientId);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
15
|
function codeableConcept(text, { code = null, system = null, display = null } = {}) {
|
|
21
16
|
const out = { text };
|
|
22
17
|
if (code !== null && system !== null) {
|
|
@@ -29,26 +24,10 @@ function extension(name, value) {
|
|
|
29
24
|
return { url: extensionUrl(name), ...value };
|
|
30
25
|
}
|
|
31
26
|
|
|
32
|
-
function notes(...texts) {
|
|
33
|
-
const present = texts.filter(Boolean).map((text) => ({ text }));
|
|
34
|
-
return present.length ? present : null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function toIso(value) {
|
|
38
|
-
if (value instanceof Date) return value.toISOString().replace(/\.000Z$/, 'Z');
|
|
39
|
-
const s = String(value);
|
|
40
|
-
if (/\+00:00$/.test(s)) return s.replace(/\+00:00$/, 'Z');
|
|
41
|
-
if (/(Z|[+-]\d{2}:\d{2})$/.test(s)) return s;
|
|
42
|
-
return `${s}Z`;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
27
|
module.exports = {
|
|
46
28
|
identifier,
|
|
47
29
|
reference,
|
|
48
30
|
patientReference,
|
|
49
|
-
recipientReference,
|
|
50
31
|
codeableConcept,
|
|
51
32
|
extension,
|
|
52
|
-
notes,
|
|
53
|
-
toIso,
|
|
54
33
|
};
|
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
const { getIdentBase, getExtBase
|
|
1
|
+
const { getIdentBase, getExtBase } = require('../config/fhirConfig');
|
|
2
2
|
|
|
3
3
|
function fhirId(businessId) {
|
|
4
|
-
|
|
5
|
-
return id || 'x';
|
|
4
|
+
return String(businessId).replace(/_/g, '-').replace(/[^A-Za-z0-9.-]/g, '').slice(0, 64);
|
|
6
5
|
}
|
|
7
6
|
|
|
8
7
|
const identifierSystem = (slug) => `${getIdentBase()}/${slug}`;
|
|
9
8
|
const extensionUrl = (name) => `${getExtBase()}/${name}`;
|
|
10
|
-
const codeSystemUrl = (name) => `${getCodesystemBase()}/${name}`;
|
|
11
9
|
|
|
12
10
|
module.exports = {
|
|
13
11
|
fhirId,
|
|
14
12
|
identifierSystem,
|
|
15
13
|
extensionUrl,
|
|
16
|
-
codeSystemUrl,
|
|
17
14
|
};
|
package/lib/fhir/index.js
CHANGED
|
@@ -1,14 +1,5 @@
|
|
|
1
1
|
const { registerProjector, project, storeResources } = require('./services/fhirService');
|
|
2
2
|
const { storeClinicalMentions } = require('./services/clinicalMentionService');
|
|
3
|
-
const { storeResultObservations } = require('./services/resultObservationService');
|
|
4
|
-
const { storeSymptomCase, readSymptomCases } = require('./services/symptomCaseService');
|
|
5
|
-
const { storePatientRisk, readPatientRisk } = require('./services/riskService');
|
|
6
|
-
const { storeContingency, readContingencies } = require('./services/contingencyService');
|
|
7
|
-
const { storeIntervention, readInterventions } = require('./services/interventionService');
|
|
8
|
-
const { storeCluster, readClusters } = require('./services/clusterService');
|
|
9
|
-
const { storeRoutingDecision, readRoutingByTurn } = require('./services/routingService');
|
|
10
|
-
const { storeTriage } = require('./services/triageService');
|
|
11
|
-
const { patientSnapshot } = require('./services/snapshotService');
|
|
12
3
|
const { registerAllProjectors } = require('./projections/registerProjectors');
|
|
13
4
|
const { getFhirStore, ensureFhirIndexes } = require('./stores/fhirStore');
|
|
14
5
|
const fhirConfig = require('./config/fhirConfig');
|
|
@@ -25,21 +16,6 @@ module.exports = {
|
|
|
25
16
|
project,
|
|
26
17
|
storeResources,
|
|
27
18
|
storeClinicalMentions,
|
|
28
|
-
storeResultObservations,
|
|
29
|
-
storeSymptomCase,
|
|
30
|
-
readSymptomCases,
|
|
31
|
-
storePatientRisk,
|
|
32
|
-
readPatientRisk,
|
|
33
|
-
storeContingency,
|
|
34
|
-
readContingencies,
|
|
35
|
-
storeIntervention,
|
|
36
|
-
readInterventions,
|
|
37
|
-
storeCluster,
|
|
38
|
-
readClusters,
|
|
39
|
-
storeRoutingDecision,
|
|
40
|
-
readRoutingByTurn,
|
|
41
|
-
storeTriage,
|
|
42
|
-
patientSnapshot,
|
|
43
19
|
getFhirStore,
|
|
44
20
|
ensureFhirIndexes,
|
|
45
21
|
};
|
|
@@ -48,14 +48,10 @@ envelopeSchema.statics.deriveIndex = function deriveIndex(resource) {
|
|
|
48
48
|
resource.date ||
|
|
49
49
|
resource.recorded ||
|
|
50
50
|
resource.recordedDate ||
|
|
51
|
-
resource.onsetDateTime ||
|
|
52
|
-
resource.occurrenceDateTime ||
|
|
53
|
-
resource.sent ||
|
|
54
51
|
resource.start ||
|
|
55
52
|
(resource.effectivePeriod || {}).start ||
|
|
56
53
|
(resource.period || {}).start ||
|
|
57
54
|
(resource.actualPeriod || {}).start ||
|
|
58
|
-
((resource.requestedPeriod || [])[0] || {}).start ||
|
|
59
55
|
null;
|
|
60
56
|
return {
|
|
61
57
|
resourceType: resource.resourceType || null,
|
|
@@ -123,7 +119,6 @@ function patientReferenceOf(resource) {
|
|
|
123
119
|
return (
|
|
124
120
|
patientRef(resource.subject) ||
|
|
125
121
|
patientRef(resource.patient) ||
|
|
126
|
-
patientRef(resource.for) ||
|
|
127
122
|
patientRef(resource.performer) ||
|
|
128
123
|
participantRef()
|
|
129
124
|
);
|
|
@@ -1,24 +1,8 @@
|
|
|
1
1
|
const { registerProjector } = require('../services/fhirService');
|
|
2
2
|
const { projectClinicalMention, PROJECTOR_NAME: MENTION_PROJECTOR_NAME } = require('./clinicalMentionProjection');
|
|
3
|
-
const { projectDispatchedEscalation, PROJECTOR_NAME: ESCALATION_PROJECTOR_NAME } = require('./dispatchedEscalationProjection');
|
|
4
|
-
const { projectReminder, PROJECTOR_NAME: REMINDER_PROJECTOR_NAME } = require('./reminderProjection');
|
|
5
|
-
const { projectSymptomCase, PROJECTOR_NAME: SYMPTOM_CASE_PROJECTOR_NAME } = require('./symptomCaseProjection');
|
|
6
|
-
const { projectPatientRisk, PROJECTOR_NAME: RISK_PROJECTOR_NAME } = require('./riskProjection');
|
|
7
|
-
const { projectContingency, PROJECTOR_NAME: CONTINGENCY_PROJECTOR_NAME } = require('./contingencyProjection');
|
|
8
|
-
const { projectIntervention, PROJECTOR_NAME: INTERVENTION_PROJECTOR_NAME } = require('./interventionProjection');
|
|
9
|
-
const { projectCluster, PROJECTOR_NAME: CLUSTER_PROJECTOR_NAME } = require('./clusterProjection');
|
|
10
|
-
const { projectRoutingDecision, PROJECTOR_NAME: ROUTING_PROJECTOR_NAME } = require('./routingProjection');
|
|
11
3
|
|
|
12
4
|
const PROJECTORS = [
|
|
13
5
|
{ name: MENTION_PROJECTOR_NAME, project: projectClinicalMention },
|
|
14
|
-
{ name: ESCALATION_PROJECTOR_NAME, project: projectDispatchedEscalation },
|
|
15
|
-
{ name: REMINDER_PROJECTOR_NAME, project: projectReminder },
|
|
16
|
-
{ name: SYMPTOM_CASE_PROJECTOR_NAME, project: projectSymptomCase },
|
|
17
|
-
{ name: RISK_PROJECTOR_NAME, project: projectPatientRisk },
|
|
18
|
-
{ name: CONTINGENCY_PROJECTOR_NAME, project: projectContingency },
|
|
19
|
-
{ name: INTERVENTION_PROJECTOR_NAME, project: projectIntervention },
|
|
20
|
-
{ name: CLUSTER_PROJECTOR_NAME, project: projectCluster },
|
|
21
|
-
{ name: ROUTING_PROJECTOR_NAME, project: projectRoutingDecision },
|
|
22
6
|
];
|
|
23
7
|
|
|
24
8
|
function registerAllProjectors() {
|
|
@@ -1,95 +1,12 @@
|
|
|
1
1
|
const { getSystems } = require('../config/fhirConfig');
|
|
2
|
-
const { fhirId
|
|
3
|
-
const { identifier,
|
|
4
|
-
const { SYMPTOM_CASE_SLUG } = require('../constants/projectionSlugs');
|
|
5
|
-
|
|
6
|
-
const CLINICAL_STATUS_TO_FHIR = {
|
|
7
|
-
resolved: 'resolved',
|
|
8
|
-
inactive: 'inactive',
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
const RESOLUTION_CLOSE_REASONS = new Set([
|
|
12
|
-
'resolved', 'team_resolved', 'resolved_spontaneously', 'resolved_by_intervention',
|
|
13
|
-
]);
|
|
14
|
-
|
|
15
|
-
const VALID_VERIFICATION = new Set(['confirmed', 'provisional', 'unconfirmed']);
|
|
16
|
-
|
|
17
|
-
const PROVENANCE_PARTICIPANT_TYPE_SYSTEM = 'http://terminology.hl7.org/CodeSystem/provenance-participant-type';
|
|
18
|
-
|
|
19
|
-
function fhirClinicalStatusFor(condition) {
|
|
20
|
-
const status = condition.clinicalStatus;
|
|
21
|
-
if (status === 'characterizing' || status === 'monitoring') {
|
|
22
|
-
return condition.recurrenceCount > 0 ? 'recurrence' : 'active';
|
|
23
|
-
}
|
|
24
|
-
if (status === 'closed') {
|
|
25
|
-
return RESOLUTION_CLOSE_REASONS.has((condition.closeReason || '').trim().toLowerCase()) ? 'resolved' : 'inactive';
|
|
26
|
-
}
|
|
27
|
-
return CLINICAL_STATUS_TO_FHIR[status];
|
|
28
|
-
}
|
|
2
|
+
const { fhirId } = require('../helpers/fhirHelper');
|
|
3
|
+
const { identifier, patientReference, codeableConcept } = require('../helpers/elementHelper');
|
|
29
4
|
|
|
30
5
|
class Condition {
|
|
31
6
|
constructor(payload) {
|
|
32
7
|
Object.assign(this, payload);
|
|
33
8
|
}
|
|
34
9
|
|
|
35
|
-
static fromSymptomCase({ condition, verification, headAssessmentId = null }) {
|
|
36
|
-
const systems = getSystems();
|
|
37
|
-
const clinicalStatusCode = fhirClinicalStatusFor(condition);
|
|
38
|
-
const verificationCode = VALID_VERIFICATION.has(verification) ? verification : 'provisional';
|
|
39
|
-
const code = condition.ctcaeTerm && condition.ctcaeTerm !== 'other'
|
|
40
|
-
? codeableConcept(condition.ctcaeTerm, { code: condition.ctcaeTerm, system: codeSystemUrl('pro-ctcae'), display: condition.ctcaeTerm })
|
|
41
|
-
: codeableConcept(condition.ctcaeTerm || 'other');
|
|
42
|
-
const extensions = [extension('symptom-case-status', { valueString: condition.clinicalStatus })];
|
|
43
|
-
const payload = {
|
|
44
|
-
resourceType: 'Condition',
|
|
45
|
-
id: fhirId(condition.conditionId),
|
|
46
|
-
identifier: [identifier(SYMPTOM_CASE_SLUG, condition.conditionId)],
|
|
47
|
-
clinicalStatus: codeableConcept(clinicalStatusCode, { code: clinicalStatusCode, system: systems.conditionClinical }),
|
|
48
|
-
verificationStatus: codeableConcept(verificationCode, { code: verificationCode, system: systems.conditionVerStatus }),
|
|
49
|
-
category: [
|
|
50
|
-
codeableConcept('problem-list-item', { code: 'problem-list-item', system: systems.conditionCategory }),
|
|
51
|
-
codeableConcept('PRO-CTCAE patient-reported symptom (agent-tracked case)', { code: 'proctcae-symptom', system: codeSystemUrl('condition-category') }),
|
|
52
|
-
],
|
|
53
|
-
code,
|
|
54
|
-
subject: patientReference(condition.patientId),
|
|
55
|
-
participant: [{
|
|
56
|
-
function: codeableConcept('Informant', { code: 'informant', system: PROVENANCE_PARTICIPANT_TYPE_SYSTEM }),
|
|
57
|
-
actor: patientReference(condition.patientId),
|
|
58
|
-
}],
|
|
59
|
-
onsetDateTime: toIso(condition.onsetAt),
|
|
60
|
-
extension: extensions,
|
|
61
|
-
};
|
|
62
|
-
if (condition.abatementAt) {
|
|
63
|
-
if (clinicalStatusCode === 'resolved') {
|
|
64
|
-
payload.abatementDateTime = toIso(condition.abatementAt);
|
|
65
|
-
} else {
|
|
66
|
-
extensions.push(extension('legacy-abatement', { valueDateTime: toIso(condition.abatementAt) }));
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
if (condition.closeReason) {
|
|
70
|
-
extensions.push(extension('symptom-close-reason', { valueString: condition.closeReason }));
|
|
71
|
-
if (clinicalStatusCode === 'inactive' && condition.closeReason === 'stale_no_activity') {
|
|
72
|
-
extensions.push(extension('presumed-inactive-by-gap', { valueBoolean: true }));
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
if (condition.temporality) extensions.push(extension('symptom-temporality', { valueString: condition.temporality }));
|
|
76
|
-
if (condition.currentEpisodeId) extensions.push(extension('symptom-current-episode-id', { valueString: condition.currentEpisodeId }));
|
|
77
|
-
for (const id of condition.evidenceEpisodeIds) extensions.push(extension('symptom-evidence-episode-ids', { valueString: id }));
|
|
78
|
-
for (const id of condition.evidenceMentionIds) extensions.push(extension('symptom-evidence-mention-ids', { valueString: id }));
|
|
79
|
-
extensions.push(extension('symptom-recurrence-count', { valueInteger: condition.recurrenceCount }));
|
|
80
|
-
if (condition.reactivatedAt) extensions.push(extension('symptom-reactivated-at', { valueDateTime: toIso(condition.reactivatedAt) }));
|
|
81
|
-
if (condition.lastTransitionReason) payload.note = [{ text: condition.lastTransitionReason }];
|
|
82
|
-
const evidence = [
|
|
83
|
-
...condition.evidenceEpisodeIds.map((id) => ({ reference: reference('Observation', id) })),
|
|
84
|
-
...condition.evidenceMentionIds.map((id) => ({ reference: reference('Observation', id) })),
|
|
85
|
-
];
|
|
86
|
-
if (evidence.length) payload.evidence = evidence;
|
|
87
|
-
if (headAssessmentId) {
|
|
88
|
-
payload.stage = [{ assessment: [reference('ClinicalImpression', headAssessmentId)] }];
|
|
89
|
-
}
|
|
90
|
-
return new Condition(payload);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
10
|
static fromMentionProjection({ fact, provenance, code, extensions, slug }) {
|
|
94
11
|
const assertion = fact.assertion;
|
|
95
12
|
const notes = provenance.verbatimQuotes && provenance.verbatimQuotes.length
|
|
@@ -112,28 +29,6 @@ class Condition {
|
|
|
112
29
|
if (notes) payload.note = notes;
|
|
113
30
|
return new Condition(payload);
|
|
114
31
|
}
|
|
115
|
-
|
|
116
|
-
static fromTriageProjection({ sym, code }) {
|
|
117
|
-
const caseId = `${sym.patientId}-${sym.term}`;
|
|
118
|
-
const systems = getSystems();
|
|
119
|
-
const payload = {
|
|
120
|
-
resourceType: 'Condition',
|
|
121
|
-
id: fhirId(`symptom-${caseId}`),
|
|
122
|
-
identifier: [identifier('symptom-condition', caseId)],
|
|
123
|
-
clinicalStatus: codeableConcept('active', { code: 'active', system: systems.conditionClinical }),
|
|
124
|
-
verificationStatus: codeableConcept('provisional', { code: 'provisional', system: systems.conditionVerStatus }),
|
|
125
|
-
category: [
|
|
126
|
-
codeableConcept('problem-list-item', { code: 'problem-list-item', system: systems.conditionCategory }),
|
|
127
|
-
codeableConcept('PRO-CTCAE patient-reported symptom (triage-tracked case)', { code: 'proctcae-symptom', system: codeSystemUrl('condition-category') }),
|
|
128
|
-
],
|
|
129
|
-
code,
|
|
130
|
-
subject: patientReference(sym.patientId),
|
|
131
|
-
evidence: [{ reference: reference('Observation', String(sym.symptomId)) }],
|
|
132
|
-
extension: [extension('symptom-source', { valueString: String(sym.source || 'triage') })],
|
|
133
|
-
};
|
|
134
|
-
if (sym.effective) payload.onsetDateTime = toIso(sym.effective);
|
|
135
|
-
return new Condition(payload);
|
|
136
|
-
}
|
|
137
32
|
}
|
|
138
33
|
|
|
139
34
|
module.exports = { Condition };
|