@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
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
const { getSystems } = require('../config/fhirConfig');
|
|
2
2
|
const { fhirId } = require('../helpers/fhirHelper');
|
|
3
|
-
const { identifier,
|
|
4
|
-
const { SYMPTOM_GRADE_SLUG } = require('../constants/projectionSlugs');
|
|
3
|
+
const { identifier, patientReference, codeableConcept } = require('../helpers/elementHelper');
|
|
5
4
|
|
|
6
5
|
const OBSERVED_FIELD_SYSTEM_KEY = 'observedField';
|
|
7
6
|
|
|
@@ -46,77 +45,6 @@ class Observation {
|
|
|
46
45
|
if (components.length) payload.component = components;
|
|
47
46
|
return new Observation(payload);
|
|
48
47
|
}
|
|
49
|
-
|
|
50
|
-
static fromTriageProjection({ sym, code, aeCategoryConcept, components, gradeConcept, method, deviceId }) {
|
|
51
|
-
const sid = String(sym.symptomId);
|
|
52
|
-
const payload = {
|
|
53
|
-
resourceType: 'Observation',
|
|
54
|
-
id: fhirId(sid),
|
|
55
|
-
identifier: [identifier('pro-ctcae-symptom', sid)],
|
|
56
|
-
status: 'preliminary',
|
|
57
|
-
category: [observationCategory('survey'), ...(aeCategoryConcept ? [aeCategoryConcept] : [])],
|
|
58
|
-
code,
|
|
59
|
-
subject: patientReference(sym.patientId),
|
|
60
|
-
performer: [patientReference(sym.patientId)],
|
|
61
|
-
valueCodeableConcept: gradeConcept,
|
|
62
|
-
method,
|
|
63
|
-
extension: [
|
|
64
|
-
extension('grade-verification', { valueCode: 'provisional' }),
|
|
65
|
-
extension('grade-derivation', { valueCode: 'pro-ctcae-composite' }),
|
|
66
|
-
extension('symptom-source', { valueString: String(sym.source || 'triage') }),
|
|
67
|
-
],
|
|
68
|
-
derivedFrom: [reference('QuestionnaireResponse', `triage-${sym.triageId}`)],
|
|
69
|
-
device: reference('Device', deviceId),
|
|
70
|
-
};
|
|
71
|
-
if (sym.effective) payload.effectiveDateTime = toIso(sym.effective);
|
|
72
|
-
if (components.length) payload.component = components;
|
|
73
|
-
return new Observation(payload);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
static fromSymptomGrade({ assessment }) {
|
|
77
|
-
const grade = assessment.grade;
|
|
78
|
-
const gradeId = `${assessment.assessmentId}-grade`;
|
|
79
|
-
const extensions = [];
|
|
80
|
-
if (grade.confidence !== null) extensions.push(extension('grade-confidence', { valueString: grade.confidence }));
|
|
81
|
-
if (grade.possibleRange !== null) extensions.push(extension('grade-range', { valueString: `${grade.possibleRange[0]}-${grade.possibleRange[1]}` }));
|
|
82
|
-
if (grade.reasoning) extensions.push(extension('grade-reasoning', { valueString: grade.reasoning }));
|
|
83
|
-
if (assessment.gradeVerification) extensions.push(extension('grade-verification', { valueString: assessment.gradeVerification }));
|
|
84
|
-
if (assessment.gradeThresholdClause) extensions.push(extension('grade-threshold-clause', { valueString: assessment.gradeThresholdClause }));
|
|
85
|
-
const payload = {
|
|
86
|
-
resourceType: 'Observation',
|
|
87
|
-
id: fhirId(gradeId),
|
|
88
|
-
identifier: [identifier(SYMPTOM_GRADE_SLUG, gradeId)],
|
|
89
|
-
status: 'final',
|
|
90
|
-
category: [observationCategory('survey')],
|
|
91
|
-
code: codeableConcept('CTCAE severity grade'),
|
|
92
|
-
subject: patientReference(assessment.patientId),
|
|
93
|
-
performer: [patientReference(assessment.patientId)],
|
|
94
|
-
effectiveDateTime: toIso(assessment.performedAt),
|
|
95
|
-
};
|
|
96
|
-
if (grade.bestEstimate !== null) payload.valueInteger = grade.bestEstimate;
|
|
97
|
-
if (extensions.length) payload.extension = extensions;
|
|
98
|
-
return new Observation(payload);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
static fromResultObservation({ patientId, obsId, code, value, unit, category, effective, now, slug }) {
|
|
102
|
-
const payload = {
|
|
103
|
-
resourceType: 'Observation',
|
|
104
|
-
id: fhirId(obsId),
|
|
105
|
-
identifier: [identifier(slug, obsId)],
|
|
106
|
-
status: 'final',
|
|
107
|
-
category: [observationCategory(category || 'laboratory')],
|
|
108
|
-
code: codeableConcept(code),
|
|
109
|
-
subject: patientReference(patientId),
|
|
110
|
-
effectiveDateTime: effective || now,
|
|
111
|
-
issued: now,
|
|
112
|
-
};
|
|
113
|
-
if (typeof value === 'number') {
|
|
114
|
-
if (Number.isFinite(value)) payload.valueQuantity = unit ? { value, unit } : { value };
|
|
115
|
-
} else if (value !== null && value !== undefined && value !== '') {
|
|
116
|
-
payload.valueString = String(value);
|
|
117
|
-
}
|
|
118
|
-
return new Observation(payload);
|
|
119
|
-
}
|
|
120
48
|
}
|
|
121
49
|
|
|
122
50
|
function observationCategory(code) {
|
|
@@ -1,43 +1,11 @@
|
|
|
1
|
-
const { fhirId
|
|
2
|
-
const {
|
|
3
|
-
identifier,
|
|
4
|
-
patientReference,
|
|
5
|
-
reference,
|
|
6
|
-
codeableConcept,
|
|
7
|
-
notes,
|
|
8
|
-
toIso,
|
|
9
|
-
} = require('../helpers/elementHelper');
|
|
10
|
-
const {
|
|
11
|
-
INTERVENTION_SLUG,
|
|
12
|
-
INTERVENTION_KIND_SLUG,
|
|
13
|
-
INTERVENTION_OUTCOME_SLUG,
|
|
14
|
-
} = require('../constants/projectionSlugs');
|
|
15
|
-
const { interventionExtensions } = require('./interventionExtensions');
|
|
1
|
+
const { fhirId } = require('../helpers/fhirHelper');
|
|
2
|
+
const { identifier, patientReference } = require('../helpers/elementHelper');
|
|
16
3
|
|
|
17
4
|
class Procedure {
|
|
18
5
|
constructor(payload) {
|
|
19
6
|
Object.assign(this, payload);
|
|
20
7
|
}
|
|
21
8
|
|
|
22
|
-
static fromIntervention({ intervention }) {
|
|
23
|
-
const payload = {
|
|
24
|
-
resourceType: 'Procedure',
|
|
25
|
-
id: fhirId(intervention.interventionId),
|
|
26
|
-
identifier: [identifier(INTERVENTION_SLUG, intervention.interventionId)],
|
|
27
|
-
status: intervention.outcome === 'pending' ? 'in-progress' : 'completed',
|
|
28
|
-
code: codeableConcept(intervention.interventionLabel),
|
|
29
|
-
subject: patientReference(intervention.patientId),
|
|
30
|
-
reason: [{ reference: reference('Condition', intervention.caseId) }],
|
|
31
|
-
occurrenceDateTime: toIso(intervention.deliveredAt),
|
|
32
|
-
outcome: codeableConcept(intervention.outcome, { code: intervention.outcome, system: codeSystemUrl(INTERVENTION_OUTCOME_SLUG) }),
|
|
33
|
-
category: [codeableConcept(intervention.kind, { code: intervention.kind, system: codeSystemUrl(INTERVENTION_KIND_SLUG) })],
|
|
34
|
-
extension: interventionExtensions(intervention, { withRecipient: true }),
|
|
35
|
-
};
|
|
36
|
-
const note = notes(intervention.rationale, intervention.outcomeNotes);
|
|
37
|
-
if (note) payload.note = note;
|
|
38
|
-
return new Procedure(payload);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
9
|
static fromMentionProjection({ fact, provenance, code, extensions, slug }) {
|
|
42
10
|
const assertion = fact.assertion;
|
|
43
11
|
const notes = provenance.verbatimQuotes && provenance.verbatimQuotes.length
|
|
@@ -1,33 +1,12 @@
|
|
|
1
1
|
const { getDeviceId, getOrganizationId } = require('../config/fhirConfig');
|
|
2
|
-
const { fhirId
|
|
3
|
-
const { extension, reference, codeableConcept
|
|
4
|
-
|
|
5
|
-
const CLUSTER_ACTIVITY_CODESYSTEM = 'clinical-activity';
|
|
6
|
-
const CLUSTER_ACTIVITY_CODES = {
|
|
7
|
-
opened: 'cluster-hypothesis-opened',
|
|
8
|
-
updated: 'cluster-hypothesis-updated',
|
|
9
|
-
dissolved: 'cluster-hypothesis-dissolved',
|
|
10
|
-
};
|
|
2
|
+
const { fhirId } = require('../helpers/fhirHelper');
|
|
3
|
+
const { extension, reference, codeableConcept } = require('../helpers/elementHelper');
|
|
11
4
|
|
|
12
5
|
class Provenance {
|
|
13
6
|
constructor(payload) {
|
|
14
7
|
Object.assign(this, payload);
|
|
15
8
|
}
|
|
16
9
|
|
|
17
|
-
static fromClusterImpression({ clusterId, turnId, action, recordedAt, transitionReason }) {
|
|
18
|
-
const activityCode = CLUSTER_ACTIVITY_CODES[action] || 'cluster-hypothesis';
|
|
19
|
-
const payload = {
|
|
20
|
-
resourceType: 'Provenance',
|
|
21
|
-
id: fhirId(`${clusterId}-prov-${turnId}-${action}`),
|
|
22
|
-
target: [{ reference: `ClinicalImpression/${fhirId(clusterId)}` }],
|
|
23
|
-
recorded: toIso(recordedAt),
|
|
24
|
-
agent: [{ who: reference('Device', getDeviceId()), onBehalfOf: reference('Organization', getOrganizationId()) }],
|
|
25
|
-
activity: codeableConcept(activityCode, { code: activityCode, system: codeSystemUrl(CLUSTER_ACTIVITY_CODESYSTEM) }),
|
|
26
|
-
};
|
|
27
|
-
if (transitionReason) payload.extension = [extension('transition-reason', { valueString: transitionReason })];
|
|
28
|
-
return new Provenance(payload);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
10
|
static fromMentionProjection({ target, provenance, activityText }) {
|
|
32
11
|
const payload = {
|
|
33
12
|
resourceType: 'Provenance',
|
|
@@ -44,76 +23,6 @@ class Provenance {
|
|
|
44
23
|
};
|
|
45
24
|
return new Provenance(payload);
|
|
46
25
|
}
|
|
47
|
-
|
|
48
|
-
static fromDispatchedEscalation({ target, recordedAt, activityText }) {
|
|
49
|
-
return new Provenance({
|
|
50
|
-
resourceType: 'Provenance',
|
|
51
|
-
id: fhirId(`${target.id}-prov`),
|
|
52
|
-
target: [{ reference: `${target.resourceType}/${target.id}` }],
|
|
53
|
-
recorded: toIso(recordedAt),
|
|
54
|
-
agent: [{ who: reference('Device', getDeviceId()), onBehalfOf: reference('Organization', getOrganizationId()) }],
|
|
55
|
-
activity: codeableConcept(activityText),
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
static fromRoutingDecision({ provenanceId, targetRefs, provenance, activityText }) {
|
|
60
|
-
return new Provenance({
|
|
61
|
-
resourceType: 'Provenance',
|
|
62
|
-
id: fhirId(provenanceId),
|
|
63
|
-
target: targetRefs,
|
|
64
|
-
recorded: toIso(provenance.recordedAt),
|
|
65
|
-
agent: [{ who: reference('Device', provenance.agentDevice), onBehalfOf: reference('Organization', provenance.onBehalfOf) }],
|
|
66
|
-
activity: codeableConcept(activityText),
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
static fromSymptomCase({ provenanceId, targetRefs, recordedAt, activityText }) {
|
|
71
|
-
return new Provenance({
|
|
72
|
-
resourceType: 'Provenance',
|
|
73
|
-
id: fhirId(provenanceId),
|
|
74
|
-
target: targetRefs,
|
|
75
|
-
recorded: toIso(recordedAt),
|
|
76
|
-
agent: [{ who: reference('Device', getDeviceId()), onBehalfOf: reference('Organization', getOrganizationId()) }],
|
|
77
|
-
activity: codeableConcept(activityText),
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
static fromPatientRisk({ assessment }) {
|
|
82
|
-
return new Provenance({
|
|
83
|
-
resourceType: 'Provenance',
|
|
84
|
-
id: fhirId(`${assessment.assessmentId}-prov`),
|
|
85
|
-
target: [{ reference: `RiskAssessment/${fhirId(assessment.assessmentId)}` }],
|
|
86
|
-
recorded: toIso(assessment.assessedAt),
|
|
87
|
-
agent: [{ who: reference('Device', getDeviceId()), onBehalfOf: reference('Organization', getOrganizationId()) }],
|
|
88
|
-
activity: codeableConcept('patient-risk-assessment'),
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
static fromContingency({ planId, recipients, recordedAt, activityText }) {
|
|
93
|
-
const targets = [{ reference: `CarePlan/${fhirId(planId)}` }];
|
|
94
|
-
for (const recipient of recipients) {
|
|
95
|
-
targets.push({ reference: `CommunicationRequest/${fhirId(`${planId}-${recipient}`)}` });
|
|
96
|
-
}
|
|
97
|
-
return new Provenance({
|
|
98
|
-
resourceType: 'Provenance',
|
|
99
|
-
id: fhirId(`${planId}-prov`),
|
|
100
|
-
target: targets,
|
|
101
|
-
recorded: toIso(recordedAt),
|
|
102
|
-
agent: [{ who: reference('Device', getDeviceId()), onBehalfOf: reference('Organization', getOrganizationId()) }],
|
|
103
|
-
activity: codeableConcept(activityText),
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
static fromResultObservations({ targetIds, provenanceId, activityText, now }) {
|
|
108
|
-
return new Provenance({
|
|
109
|
-
resourceType: 'Provenance',
|
|
110
|
-
id: fhirId(provenanceId),
|
|
111
|
-
target: targetIds.map((id) => ({ reference: `Observation/${fhirId(id)}` })),
|
|
112
|
-
recorded: now,
|
|
113
|
-
agent: [{ who: reference('Device', getDeviceId()), onBehalfOf: reference('Organization', getOrganizationId()) }],
|
|
114
|
-
activity: codeableConcept(activityText),
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
26
|
}
|
|
118
27
|
|
|
119
28
|
module.exports = { Provenance };
|
|
@@ -15,11 +15,11 @@ function project(items) {
|
|
|
15
15
|
return registry.project(items);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
async function storeResources(resources, { store = null
|
|
18
|
+
async function storeResources(resources, { store = null } = {}) {
|
|
19
19
|
const resourceStore = store || getFhirStore();
|
|
20
20
|
const stored = [];
|
|
21
21
|
for (const resource of [resources].flat(Infinity).filter((entry) => entry && entry.resourceType)) {
|
|
22
|
-
stored.push(await resourceStore.upsert(resource
|
|
22
|
+
stored.push(await resourceStore.upsert(resource));
|
|
23
23
|
}
|
|
24
24
|
return { stored };
|
|
25
25
|
}
|
|
@@ -121,18 +121,10 @@ 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
|
-
|
|
131
124
|
module.exports = {
|
|
132
125
|
convertTwilioToInternalFormat,
|
|
133
126
|
downloadMediaFromTwilio,
|
|
134
127
|
getMediaTypeFromContentType,
|
|
135
128
|
extractTitle,
|
|
136
|
-
ensureWhatsAppFormat
|
|
137
|
-
stripWhatsAppFormat
|
|
129
|
+
ensureWhatsAppFormat
|
|
138
130
|
};
|
package/lib/index.d.ts
CHANGED
|
@@ -416,20 +416,6 @@ declare module '@peopl-health/nexus' {
|
|
|
416
416
|
export function createQueueAdapter(type: 'local' | 'redis' | string, config?: any): QueueAdapter;
|
|
417
417
|
export function registerQueueAdapter(name: string, AdapterClass: typeof QueueAdapter): void;
|
|
418
418
|
|
|
419
|
-
// Workflows
|
|
420
|
-
export interface Workflow {
|
|
421
|
-
kind: string;
|
|
422
|
-
dedupeKey: (trigger: any) => string;
|
|
423
|
-
prepare: (trigger: any) => Promise<any>;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
export interface WorkflowRunner {
|
|
427
|
-
register(workflow: Workflow): Promise<Workflow>;
|
|
428
|
-
enqueue(kind: string, trigger: any, options?: QueueJobOptions): Promise<string>;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
export function createWorkflowRunner(options: { queueAdapter: QueueAdapter }): WorkflowRunner;
|
|
432
|
-
|
|
433
419
|
// Memory System
|
|
434
420
|
export interface PatientMemoryDocument {
|
|
435
421
|
_id: any;
|
|
@@ -631,8 +617,6 @@ declare module '@peopl-health/nexus' {
|
|
|
631
617
|
prompt?: string | null;
|
|
632
618
|
preset?: string | null;
|
|
633
619
|
response_id?: string | null;
|
|
634
|
-
turnId?: string | null;
|
|
635
|
-
traceId?: string | null;
|
|
636
620
|
run?: any;
|
|
637
621
|
predictionTimeMs?: number;
|
|
638
622
|
retries?: number;
|
package/lib/index.js
CHANGED
|
@@ -16,7 +16,6 @@ const {
|
|
|
16
16
|
DefaultMemoryManager,
|
|
17
17
|
EnhancedMemoryManager,
|
|
18
18
|
initialize: initializeClinical,
|
|
19
|
-
registerClinicalTools,
|
|
20
19
|
} = require('./clinical');
|
|
21
20
|
const { initialize: initializeFhir } = require('./fhir');
|
|
22
21
|
const { logger } = require('./utils/logger');
|
|
@@ -27,7 +26,6 @@ const { BaileysProvider } = require('./adapters/BaileysProvider');
|
|
|
27
26
|
const { setPreprocessingHandler, hasPreprocessingHandler, invokePreprocessingHandler } = require('./services/preprocessingService');
|
|
28
27
|
const { requestIdMiddleware, getRequestId } = require('./middleware/requestId');
|
|
29
28
|
const { QueueAdapter, LocalQueueAdapter, RedisQueueAdapter, createQueueAdapter, registerQueueAdapter } = require('./queue');
|
|
30
|
-
const { createWorkflowRunner } = require('./core/workflowRunner');
|
|
31
29
|
const routes = require('./routes');
|
|
32
30
|
const { resetAll } = require('./config/lifecycle');
|
|
33
31
|
const { EvalProvider } = require('./eval/EvalProvider');
|
|
@@ -159,12 +157,6 @@ class Nexus {
|
|
|
159
157
|
logger.warn('[Nexus] clinical init skipped', { error: e?.message || e });
|
|
160
158
|
}
|
|
161
159
|
|
|
162
|
-
try {
|
|
163
|
-
registerClinicalTools();
|
|
164
|
-
} catch (e) {
|
|
165
|
-
logger.warn('Warning: failed to register clinical tools:', e?.message || e);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
160
|
this.isInitialized = true;
|
|
169
161
|
}
|
|
170
162
|
|
|
@@ -215,7 +207,6 @@ class Nexus {
|
|
|
215
207
|
}
|
|
216
208
|
|
|
217
209
|
module.exports = {
|
|
218
|
-
createWorkflowRunner,
|
|
219
210
|
Nexus,
|
|
220
211
|
TwilioProvider,
|
|
221
212
|
BaileysProvider,
|
|
@@ -78,8 +78,6 @@ const messageSchema = new mongoose.Schema({
|
|
|
78
78
|
prompt: { type: Object, default: null },
|
|
79
79
|
preset: { type: Object, default: null },
|
|
80
80
|
response_id: { type: String, default: null },
|
|
81
|
-
turnId: { type: String },
|
|
82
|
-
traceId: { type: String },
|
|
83
81
|
triggeredBy: { type: String, default: null }
|
|
84
82
|
}, { timestamps: true });
|
|
85
83
|
|
|
@@ -97,10 +95,6 @@ messageSchema.index({ createdAt: -1 }, { name: 'global_sort_idx' });
|
|
|
97
95
|
|
|
98
96
|
messageSchema.index({ message_id: 1 }, { name: 'message_id_idx', sparse: true });
|
|
99
97
|
|
|
100
|
-
messageSchema.index({ turnId: 1 }, { name: 'turn_id_idx', sparse: true });
|
|
101
|
-
|
|
102
|
-
messageSchema.index({ traceId: 1 }, { name: 'trace_id_idx', sparse: true });
|
|
103
|
-
|
|
104
98
|
messageSchema.index({ triggeredBy: 1, createdAt: -1 }, { name: 'triggered_by_idx', sparse: true });
|
|
105
99
|
|
|
106
100
|
const Message = mongoose.model('Message', messageSchema);
|
package/lib/routes/index.js
CHANGED
|
@@ -90,14 +90,6 @@ const presetRouteDefinitions = {
|
|
|
90
90
|
'POST /cache/clear': 'clearPresetCacheController'
|
|
91
91
|
};
|
|
92
92
|
|
|
93
|
-
const cohortRouteDefinitions = {
|
|
94
|
-
'POST /cache/clear': 'clearCohortCacheController'
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
const traceRouteDefinitions = {
|
|
98
|
-
'GET /:traceId': 'getTraceController'
|
|
99
|
-
};
|
|
100
|
-
|
|
101
93
|
const createRouter = (routeDefinitions, controllers) => {
|
|
102
94
|
const router = express.Router();
|
|
103
95
|
|
|
@@ -123,7 +115,6 @@ const messageController = require('../controllers/messageController');
|
|
|
123
115
|
const messageStatusController = require('../controllers/messageStatusController');
|
|
124
116
|
const patientController = require('../controllers/patientController');
|
|
125
117
|
const presetController = require('../controllers/presetController');
|
|
126
|
-
const cohortController = require('../controllers/cohortController');
|
|
127
118
|
const qualityMessageController = require('../controllers/qualityMessageController');
|
|
128
119
|
const templateController = require('../controllers/templateController');
|
|
129
120
|
const templateFlowController = require('../controllers/templateFlowController');
|
|
@@ -131,7 +122,6 @@ const flowDataExchangeController = require('../controllers/flowDataExchangeContr
|
|
|
131
122
|
const uploadController = require('../controllers/uploadController');
|
|
132
123
|
const dashboardController = require('../controllers/dashboardController');
|
|
133
124
|
const prescriptionController = require('../controllers/prescriptionController');
|
|
134
|
-
const traceController = require('../controllers/traceController');
|
|
135
125
|
|
|
136
126
|
const builtInControllers = {
|
|
137
127
|
// Assistant controllers
|
|
@@ -191,9 +181,6 @@ const builtInControllers = {
|
|
|
191
181
|
// Preset controllers
|
|
192
182
|
clearPresetCacheController: presetController.clearPresetCacheController,
|
|
193
183
|
|
|
194
|
-
// Cohort controllers
|
|
195
|
-
clearCohortCacheController: cohortController.clearCohortCacheController,
|
|
196
|
-
|
|
197
184
|
// Template controllers
|
|
198
185
|
createTemplate: templateController.createTemplate,
|
|
199
186
|
listTemplates: templateController.listTemplates,
|
|
@@ -221,10 +208,7 @@ const builtInControllers = {
|
|
|
221
208
|
getDashboardController: dashboardController.getDashboardController,
|
|
222
209
|
getDashboardStatsControllerById: dashboardController.getDashboardStatsControllerById,
|
|
223
210
|
getDashboardTrendController: dashboardController.getDashboardTrendController,
|
|
224
|
-
updateDashboardControllerById: dashboardController.updateDashboardControllerById
|
|
225
|
-
|
|
226
|
-
// Trace controllers
|
|
227
|
-
getTraceController: traceController.getTraceController
|
|
211
|
+
updateDashboardControllerById: dashboardController.updateDashboardControllerById
|
|
228
212
|
};
|
|
229
213
|
|
|
230
214
|
const setupDefaultRoutes = (app) => {
|
|
@@ -239,11 +223,9 @@ const setupDefaultRoutes = (app) => {
|
|
|
239
223
|
app.use('/api/message', createRouter(messageRouteDefinitions, builtInControllers));
|
|
240
224
|
app.use('/api/patient', createRouter(patientRouteDefinitions, builtInControllers));
|
|
241
225
|
app.use('/api/preset', createRouter(presetRouteDefinitions, builtInControllers));
|
|
242
|
-
app.use('/api/cohort', createRouter(cohortRouteDefinitions, builtInControllers));
|
|
243
226
|
app.use('/api/template', createRouter(templateRouteDefinitions, builtInControllers));
|
|
244
227
|
app.use('/api/prescription', createRouter(prescriptionRouteDefinitions, builtInControllers));
|
|
245
228
|
app.use('/api/dashboard', createRouter(dashboardRouteDefinitions, builtInControllers));
|
|
246
|
-
app.use('/api/trace', createRouter(traceRouteDefinitions, builtInControllers));
|
|
247
229
|
};
|
|
248
230
|
|
|
249
231
|
module.exports = {
|
|
@@ -253,11 +235,9 @@ module.exports = {
|
|
|
253
235
|
messageRoutes: messageRouteDefinitions,
|
|
254
236
|
patientRoutes: patientRouteDefinitions,
|
|
255
237
|
presetRoutes: presetRouteDefinitions,
|
|
256
|
-
cohortRoutes: cohortRouteDefinitions,
|
|
257
238
|
templateRoutes: templateRouteDefinitions,
|
|
258
239
|
prescriptionRoutes: prescriptionRouteDefinitions,
|
|
259
240
|
dashboardRoutes: dashboardRouteDefinitions,
|
|
260
|
-
traceRoutes: traceRouteDefinitions,
|
|
261
241
|
|
|
262
242
|
createRouter,
|
|
263
243
|
setupDefaultRoutes
|
|
@@ -78,8 +78,7 @@ async function getRecords(baseID, tableName) {
|
|
|
78
78
|
|
|
79
79
|
async function getRecordByFilter(baseID, tableName, filter, view = 'Grid view', fields) {
|
|
80
80
|
try {
|
|
81
|
-
const selectOptions = { filterByFormula: filter };
|
|
82
|
-
if (view) selectOptions.view = view;
|
|
81
|
+
const selectOptions = { filterByFormula: filter, view };
|
|
83
82
|
if (fields && fields.length) selectOptions.fields = fields;
|
|
84
83
|
return await withAirtableRetry(
|
|
85
84
|
() => collectRecords(getBase(baseID)(tableName).select(selectOptions)),
|
|
@@ -4,13 +4,10 @@ const { BaseDto } = require('./BaseDto');
|
|
|
4
4
|
|
|
5
5
|
const dateTime = z.iso.datetime({ offset: true });
|
|
6
6
|
|
|
7
|
-
const ESCALATION_PRIORITIES = ['critical', 'high', 'medium', 'low', 'routine', 'urgent', 'asap', 'stat'];
|
|
8
|
-
const ACTIVE_CARE_PLAN_STATUSES = ['armed', 'fired_reminder', 'fired_team'];
|
|
9
|
-
|
|
10
7
|
const escalationStepSchema = z.strictObject({
|
|
11
8
|
recipient: z.enum(['patient', 'care_team']),
|
|
12
9
|
message: z.string(),
|
|
13
|
-
priority: z.enum(
|
|
10
|
+
priority: z.enum(['critical', 'high', 'medium', 'low', 'routine', 'urgent', 'asap', 'stat']).nullable().default(null),
|
|
14
11
|
actionRequested: z.string().nullable().default(null),
|
|
15
12
|
firesAfterSilenceMinutes: z.number().int().nonnegative().nullable().default(null),
|
|
16
13
|
});
|
|
@@ -24,7 +21,7 @@ const carePlanSchema = z.strictObject({
|
|
|
24
21
|
awaitingCharacterization: z.string(),
|
|
25
22
|
whyThisMatters: z.string(),
|
|
26
23
|
creationReasoning: z.string(),
|
|
27
|
-
status: z.enum([
|
|
24
|
+
status: z.enum(['armed', 'fired_reminder', 'fired_team', 'auto_resolved', 'cancelled']).default('armed'),
|
|
28
25
|
createdAt: dateTime,
|
|
29
26
|
resolvedAt: dateTime.nullable().default(null),
|
|
30
27
|
resolvedReason: z.string().nullable().default(null),
|
|
@@ -47,8 +44,4 @@ class ContingencySafetyNet extends BaseDto {}
|
|
|
47
44
|
|
|
48
45
|
ContingencySafetyNet.schema = schema;
|
|
49
46
|
|
|
50
|
-
module.exports = {
|
|
51
|
-
ContingencySafetyNet,
|
|
52
|
-
ESCALATION_PRIORITIES,
|
|
53
|
-
ACTIVE_CARE_PLAN_STATUSES,
|
|
54
|
-
};
|
|
47
|
+
module.exports = { ContingencySafetyNet };
|
|
@@ -16,10 +16,15 @@ const baseFields = {
|
|
|
16
16
|
|
|
17
17
|
const notifyEscalation = z.strictObject({
|
|
18
18
|
...baseFields,
|
|
19
|
-
kind: z.enum(['page_navigator', 'page_oncologist', 'notify_911_guidance'
|
|
19
|
+
kind: z.enum(['page_navigator', 'page_oncologist', 'notify_911_guidance']),
|
|
20
20
|
recipient: z.string().nullable().default(null),
|
|
21
21
|
requiresAck: z.boolean().default(false),
|
|
22
|
-
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const followupEscalation = z.strictObject({
|
|
25
|
+
...baseFields,
|
|
26
|
+
kind: z.literal('schedule_followup'),
|
|
27
|
+
fireAt: dateTime,
|
|
23
28
|
followupPurpose: z.string().nullable().default(null),
|
|
24
29
|
});
|
|
25
30
|
|
|
@@ -30,7 +35,7 @@ const ticketEscalation = z.strictObject({
|
|
|
30
35
|
slaMinutes: z.number().int().nullable().default(null),
|
|
31
36
|
});
|
|
32
37
|
|
|
33
|
-
const schema = z.discriminatedUnion('kind', [notifyEscalation, ticketEscalation]);
|
|
38
|
+
const schema = z.discriminatedUnion('kind', [notifyEscalation, followupEscalation, ticketEscalation]);
|
|
34
39
|
|
|
35
40
|
class DispatchedEscalation extends BaseDto {}
|
|
36
41
|
|
|
@@ -21,8 +21,8 @@ const schema = z.strictObject({
|
|
|
21
21
|
.refine((d) => (d.status === 'cancelled') === (d.cancelReason !== null), {
|
|
22
22
|
message: 'cancelReason is set iff status is cancelled',
|
|
23
23
|
})
|
|
24
|
-
.refine((d) => d.recipientKind !== 'related_person' ||
|
|
25
|
-
message: 'a related_person recipient requires
|
|
24
|
+
.refine((d) => d.recipientKind !== 'related_person' || d.recipientRefId !== null, {
|
|
25
|
+
message: 'a related_person recipient requires recipientRefId',
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
class Reminder extends BaseDto {}
|
|
@@ -123,8 +123,6 @@ class MongoStorage {
|
|
|
123
123
|
prompt: messageData.prompt || null,
|
|
124
124
|
preset: messageData.preset || null,
|
|
125
125
|
response_id: messageData.response_id || null,
|
|
126
|
-
turnId: messageData.turnId || undefined,
|
|
127
|
-
traceId: messageData.traceId || undefined,
|
|
128
126
|
statusInfo: messageData.statusInfo,
|
|
129
127
|
clinical_context: messageData.clinicalContext || null,
|
|
130
128
|
triggeredBy: messageData.triggeredBy || null
|
package/package.json
CHANGED
|
@@ -1,68 +0,0 @@
|
|
|
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 };
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
const { Config_ID } = require('../../config/airtableConfig');
|
|
2
|
-
|
|
3
|
-
const { getRecordByFilter } = require('../../services/airtableService');
|
|
4
|
-
|
|
5
|
-
const { logger } = require('../../utils/logger');
|
|
6
|
-
const MapCache = require('../../utils/MapCache');
|
|
7
|
-
|
|
8
|
-
const CONFIG_TABLE = 'registry';
|
|
9
|
-
const CACHE_TTL = 5 * 60 * 1000;
|
|
10
|
-
const CACHE_KEY = 'composerConfig';
|
|
11
|
-
|
|
12
|
-
const cache = new MapCache({ maxSize: 1, ttl: CACHE_TTL });
|
|
13
|
-
|
|
14
|
-
async function load() {
|
|
15
|
-
const cached = cache.get(CACHE_KEY);
|
|
16
|
-
if (cached) return cached;
|
|
17
|
-
|
|
18
|
-
const records = await getRecordByFilter(Config_ID, CONFIG_TABLE, 'TRUE()');
|
|
19
|
-
if (!records) {
|
|
20
|
-
logger.warn('[composerConfig] could not read composer prompt blocks; composer will fall back to legacy render');
|
|
21
|
-
const fallback = { skillPrompt: '', constitution: '' };
|
|
22
|
-
cache.set(CACHE_KEY, fallback);
|
|
23
|
-
return fallback;
|
|
24
|
-
}
|
|
25
|
-
const byKey = Object.fromEntries(records.map((r) => [r.config, r.value]));
|
|
26
|
-
const config = {
|
|
27
|
-
skillPrompt: byKey.COMPOSER_SKILL_PROMPT || '',
|
|
28
|
-
constitution: byKey.COMPOSER_CONSTITUTION || '',
|
|
29
|
-
};
|
|
30
|
-
cache.set(CACHE_KEY, config);
|
|
31
|
-
return config;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async function getPromptBlocks() {
|
|
35
|
-
const { skillPrompt, constitution } = await load();
|
|
36
|
-
if (!skillPrompt || !constitution) return null;
|
|
37
|
-
return { skillPrompt, constitution };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
module.exports = {
|
|
41
|
-
getPromptBlocks,
|
|
42
|
-
};
|