@peopl-health/nexus 5.9.2 → 5.10.0-dev.1020
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/config/subAgentsConfig.js +1 -7
- package/lib/clinical/memory/DefaultMemoryManager.js +1 -1
- package/lib/clinical/providers/AnthropicProvider.js +0 -10
- package/lib/clinical/tools/{getPatientRiskAssessmentTool.js → getPatientRiskProfileTool.js} +4 -4
- package/lib/clinical/tools/getRouterContextBundleTool.js +1 -1
- package/lib/clinical/tools/registerClinicalTools.js +1 -1
- package/lib/fhir/constants/projectionSlugs.js +0 -12
- package/lib/fhir/index.js +1 -9
- package/lib/fhir/projections/registerProjectors.js +0 -6
- package/lib/fhir/resources/CarePlan.js +1 -23
- package/lib/fhir/resources/ClinicalImpression.js +1 -64
- package/lib/fhir/resources/Provenance.js +0 -20
- package/lib/fhir/resources/RiskAssessment.js +1 -40
- package/lib/fhir/resources/Task.js +0 -57
- package/lib/fhir/services/riskService.js +0 -19
- package/package.json +1 -1
- package/lib/clinical/helpers/palliativeAssessmentHelper.js +0 -62
- package/lib/clinical/models/patternConsultModel.js +0 -37
- package/lib/clinical/services/patternConsultService.js +0 -115
- package/lib/clinical/tools/requestPatternConsultTool.js +0 -70
- package/lib/clinical/tools/submitPalliativeAssessmentTool.js +0 -75
- package/lib/clinical/tools/submitRecommendationPlanTool.js +0 -242
- package/lib/clinical/tools/submitSafetyGateTool.js +0 -207
- package/lib/clinical/tools/updatePalliativeAssessmentTool.js +0 -74
- package/lib/fhir/projections/palliativeProjection.js +0 -37
- package/lib/fhir/projections/recommendationPlanProjection.js +0 -25
- package/lib/fhir/projections/safetyGateProjection.js +0 -16
- package/lib/fhir/services/palliativeService.js +0 -121
- package/lib/fhir/services/recommendationService.js +0 -27
- package/lib/shared/dtos/PalliativeAssessment.js +0 -62
- package/lib/shared/dtos/RecommendationPlan.js +0 -109
- package/lib/shared/dtos/RecommendationSafetyGate.js +0 -64
|
@@ -10,7 +10,6 @@ const CACHE_TTL = 5 * 60 * 1000;
|
|
|
10
10
|
const CACHE_KEY = 'subAgentsConfig';
|
|
11
11
|
const EXTRACTOR_DEFAULTS = { presetId: '' };
|
|
12
12
|
const COMPOSER_DEFAULTS = { presetId: '' };
|
|
13
|
-
const PATTERN_CONSULT_DEFAULTS = { presetId: '' };
|
|
14
13
|
|
|
15
14
|
const cache = new MapCache({ maxSize: 1, ttl: CACHE_TTL });
|
|
16
15
|
|
|
@@ -30,11 +29,7 @@ async function load() {
|
|
|
30
29
|
if (byKey.COMPOSER && !isPlainObject(parsedComposer)) throw new Error('subAgents config COMPOSER is not a JSON object');
|
|
31
30
|
const composer = { ...COMPOSER_DEFAULTS, ...(parsedComposer || {}) };
|
|
32
31
|
|
|
33
|
-
const
|
|
34
|
-
if (byKey.PATTERN_CONSULT && !isPlainObject(parsedPatternConsult)) throw new Error('subAgents config PATTERN_CONSULT is not a JSON object');
|
|
35
|
-
const patternConsult = { ...PATTERN_CONSULT_DEFAULTS, ...(parsedPatternConsult || {}) };
|
|
36
|
-
|
|
37
|
-
const config = { EXTRACTOR: extractor, COMPOSER: composer, PATTERN_CONSULT: patternConsult };
|
|
32
|
+
const config = { EXTRACTOR: extractor, COMPOSER: composer };
|
|
38
33
|
cache.set(CACHE_KEY, config);
|
|
39
34
|
return config;
|
|
40
35
|
}
|
|
@@ -46,5 +41,4 @@ async function get(key) {
|
|
|
46
41
|
module.exports = {
|
|
47
42
|
getExtractor: () => get('EXTRACTOR'),
|
|
48
43
|
getComposer: () => get('COMPOSER'),
|
|
49
|
-
getPatternConsult: () => get('PATTERN_CONSULT'),
|
|
50
44
|
};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const { Anthropic } = require('@anthropic-ai/sdk');
|
|
2
2
|
|
|
3
|
-
const { OPERATOR_TURN_PREFIX } = require('../memory/DefaultMemoryManager');
|
|
4
3
|
const { appendCall } = require('../services/llmAuditService');
|
|
5
4
|
const { composePrompt } = require('../services/promptComposerService');
|
|
6
5
|
const { retryWithBackoff } = require('../../utils/retryUtils');
|
|
@@ -385,15 +384,6 @@ class AnthropicProvider extends BaseLLMProvider {
|
|
|
385
384
|
return typeof output === 'string' ? output : JSON.stringify(output ?? '');
|
|
386
385
|
}
|
|
387
386
|
|
|
388
|
-
// Split placement: the inline copy carries recency on single-round turns, the hoisted developer
|
|
389
|
-
// turn persists once tool results are appended after it on multi-round turns.
|
|
390
|
-
_buildInput({ additionalInstructions, ...rest }) {
|
|
391
|
-
const input = super._buildInput({ additionalInstructions, ...rest });
|
|
392
|
-
if (!additionalInstructions) return input;
|
|
393
|
-
const { content } = this._operatorInstructionTurn(additionalInstructions);
|
|
394
|
-
return [...input, { role: 'user', content: `${OPERATOR_TURN_PREFIX} ${content}`, type: 'message' }];
|
|
395
|
-
}
|
|
396
|
-
|
|
397
387
|
_mapModelConfig(modelConfig) {
|
|
398
388
|
const mapped = { ...(modelConfig || {}) };
|
|
399
389
|
if (mapped.max_tokens == null && mapped.max_output_tokens != null) mapped.max_tokens = mapped.max_output_tokens;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const { readPatientRisk } = require('../../fhir');
|
|
2
2
|
|
|
3
3
|
const definition = {
|
|
4
|
-
name: '
|
|
5
|
-
description: '**Does:** Returns the patient\'s persistent risk
|
|
4
|
+
name: 'getPatientRiskProfile',
|
|
5
|
+
description: '**Does:** Returns the patient\'s persistent risk profile — predicted adverse events, disease risks, and comorbidity amplifiers derived from their regimen. Read-only.\n\n**Required inputs:** none.\n\n**When to call:** before routing, to anticipate which adverse events or complications the current regimen makes plausible.\n\n**Returns:** `status` (`final` when a profile exists, `not_generated` otherwise) and `profile` (`assessment_id`, `source_regimen`, `source_diagnosis`, `source_phase`, `predicted_aes[]`, `disease_risks[]`, `comorbidity_amplifiers[]`, `assessed_at`, `assessed_by_model`) — `null` when not yet generated.',
|
|
6
6
|
strict: false,
|
|
7
7
|
parameters: {
|
|
8
8
|
type: 'object',
|
|
@@ -55,12 +55,12 @@ async function handler(_args = {}, context = {}) {
|
|
|
55
55
|
try {
|
|
56
56
|
const runtime = context?.toolRuntimeContext || null;
|
|
57
57
|
if (!runtime?.turnId || !runtime?.patientCode) {
|
|
58
|
-
return JSON.stringify({ success: false, error: '
|
|
58
|
+
return JSON.stringify({ success: false, error: 'getPatientRiskProfile requires an active turn context (turnId, patientCode).', data: {} });
|
|
59
59
|
}
|
|
60
60
|
const data = await assembleRiskProfile(runtime.patientCode);
|
|
61
61
|
return JSON.stringify({ success: true, data });
|
|
62
62
|
} catch (err) {
|
|
63
|
-
return JSON.stringify({ success: false, error: err?.message || '
|
|
63
|
+
return JSON.stringify({ success: false, error: err?.message || 'getPatientRiskProfile failed', data: {} });
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const { assembleLandscape } = require('./getActiveSymptomLandscapeTool');
|
|
2
2
|
const { assembleHistory, SUPPORTED_SCOPES, TIMEFRAME_DAYS } = require('./getPatientHistoryTool');
|
|
3
|
-
const { assembleRiskProfile } = require('./
|
|
3
|
+
const { assembleRiskProfile } = require('./getPatientRiskProfileTool');
|
|
4
4
|
|
|
5
5
|
const ALLOWED_SECTIONS = ['landscape', 'history', 'risk'];
|
|
6
6
|
|
|
@@ -17,7 +17,7 @@ const TOOLS = [
|
|
|
17
17
|
require('./getActiveSymptomLandscapeTool'),
|
|
18
18
|
require('./recordInterventionTool'),
|
|
19
19
|
require('./setContingencyPlanTool'),
|
|
20
|
-
require('./
|
|
20
|
+
require('./getPatientRiskProfileTool'),
|
|
21
21
|
require('./getPatientHistoryTool'),
|
|
22
22
|
require('./getRouterContextBundleTool'),
|
|
23
23
|
require('./deliverPatientMessageTool'),
|
|
@@ -9,10 +9,6 @@ const SYMPTOM_CASE_SLUG = 'symptom-condition';
|
|
|
9
9
|
const SYMPTOM_ASSESSMENT_SLUG = 'symptom-assessment';
|
|
10
10
|
const SYMPTOM_GRADE_SLUG = 'symptom-grade';
|
|
11
11
|
const PATIENT_RISK_SLUG = 'patient-risk-assessment';
|
|
12
|
-
const SAFETY_GATE_SLUG = 'recommendation-safety';
|
|
13
|
-
const RECOMMENDATION_PLAN_SLUG = 'recommendation-careplan';
|
|
14
|
-
const RECOMMENDATION_ELEMENTS_SLUG = 'recommendation-palliative';
|
|
15
|
-
const RECOMMENDATION_REC_SLUG = 'recommendation-rec';
|
|
16
12
|
const CONTINGENCY_PLAN_SLUG = 'contingency-plan';
|
|
17
13
|
const CONTINGENCY_STEP_SLUG = 'contingency-step';
|
|
18
14
|
const INTERVENTION_SLUG = 'intervention';
|
|
@@ -22,8 +18,6 @@ const CLUSTER_IMPRESSION_SLUG = 'cluster-impression';
|
|
|
22
18
|
const CLUSTER_LABEL_SLUG = 'cluster-label';
|
|
23
19
|
const ROUTING_ASSESSMENT_SLUG = 'routing-assessment';
|
|
24
20
|
const ROUTING_DISPOSITION_SLUG = 'routing-disposition';
|
|
25
|
-
const PALLIATIVE_ASSESSMENT_SLUG = 'palliative-assessment';
|
|
26
|
-
const PALLIATIVE_ESCALATION_SLUG = 'palliative-escalation';
|
|
27
21
|
|
|
28
22
|
module.exports = {
|
|
29
23
|
MENTION_SLUG,
|
|
@@ -37,10 +31,6 @@ module.exports = {
|
|
|
37
31
|
SYMPTOM_ASSESSMENT_SLUG,
|
|
38
32
|
SYMPTOM_GRADE_SLUG,
|
|
39
33
|
PATIENT_RISK_SLUG,
|
|
40
|
-
SAFETY_GATE_SLUG,
|
|
41
|
-
RECOMMENDATION_PLAN_SLUG,
|
|
42
|
-
RECOMMENDATION_ELEMENTS_SLUG,
|
|
43
|
-
RECOMMENDATION_REC_SLUG,
|
|
44
34
|
CONTINGENCY_PLAN_SLUG,
|
|
45
35
|
CONTINGENCY_STEP_SLUG,
|
|
46
36
|
INTERVENTION_SLUG,
|
|
@@ -50,6 +40,4 @@ module.exports = {
|
|
|
50
40
|
CLUSTER_LABEL_SLUG,
|
|
51
41
|
ROUTING_ASSESSMENT_SLUG,
|
|
52
42
|
ROUTING_DISPOSITION_SLUG,
|
|
53
|
-
PALLIATIVE_ASSESSMENT_SLUG,
|
|
54
|
-
PALLIATIVE_ESCALATION_SLUG,
|
|
55
43
|
};
|
package/lib/fhir/index.js
CHANGED
|
@@ -2,13 +2,11 @@ const { registerProjector, project, storeResources } = require('./services/fhirS
|
|
|
2
2
|
const { storeClinicalMentions } = require('./services/clinicalMentionService');
|
|
3
3
|
const { storeResultObservations } = require('./services/resultObservationService');
|
|
4
4
|
const { storeSymptomCase, readSymptomCases } = require('./services/symptomCaseService');
|
|
5
|
-
const { storePatientRisk, readPatientRisk
|
|
6
|
-
const { storeRecommendationPlan, recommendationPlanExists } = require('./services/recommendationService');
|
|
5
|
+
const { storePatientRisk, readPatientRisk } = require('./services/riskService');
|
|
7
6
|
const { storeContingency, readContingencies } = require('./services/contingencyService');
|
|
8
7
|
const { storeIntervention, readInterventions } = require('./services/interventionService');
|
|
9
8
|
const { storeCluster, readClusters } = require('./services/clusterService');
|
|
10
9
|
const { storeRoutingDecision, readRoutingByTurn } = require('./services/routingService');
|
|
11
|
-
const { storePalliativeAssessment, readPalliativeByTurn } = require('./services/palliativeService');
|
|
12
10
|
const { storeTriage } = require('./services/triageService');
|
|
13
11
|
const { patientSnapshot } = require('./services/snapshotService');
|
|
14
12
|
const { registerAllProjectors } = require('./projections/registerProjectors');
|
|
@@ -32,10 +30,6 @@ module.exports = {
|
|
|
32
30
|
readSymptomCases,
|
|
33
31
|
storePatientRisk,
|
|
34
32
|
readPatientRisk,
|
|
35
|
-
storeSafetyGate,
|
|
36
|
-
safetyGateExists,
|
|
37
|
-
storeRecommendationPlan,
|
|
38
|
-
recommendationPlanExists,
|
|
39
33
|
storeContingency,
|
|
40
34
|
readContingencies,
|
|
41
35
|
storeIntervention,
|
|
@@ -44,8 +38,6 @@ module.exports = {
|
|
|
44
38
|
readClusters,
|
|
45
39
|
storeRoutingDecision,
|
|
46
40
|
readRoutingByTurn,
|
|
47
|
-
storePalliativeAssessment,
|
|
48
|
-
readPalliativeByTurn,
|
|
49
41
|
storeTriage,
|
|
50
42
|
patientSnapshot,
|
|
51
43
|
getFhirStore,
|
|
@@ -4,13 +4,10 @@ const { projectDispatchedEscalation, PROJECTOR_NAME: ESCALATION_PROJECTOR_NAME }
|
|
|
4
4
|
const { projectReminder, PROJECTOR_NAME: REMINDER_PROJECTOR_NAME } = require('./reminderProjection');
|
|
5
5
|
const { projectSymptomCase, PROJECTOR_NAME: SYMPTOM_CASE_PROJECTOR_NAME } = require('./symptomCaseProjection');
|
|
6
6
|
const { projectPatientRisk, PROJECTOR_NAME: RISK_PROJECTOR_NAME } = require('./riskProjection');
|
|
7
|
-
const { projectSafetyGate, PROJECTOR_NAME: SAFETY_GATE_PROJECTOR_NAME } = require('./safetyGateProjection');
|
|
8
|
-
const { projectRecommendationPlan, PROJECTOR_NAME: RECOMMENDATION_PLAN_PROJECTOR_NAME } = require('./recommendationPlanProjection');
|
|
9
7
|
const { projectContingency, PROJECTOR_NAME: CONTINGENCY_PROJECTOR_NAME } = require('./contingencyProjection');
|
|
10
8
|
const { projectIntervention, PROJECTOR_NAME: INTERVENTION_PROJECTOR_NAME } = require('./interventionProjection');
|
|
11
9
|
const { projectCluster, PROJECTOR_NAME: CLUSTER_PROJECTOR_NAME } = require('./clusterProjection');
|
|
12
10
|
const { projectRoutingDecision, PROJECTOR_NAME: ROUTING_PROJECTOR_NAME } = require('./routingProjection');
|
|
13
|
-
const { projectPalliativeAssessment, PROJECTOR_NAME: PALLIATIVE_PROJECTOR_NAME } = require('./palliativeProjection');
|
|
14
11
|
|
|
15
12
|
const PROJECTORS = [
|
|
16
13
|
{ name: MENTION_PROJECTOR_NAME, project: projectClinicalMention },
|
|
@@ -18,13 +15,10 @@ const PROJECTORS = [
|
|
|
18
15
|
{ name: REMINDER_PROJECTOR_NAME, project: projectReminder },
|
|
19
16
|
{ name: SYMPTOM_CASE_PROJECTOR_NAME, project: projectSymptomCase },
|
|
20
17
|
{ name: RISK_PROJECTOR_NAME, project: projectPatientRisk },
|
|
21
|
-
{ name: SAFETY_GATE_PROJECTOR_NAME, project: projectSafetyGate },
|
|
22
|
-
{ name: RECOMMENDATION_PLAN_PROJECTOR_NAME, project: projectRecommendationPlan },
|
|
23
18
|
{ name: CONTINGENCY_PROJECTOR_NAME, project: projectContingency },
|
|
24
19
|
{ name: INTERVENTION_PROJECTOR_NAME, project: projectIntervention },
|
|
25
20
|
{ name: CLUSTER_PROJECTOR_NAME, project: projectCluster },
|
|
26
21
|
{ name: ROUTING_PROJECTOR_NAME, project: projectRoutingDecision },
|
|
27
|
-
{ name: PALLIATIVE_PROJECTOR_NAME, project: projectPalliativeAssessment },
|
|
28
22
|
];
|
|
29
23
|
|
|
30
24
|
function registerAllProjectors() {
|
|
@@ -7,7 +7,7 @@ const {
|
|
|
7
7
|
extension,
|
|
8
8
|
toIso,
|
|
9
9
|
} = require('../helpers/elementHelper');
|
|
10
|
-
const { CONTINGENCY_PLAN_SLUG, ROUTING_DISPOSITION_SLUG
|
|
10
|
+
const { CONTINGENCY_PLAN_SLUG, ROUTING_DISPOSITION_SLUG } = require('../constants/projectionSlugs');
|
|
11
11
|
|
|
12
12
|
const CONTINGENCY_STATUS_TO_FHIR = {
|
|
13
13
|
armed: 'active',
|
|
@@ -45,28 +45,6 @@ class CarePlan {
|
|
|
45
45
|
return new CarePlan(payload);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
static fromRecommendationPlan({ plan, taskIds, hasSafetyGate }) {
|
|
49
|
-
const payload = {
|
|
50
|
-
resourceType: 'CarePlan',
|
|
51
|
-
id: fhirId(`${plan.runId}-careplan`),
|
|
52
|
-
identifier: [identifier(RECOMMENDATION_PLAN_SLUG, `${plan.runId}-careplan`)],
|
|
53
|
-
status: 'active',
|
|
54
|
-
intent: 'plan',
|
|
55
|
-
category: [codeableConcept('symptom_management')],
|
|
56
|
-
subject: patientReference(plan.patientId),
|
|
57
|
-
created: toIso(plan.createdAt),
|
|
58
|
-
activity: taskIds.map((taskId) => ({
|
|
59
|
-
plannedActivityReference: reference('Task', taskId),
|
|
60
|
-
})),
|
|
61
|
-
};
|
|
62
|
-
if (hasSafetyGate) {
|
|
63
|
-
payload.supportingInfo = [reference('RiskAssessment', `${plan.runId}-safety`)];
|
|
64
|
-
}
|
|
65
|
-
if (plan.takeaways.summaryInternal) payload.description = plan.takeaways.summaryInternal;
|
|
66
|
-
if (plan.takeaways.summaryPatientFacing) payload.note = [{ text: plan.takeaways.summaryPatientFacing }];
|
|
67
|
-
return new CarePlan(payload);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
48
|
static fromRoutingDisposition({ disposition }) {
|
|
71
49
|
const payload = {
|
|
72
50
|
resourceType: 'CarePlan',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const { fhirId } = require('../helpers/fhirHelper');
|
|
2
2
|
const { identifier, reference, patientReference, codeableConcept, extension, toIso } = require('../helpers/elementHelper');
|
|
3
|
-
const { SYMPTOM_ASSESSMENT_SLUG, CLUSTER_IMPRESSION_SLUG, CLUSTER_LABEL_SLUG, ROUTING_ASSESSMENT_SLUG
|
|
3
|
+
const { SYMPTOM_ASSESSMENT_SLUG, CLUSTER_IMPRESSION_SLUG, CLUSTER_LABEL_SLUG, ROUTING_ASSESSMENT_SLUG } = require('../constants/projectionSlugs');
|
|
4
4
|
|
|
5
5
|
const CLUSTER_STATUS_TO_FHIR = { active: 'in-progress', dissolved: 'completed' };
|
|
6
6
|
const CLUSTER_LIST_SEPARATOR = '; ';
|
|
@@ -76,30 +76,6 @@ class ClinicalImpression {
|
|
|
76
76
|
return new ClinicalImpression(payload);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
static fromRecommendationPlan({ plan }) {
|
|
80
|
-
const finding = [...plan.elements]
|
|
81
|
-
.sort((a, b) => a.rank - b.rank)
|
|
82
|
-
.map((element) => {
|
|
83
|
-
const basis = [`rank: ${element.rank}`, `tipo: ${element.type}`];
|
|
84
|
-
if (element.qolImpact) basis.push(`QoL: ${element.qolImpact}`);
|
|
85
|
-
if (element.whyRelevant) basis.push(`relevancia: ${element.whyRelevant}`);
|
|
86
|
-
const grounding = element.groundedIn.filter(Boolean).join('; ');
|
|
87
|
-
if (grounding) basis.push(`sustento: ${grounding}`);
|
|
88
|
-
return { item: { concept: codeableConcept(element.nameShort) }, basis: basis.join('; ') };
|
|
89
|
-
});
|
|
90
|
-
const payload = {
|
|
91
|
-
resourceType: 'ClinicalImpression',
|
|
92
|
-
id: fhirId(`${plan.runId}-palliative`),
|
|
93
|
-
identifier: [identifier(RECOMMENDATION_ELEMENTS_SLUG, `${plan.runId}-palliative`)],
|
|
94
|
-
status: 'completed',
|
|
95
|
-
subject: patientReference(plan.patientId),
|
|
96
|
-
effectiveDateTime: toIso(plan.createdAt),
|
|
97
|
-
finding,
|
|
98
|
-
};
|
|
99
|
-
if (plan.takeaways.summaryInternal) payload.summary = plan.takeaways.summaryInternal;
|
|
100
|
-
return new ClinicalImpression(payload);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
79
|
static fromRoutingAssessment({ assessment, decisionId, committedAt, deferredItems = [], primaryCaseId = null }) {
|
|
104
80
|
const extensions = [
|
|
105
81
|
extension('routing-turn-id', { valueString: assessment.turnId }),
|
|
@@ -133,45 +109,6 @@ class ClinicalImpression {
|
|
|
133
109
|
if (assessment.notes.length) payload.note = assessment.notes.map((text) => ({ text }));
|
|
134
110
|
return new ClinicalImpression(payload);
|
|
135
111
|
}
|
|
136
|
-
|
|
137
|
-
static fromPalliativeAssessment({ assessment }) {
|
|
138
|
-
const payload = {
|
|
139
|
-
resourceType: 'ClinicalImpression',
|
|
140
|
-
id: fhirId(assessment.assessmentId),
|
|
141
|
-
identifier: [identifier(PALLIATIVE_ASSESSMENT_SLUG, assessment.assessmentId)],
|
|
142
|
-
status: 'completed',
|
|
143
|
-
subject: patientReference(assessment.patientId),
|
|
144
|
-
effectiveDateTime: toIso(assessment.committedAt),
|
|
145
|
-
finding: assessment.findings.map(palliativeFinding),
|
|
146
|
-
extension: [extension('palliative-turn-id', { valueString: assessment.turnId })],
|
|
147
|
-
};
|
|
148
|
-
if (assessment.evaluationSummary) payload.summary = assessment.evaluationSummary;
|
|
149
|
-
if (assessment.rankingRationale) payload.description = assessment.rankingRationale;
|
|
150
|
-
if (assessment.escalationReason) payload.note = [{ text: assessment.escalationReason }];
|
|
151
|
-
return new ClinicalImpression(payload);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function palliativeFinding(finding) {
|
|
156
|
-
const extensions = [
|
|
157
|
-
extension('palliative-finding-rank', { valueInteger: finding.rank }),
|
|
158
|
-
extension('palliative-finding-element-type', { valueString: finding.elementType }),
|
|
159
|
-
extension('palliative-finding-time-window', { valueString: finding.timeWindow }),
|
|
160
|
-
extension('palliative-finding-clinical-description', { valueString: finding.clinicalDescription }),
|
|
161
|
-
extension('palliative-finding-why-relevant', { valueString: finding.whyRelevant }),
|
|
162
|
-
extension('palliative-finding-qol-criteria', { valueString: finding.qolCriteria }),
|
|
163
|
-
extension('palliative-finding-improvement-criteria', { valueString: finding.improvementCriteria }),
|
|
164
|
-
extension('palliative-finding-characterization-criteria', { valueString: finding.characterizationCriteria }),
|
|
165
|
-
extension('palliative-finding-audit-notes', { valueString: finding.auditNotes }),
|
|
166
|
-
];
|
|
167
|
-
for (const term of finding.symptomTerms) extensions.push(extension('palliative-finding-symptom-term', { valueString: term }));
|
|
168
|
-
for (const question of finding.probeQuestions) extensions.push(extension('palliative-finding-probe-question', { valueString: question }));
|
|
169
|
-
if (finding.qolImpact !== null) extensions.push(extension('palliative-finding-qol-impact', { valueString: finding.qolImpact }));
|
|
170
|
-
if (finding.improvementCapacity !== null) extensions.push(extension('palliative-finding-improvement-capacity', { valueString: finding.improvementCapacity }));
|
|
171
|
-
if (finding.areMedsNeeded !== null) extensions.push(extension('palliative-finding-are-meds-needed', { valueBoolean: finding.areMedsNeeded }));
|
|
172
|
-
if (finding.needsCharacterization !== null) extensions.push(extension('palliative-finding-needs-characterization', { valueBoolean: finding.needsCharacterization }));
|
|
173
|
-
if (finding.clusterNote !== null) extensions.push(extension('palliative-finding-cluster-note', { valueString: finding.clusterNote }));
|
|
174
|
-
return { item: { concept: codeableConcept(finding.nameShort) }, extension: extensions };
|
|
175
112
|
}
|
|
176
113
|
|
|
177
114
|
module.exports = {
|
|
@@ -89,26 +89,6 @@ class Provenance {
|
|
|
89
89
|
});
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
static fromSafetyGate({ gate }) {
|
|
93
|
-
return Provenance.fromSymptomCase({
|
|
94
|
-
provenanceId: `${gate.runId}-safety-prov`,
|
|
95
|
-
targetRefs: [{ reference: `RiskAssessment/${fhirId(`${gate.runId}-safety`)}` }],
|
|
96
|
-
recordedAt: gate.createdAt,
|
|
97
|
-
activityText: 'recommendation-safety-gate',
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
static fromRecommendationPlan({ plan, targetRefs }) {
|
|
102
|
-
return new Provenance({
|
|
103
|
-
resourceType: 'Provenance',
|
|
104
|
-
id: fhirId(`${plan.runId}-careplan-prov`),
|
|
105
|
-
target: targetRefs,
|
|
106
|
-
recorded: toIso(plan.createdAt),
|
|
107
|
-
agent: [{ who: reference('Device', getDeviceId()), onBehalfOf: reference('Organization', getOrganizationId()) }],
|
|
108
|
-
activity: codeableConcept('recommendations-process'),
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
92
|
static fromContingency({ planId, recipients, recordedAt, activityText }) {
|
|
113
93
|
const targets = [{ reference: `CarePlan/${fhirId(planId)}` }];
|
|
114
94
|
for (const recipient of recipients) {
|
|
@@ -8,30 +8,13 @@ const {
|
|
|
8
8
|
extension,
|
|
9
9
|
toIso,
|
|
10
10
|
} = require('../helpers/elementHelper');
|
|
11
|
-
const { PATIENT_RISK_SLUG
|
|
11
|
+
const { PATIENT_RISK_SLUG } = require('../constants/projectionSlugs');
|
|
12
12
|
|
|
13
13
|
const RISK_SEVERITY_CODESYSTEM = 'risk-severity';
|
|
14
14
|
const PREDICTION_KIND_AE = 'ae';
|
|
15
15
|
const PREDICTION_KIND_DISEASE = 'disease';
|
|
16
16
|
const PREDICTION_KIND_COMORBIDITY = 'comorbidity';
|
|
17
17
|
|
|
18
|
-
const PROBABILITY_TO_RISK = { high: 'high', medium: 'moderate', low: 'low' };
|
|
19
|
-
const PREDICTION_BASIS_STATUSES = ['confirmed', 'partially_confirmed'];
|
|
20
|
-
|
|
21
|
-
function safetyGatePrediction(ae) {
|
|
22
|
-
const risk = PROBABILITY_TO_RISK[ae.probabilityGivenPatient] || ae.probabilityGivenPatient;
|
|
23
|
-
const prediction = {
|
|
24
|
-
outcome: codeableConcept(ae.ctcaeTerm),
|
|
25
|
-
qualitativeRisk: codeableConcept(risk, { code: risk, system: codeSystemUrl(RISK_SEVERITY_CODESYSTEM) }),
|
|
26
|
-
};
|
|
27
|
-
const rationaleBits = [];
|
|
28
|
-
if (ae.whyIsRelevant) rationaleBits.push(ae.whyIsRelevant);
|
|
29
|
-
const grounding = ae.groundedIn.filter(Boolean).join('; ');
|
|
30
|
-
if (grounding) rationaleBits.push(`sustento: ${grounding}`);
|
|
31
|
-
if (rationaleBits.length) prediction.rationale = rationaleBits.join(' | ');
|
|
32
|
-
return prediction;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
18
|
function aePrediction(ae) {
|
|
36
19
|
const prediction = {
|
|
37
20
|
outcome: codeableConcept(ae.aeName),
|
|
@@ -106,28 +89,6 @@ class RiskAssessment {
|
|
|
106
89
|
if (extensions.length) payload.extension = extensions;
|
|
107
90
|
return new RiskAssessment(payload);
|
|
108
91
|
}
|
|
109
|
-
|
|
110
|
-
static fromSafetyGate({ gate }) {
|
|
111
|
-
const payload = {
|
|
112
|
-
resourceType: 'RiskAssessment',
|
|
113
|
-
id: fhirId(`${gate.runId}-safety`),
|
|
114
|
-
identifier: [identifier(SAFETY_GATE_SLUG, `${gate.runId}-safety`)],
|
|
115
|
-
status: 'final',
|
|
116
|
-
subject: patientReference(gate.patientId),
|
|
117
|
-
performer: reference('Device', getDeviceId()),
|
|
118
|
-
occurrenceDateTime: toIso(gate.createdAt),
|
|
119
|
-
};
|
|
120
|
-
const prediction = gate.redFlags
|
|
121
|
-
.filter((redFlag) => PREDICTION_BASIS_STATUSES.includes(redFlag.verificationStatus))
|
|
122
|
-
.flatMap((redFlag) => redFlag.relatedAes.map(safetyGatePrediction));
|
|
123
|
-
if (prediction.length) payload.prediction = prediction;
|
|
124
|
-
if (gate.summary) payload.note = [{ text: gate.summary }];
|
|
125
|
-
if (gate.urgentUnder24h && gate.escalationRoute) {
|
|
126
|
-
payload.mitigation = `ruta de escalamiento: ${gate.escalationRoute}`
|
|
127
|
-
+ (gate.maxActionTimeHours ? `; actuar en ≤${gate.maxActionTimeHours}h` : '');
|
|
128
|
-
}
|
|
129
|
-
return new RiskAssessment(payload);
|
|
130
|
-
}
|
|
131
92
|
}
|
|
132
93
|
|
|
133
94
|
module.exports = {
|
|
@@ -16,8 +16,6 @@ const {
|
|
|
16
16
|
INTERVENTION_KIND_SLUG,
|
|
17
17
|
INTERVENTION_OUTCOME_SLUG,
|
|
18
18
|
ROUTING_DISPOSITION_SLUG,
|
|
19
|
-
RECOMMENDATION_REC_SLUG,
|
|
20
|
-
PALLIATIVE_ESCALATION_SLUG,
|
|
21
19
|
} = require('../constants/projectionSlugs');
|
|
22
20
|
const { SEVERITY_TO_PRIORITY } = require('../constants/severityPriority');
|
|
23
21
|
const { interventionExtensions } = require('./interventionExtensions');
|
|
@@ -85,61 +83,6 @@ class Task {
|
|
|
85
83
|
return new Task(payload);
|
|
86
84
|
}
|
|
87
85
|
|
|
88
|
-
static fromRecommendationPlan({ plan, element, taskId }) {
|
|
89
|
-
const { recommendation } = element;
|
|
90
|
-
const { patient } = recommendation;
|
|
91
|
-
const code = {
|
|
92
|
-
text: element.nameShort,
|
|
93
|
-
coding: element.targetedCtcaeTerms.map((term) => ({
|
|
94
|
-
system: codeSystemUrl('pro-ctcae'), code: term, display: term,
|
|
95
|
-
})),
|
|
96
|
-
};
|
|
97
|
-
const noteTexts = [];
|
|
98
|
-
if (patient.whenToEscalate) noteTexts.push(`cuándo escalar: ${patient.whenToEscalate}`);
|
|
99
|
-
if (patient.expectedEffectiveness) noteTexts.push(`efectividad esperada: ${patient.expectedEffectiveness}`);
|
|
100
|
-
if (patient.contraindications) noteTexts.push(`contraindicaciones: ${patient.contraindications}`);
|
|
101
|
-
noteTexts.push(`requiere coordinación farmacológica: ${recommendation.isPharmacologicalMedNeeded}`);
|
|
102
|
-
const payload = {
|
|
103
|
-
resourceType: 'Task',
|
|
104
|
-
id: fhirId(taskId),
|
|
105
|
-
identifier: [identifier(RECOMMENDATION_REC_SLUG, taskId)],
|
|
106
|
-
status: 'requested',
|
|
107
|
-
intent: 'plan',
|
|
108
|
-
code,
|
|
109
|
-
description: patient.mainRecommendation,
|
|
110
|
-
for: patientReference(plan.patientId),
|
|
111
|
-
authoredOn: toIso(plan.createdAt),
|
|
112
|
-
requester: reference('Device', getDeviceId()),
|
|
113
|
-
note: noteTexts.map((text) => ({ text })),
|
|
114
|
-
};
|
|
115
|
-
return new Task(payload);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
static fromPalliativeEscalation({ assessmentId, committedAt, finding, deviceId }) {
|
|
119
|
-
const { escalation } = finding;
|
|
120
|
-
const start = new Date(committedAt);
|
|
121
|
-
const end = new Date(start.getTime() + escalation.maxActionTimeHours * 3600000);
|
|
122
|
-
const payload = {
|
|
123
|
-
resourceType: 'Task',
|
|
124
|
-
id: fhirId(`${assessmentId}-${finding.rank}`),
|
|
125
|
-
identifier: [identifier(PALLIATIVE_ESCALATION_SLUG, `${assessmentId}-${finding.rank}`)],
|
|
126
|
-
status: 'requested',
|
|
127
|
-
intent: 'order',
|
|
128
|
-
priority: 'urgent',
|
|
129
|
-
code: codeableConcept(finding.nameShort),
|
|
130
|
-
description: escalation.escalationReason,
|
|
131
|
-
for: patientReference(escalation.patientId),
|
|
132
|
-
authoredOn: toIso(committedAt),
|
|
133
|
-
requester: reference('Device', deviceId),
|
|
134
|
-
restriction: { period: { end: toIso(end) } },
|
|
135
|
-
extension: [extension('palliative-escalation-rank', { valueInteger: finding.rank })],
|
|
136
|
-
};
|
|
137
|
-
if (escalation.escalationRoutes.length) {
|
|
138
|
-
payload.requestedPerformer = escalation.escalationRoutes.map((route) => ({ concept: codeableConcept(route) }));
|
|
139
|
-
}
|
|
140
|
-
return new Task(payload);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
86
|
static fromRoutingDisposition({ disposition }) {
|
|
144
87
|
const payload = {
|
|
145
88
|
resourceType: 'Task',
|
|
@@ -4,7 +4,6 @@ const { fhirId } = require('../helpers/fhirHelper');
|
|
|
4
4
|
const { identifierValue, extValues } = require('../helpers/fhirReadHelper');
|
|
5
5
|
const { PATIENT_RISK_SLUG } = require('../constants/projectionSlugs');
|
|
6
6
|
const { PROJECTOR_NAME } = require('../projections/riskProjection');
|
|
7
|
-
const { PROJECTOR_NAME: SAFETY_GATE_PROJECTOR_NAME } = require('../projections/safetyGateProjection');
|
|
8
7
|
const {
|
|
9
8
|
PREDICTION_KIND_AE,
|
|
10
9
|
PREDICTION_KIND_DISEASE,
|
|
@@ -44,22 +43,6 @@ async function readPatientRisk({ patientId }) {
|
|
|
44
43
|
return null;
|
|
45
44
|
}
|
|
46
45
|
|
|
47
|
-
async function storeSafetyGate({ patientId, safetyGate }) {
|
|
48
|
-
if (safetyGate.patientId !== patientId) {
|
|
49
|
-
throw new Error(`storeSafetyGate patientId mismatch: expected ${patientId}`);
|
|
50
|
-
}
|
|
51
|
-
const bundle = project([{ name: SAFETY_GATE_PROJECTOR_NAME, aggregate: safetyGate }]);
|
|
52
|
-
const resources = bundle.entry.map((entry) => entry.resource);
|
|
53
|
-
const { stored } = await storeResources(resources);
|
|
54
|
-
return { count: stored.length, ids: stored };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async function safetyGateExists({ runId }) {
|
|
58
|
-
const store = getFhirStore();
|
|
59
|
-
const resource = await store.getByKey(`Provenance|${fhirId(`${runId}-safety-prov`)}`);
|
|
60
|
-
return Boolean(resource);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
46
|
function reconstructRisk(resource, patientId) {
|
|
64
47
|
const predictions = resource.prediction || [];
|
|
65
48
|
return {
|
|
@@ -115,6 +98,4 @@ const outcomeText = (prediction) => (prediction.outcome && prediction.outcome.te
|
|
|
115
98
|
module.exports = {
|
|
116
99
|
storePatientRisk,
|
|
117
100
|
readPatientRisk,
|
|
118
|
-
storeSafetyGate,
|
|
119
|
-
safetyGateExists,
|
|
120
101
|
};
|
package/package.json
CHANGED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
const crypto = require('node:crypto');
|
|
2
|
-
|
|
3
|
-
const { PalliativeAssessment } = require('../../shared/dtos/PalliativeAssessment');
|
|
4
|
-
|
|
5
|
-
const uid = (prefix) => `${prefix}_${crypto.randomUUID().replace(/-/g, '').slice(0, 12)}`;
|
|
6
|
-
|
|
7
|
-
function assessmentUid() {
|
|
8
|
-
return uid('pa');
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function mapEscalation(raw, patientId) {
|
|
12
|
-
if (!raw || typeof raw !== 'object') return null;
|
|
13
|
-
return {
|
|
14
|
-
patientId,
|
|
15
|
-
maxActionTimeHours: raw.max_action_time_hours,
|
|
16
|
-
escalationRoutes: raw.escalation_routes || [],
|
|
17
|
-
escalationReason: raw.escalation_reason || '',
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function mapFinding(raw, patientId) {
|
|
22
|
-
if (!raw || typeof raw !== 'object') return {};
|
|
23
|
-
return {
|
|
24
|
-
rank: raw.rank,
|
|
25
|
-
elementType: raw.element_type,
|
|
26
|
-
nameShort: raw.name_short,
|
|
27
|
-
timeWindow: raw.time_window || '',
|
|
28
|
-
symptomTerms: raw.symptom_terms || [],
|
|
29
|
-
clinicalDescription: raw.clinical_description || '',
|
|
30
|
-
whyRelevant: raw.why_relevant || '',
|
|
31
|
-
qolImpact: raw.qol_impact ?? null,
|
|
32
|
-
qolCriteria: raw.qol_criteria || '',
|
|
33
|
-
improvementCapacity: raw.improvement_capacity ?? null,
|
|
34
|
-
improvementCriteria: raw.improvement_criteria || '',
|
|
35
|
-
areMedsNeeded: raw.are_meds_needed ?? null,
|
|
36
|
-
needsCharacterization: raw.needs_characterization ?? null,
|
|
37
|
-
characterizationCriteria: raw.characterization_criteria || '',
|
|
38
|
-
probeQuestions: raw.probe_questions || [],
|
|
39
|
-
auditNotes: raw.audit_notes || '',
|
|
40
|
-
clusterNote: raw.cluster_note ?? null,
|
|
41
|
-
escalation: mapEscalation(raw.escalation, patientId),
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function buildAssessment({ raw, assessmentId, patientId, turnId, committedAt }) {
|
|
46
|
-
const findings = Array.isArray(raw.findings) ? raw.findings : [];
|
|
47
|
-
return new PalliativeAssessment({
|
|
48
|
-
assessmentId,
|
|
49
|
-
patientId,
|
|
50
|
-
turnId,
|
|
51
|
-
committedAt,
|
|
52
|
-
evaluationSummary: raw.evaluation_summary || '',
|
|
53
|
-
rankingRationale: raw.ranking_rationale || '',
|
|
54
|
-
escalationReason: raw.escalation_reason ?? null,
|
|
55
|
-
findings: findings.map((finding) => mapFinding(finding, patientId)),
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
module.exports = {
|
|
60
|
-
assessmentUid,
|
|
61
|
-
buildAssessment,
|
|
62
|
-
};
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
const mongoose = require('mongoose');
|
|
2
|
-
|
|
3
|
-
const { clinicalConnection } = require('../config/connection');
|
|
4
|
-
|
|
5
|
-
const patternConsultSchema = new mongoose.Schema({
|
|
6
|
-
consultationId: { type: String, required: true },
|
|
7
|
-
patientCode: { type: String, required: true },
|
|
8
|
-
turnId: { type: String, default: null },
|
|
9
|
-
concern: { type: String, default: null },
|
|
10
|
-
impressions: { type: Array, default: [] },
|
|
11
|
-
recommendedProbes: { type: [String], default: [] },
|
|
12
|
-
watchFor: { type: [String], default: [] },
|
|
13
|
-
escalationAdvice: { type: Object, default: null },
|
|
14
|
-
degraded: { type: Boolean, default: false },
|
|
15
|
-
degradedReasons: { type: [String], default: [] },
|
|
16
|
-
}, { timestamps: true });
|
|
17
|
-
|
|
18
|
-
patternConsultSchema.index({ consultationId: 1 }, { unique: true });
|
|
19
|
-
patternConsultSchema.index({ patientCode: 1, createdAt: -1 });
|
|
20
|
-
|
|
21
|
-
function patternConsultStore() {
|
|
22
|
-
const conn = clinicalConnection();
|
|
23
|
-
const model = conn.models.PatternConsult || conn.model('PatternConsult', patternConsultSchema, 'patternConsult');
|
|
24
|
-
|
|
25
|
-
return {
|
|
26
|
-
get(consultationId) {
|
|
27
|
-
return model.findOne({ consultationId }).lean();
|
|
28
|
-
},
|
|
29
|
-
put(doc) {
|
|
30
|
-
return model.create(doc);
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
module.exports = {
|
|
36
|
-
patternConsultStore,
|
|
37
|
-
};
|