@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,44 +0,0 @@
|
|
|
1
|
-
const { Config_ID } = require('../../config/airtableConfig');
|
|
2
|
-
|
|
3
|
-
const { getRecordByFilter } = require('../../services/airtableService');
|
|
4
|
-
|
|
5
|
-
const MapCache = require('../../utils/MapCache');
|
|
6
|
-
const { safeParse, isPlainObject } = require('../../utils/jsonUtils');
|
|
7
|
-
|
|
8
|
-
const SUBAGENTS_CONFIG_TABLE = 'sub_agents';
|
|
9
|
-
const CACHE_TTL = 5 * 60 * 1000;
|
|
10
|
-
const CACHE_KEY = 'subAgentsConfig';
|
|
11
|
-
const EXTRACTOR_DEFAULTS = { presetId: '' };
|
|
12
|
-
const COMPOSER_DEFAULTS = { presetId: '' };
|
|
13
|
-
|
|
14
|
-
const cache = new MapCache({ maxSize: 1, ttl: CACHE_TTL });
|
|
15
|
-
|
|
16
|
-
async function load() {
|
|
17
|
-
const cached = cache.get(CACHE_KEY);
|
|
18
|
-
if (cached) return cached;
|
|
19
|
-
|
|
20
|
-
const records = await getRecordByFilter(Config_ID, SUBAGENTS_CONFIG_TABLE, 'TRUE()');
|
|
21
|
-
if (!records) throw new Error('subAgents config could not be read from the Config base');
|
|
22
|
-
const byKey = Object.fromEntries(records.map((r) => [r.config, r.value]));
|
|
23
|
-
|
|
24
|
-
const parsedExtractor = safeParse(byKey.EXTRACTOR, null);
|
|
25
|
-
if (byKey.EXTRACTOR && !isPlainObject(parsedExtractor)) throw new Error('subAgents config EXTRACTOR is not a JSON object');
|
|
26
|
-
const extractor = { ...EXTRACTOR_DEFAULTS, ...(parsedExtractor || {}) };
|
|
27
|
-
|
|
28
|
-
const parsedComposer = safeParse(byKey.COMPOSER, null);
|
|
29
|
-
if (byKey.COMPOSER && !isPlainObject(parsedComposer)) throw new Error('subAgents config COMPOSER is not a JSON object');
|
|
30
|
-
const composer = { ...COMPOSER_DEFAULTS, ...(parsedComposer || {}) };
|
|
31
|
-
|
|
32
|
-
const config = { EXTRACTOR: extractor, COMPOSER: composer };
|
|
33
|
-
cache.set(CACHE_KEY, config);
|
|
34
|
-
return config;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function get(key) {
|
|
38
|
-
return (await load())[key];
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
module.exports = {
|
|
42
|
-
getExtractor: () => get('EXTRACTOR'),
|
|
43
|
-
getComposer: () => get('COMPOSER'),
|
|
44
|
-
};
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
const runtimeConfig = require('../../config/runtimeConfig');
|
|
2
|
-
|
|
3
|
-
function isBridgeEnabled() {
|
|
4
|
-
return runtimeConfig.get('BRIDGE_ENABLED', 'false') === 'true';
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
function getBridgePresetId() {
|
|
8
|
-
return runtimeConfig.get('BRIDGE_PRESET_ID', 'clinical_agent');
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
module.exports = {
|
|
12
|
-
isBridgeEnabled,
|
|
13
|
-
getBridgePresetId,
|
|
14
|
-
};
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
const runtimeConfig = require('../../config/runtimeConfig');
|
|
2
|
-
|
|
3
|
-
function isComposerEnabled() {
|
|
4
|
-
return runtimeConfig.get('COMPOSER_ENABLED', 'false') === 'true';
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
function isAgentSelfRenderEnabled() {
|
|
8
|
-
return runtimeConfig.get('CLINICAL_AGENT_SELF_RENDER', 'false') === 'true';
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
module.exports = {
|
|
12
|
-
isComposerEnabled,
|
|
13
|
-
isAgentSelfRenderEnabled,
|
|
14
|
-
};
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
const CATEGORY_TO_KIND = {
|
|
2
|
-
symptom: 'observation',
|
|
3
|
-
lab_value: 'observation',
|
|
4
|
-
emotion: 'observation',
|
|
5
|
-
medication: 'medication',
|
|
6
|
-
adherence: 'medication',
|
|
7
|
-
diagnosis: 'condition',
|
|
8
|
-
indication: 'condition',
|
|
9
|
-
treatment: 'procedure',
|
|
10
|
-
appointment: 'appointment',
|
|
11
|
-
allergy: 'allergy',
|
|
12
|
-
};
|
|
13
|
-
const LOW_CONFIDENCE = 0.7;
|
|
14
|
-
|
|
15
|
-
function projectClinicalFacts({ clinicalMentions = [], safetyFlags = [], gradeScales = {} }) {
|
|
16
|
-
const clinicalFacts = clinicalMentions.map((m) => toClinicalFact(m, gradeScales)).filter(Boolean);
|
|
17
|
-
|
|
18
|
-
const symptoms = clinicalMentions.filter((m) => m?.fact?.category === 'symptom').length;
|
|
19
|
-
const agentHint = !clinicalMentions.length
|
|
20
|
-
? 'Sin entidades clínicas extraíbles en este mensaje.'
|
|
21
|
-
: symptoms ? `${symptoms} síntoma(s) nuevo(s)` : `${clinicalMentions.length} mención(es) clínica(s)`;
|
|
22
|
-
|
|
23
|
-
return {
|
|
24
|
-
success: true,
|
|
25
|
-
data: {
|
|
26
|
-
agent_hint: agentHint,
|
|
27
|
-
safety_flags_received: [...safetyFlags],
|
|
28
|
-
degraded: false,
|
|
29
|
-
degraded_reasons: [],
|
|
30
|
-
clinical_facts: clinicalFacts,
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function toClinicalFact(mention, gradeScales = {}) {
|
|
36
|
-
const fact = mention?.fact || {};
|
|
37
|
-
const prov = mention?.provenance || {};
|
|
38
|
-
if (!fact.mentionId || !fact.category || !fact.canonicalTerm) return null;
|
|
39
|
-
|
|
40
|
-
const kind = CATEGORY_TO_KIND[fact.category] || 'observation';
|
|
41
|
-
const out = { id: fact.mentionId, code: fact.canonicalTerm, kind };
|
|
42
|
-
if (fact.reportedAt) out.effective = fact.reportedAt;
|
|
43
|
-
|
|
44
|
-
const notes = Array.isArray(prov.verbatimQuotes) ? prov.verbatimQuotes.filter(Boolean) : [];
|
|
45
|
-
if (notes.length) out.notes = notes;
|
|
46
|
-
|
|
47
|
-
const assertion = fact.assertion;
|
|
48
|
-
if (assertion && typeof assertion === 'object') {
|
|
49
|
-
const interp = {
|
|
50
|
-
negated: Boolean(assertion.negated),
|
|
51
|
-
hedged: Boolean(assertion.hedged),
|
|
52
|
-
experiencer: assertion.experiencer || 'patient',
|
|
53
|
-
temporal: assertion.temporal || 'current',
|
|
54
|
-
};
|
|
55
|
-
const isDefault = !interp.negated && !interp.hedged
|
|
56
|
-
&& interp.experiencer === 'patient' && interp.temporal === 'current';
|
|
57
|
-
if (!isDefault) out.interpretation = interp;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const confidence = prov.extractorConfidence;
|
|
61
|
-
if (typeof confidence === 'number' && confidence < LOW_CONFIDENCE) out.confidence = confidence;
|
|
62
|
-
|
|
63
|
-
if (kind === 'observation') {
|
|
64
|
-
if (fact.ctcaeTerm) out.code_ctcae = fact.ctcaeTerm;
|
|
65
|
-
const observed = Object.fromEntries(
|
|
66
|
-
Object.entries(fact.observedFields || {}).filter(([, v]) => v !== null && v !== undefined),
|
|
67
|
-
);
|
|
68
|
-
if (Object.keys(observed).length) out.value = { observed };
|
|
69
|
-
const gradeScale = fact.ctcaeTerm ? gradeScales[fact.ctcaeTerm] || null : null;
|
|
70
|
-
if (gradeScale) out.grade_scale = gradeScale;
|
|
71
|
-
} else if (kind === 'medication') {
|
|
72
|
-
out.adherence_status = fact.category === 'adherence' ? 'non_adherent' : 'unknown';
|
|
73
|
-
} else if (kind === 'condition') {
|
|
74
|
-
out.clinical_status = fact.category === 'diagnosis' ? 'active' : 'unknown';
|
|
75
|
-
}
|
|
76
|
-
return out;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function renderClinicalIntakeSummary({ clinicalMentions = [], safetyFlags = [] } = {}) {
|
|
80
|
-
const lines = clinicalMentions.map((mention) => {
|
|
81
|
-
const fact = mention?.fact || {};
|
|
82
|
-
const prov = mention?.provenance || {};
|
|
83
|
-
if (!fact.canonicalTerm) return null;
|
|
84
|
-
const parts = [`[${fact.category || 'mención'}] ${fact.canonicalTerm}`];
|
|
85
|
-
if (fact.ctcaeTerm && fact.ctcaeTerm !== fact.canonicalTerm) parts.push(`CTCAE: ${fact.ctcaeTerm}`);
|
|
86
|
-
const observed = Object.entries(fact.observedFields || {})
|
|
87
|
-
.filter(([, value]) => value !== null && value !== undefined)
|
|
88
|
-
.map(([key, value]) => `${key}=${value}`);
|
|
89
|
-
if (observed.length) parts.push(observed.join(', '));
|
|
90
|
-
const assertion = fact.assertion || {};
|
|
91
|
-
if (assertion.negated) parts.push('negado');
|
|
92
|
-
if (assertion.hedged) parts.push('con duda');
|
|
93
|
-
if (assertion.temporal && assertion.temporal !== 'current') parts.push(assertion.temporal);
|
|
94
|
-
if (assertion.experiencer && assertion.experiencer !== 'patient') parts.push(`de: ${assertion.experiencer}`);
|
|
95
|
-
const quote = (Array.isArray(prov.verbatimQuotes) ? prov.verbatimQuotes : []).find(Boolean);
|
|
96
|
-
if (quote) parts.push(`«${quote}»`);
|
|
97
|
-
return `- ${parts.join(' — ')}`;
|
|
98
|
-
}).filter(Boolean);
|
|
99
|
-
if (!lines.length) return '';
|
|
100
|
-
const flags = safetyFlags.map((flag) => (typeof flag === 'string' ? flag : flag?.type)).filter(Boolean);
|
|
101
|
-
const flagLine = flags.length ? `\nAlertas de seguridad: ${flags.join(', ')}` : '';
|
|
102
|
-
return `Extracción clínica (clinical-agent):\n${lines.join('\n')}${flagLine}`;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
module.exports = {
|
|
106
|
-
projectClinicalFacts,
|
|
107
|
-
renderClinicalIntakeSummary,
|
|
108
|
-
};
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
const { ClinicalMention } = require('../../shared/dtos/ClinicalMention');
|
|
2
|
-
|
|
3
|
-
function buildExtractorInput(text, detectedTags, curated, template) {
|
|
4
|
-
const payload = JSON.stringify({ raw_message: text, detected_tags: detectedTags }, null, 2);
|
|
5
|
-
const tpl = curated.length ? template.symptom : template.default;
|
|
6
|
-
return tpl
|
|
7
|
-
.replace('{{curated}}', () => JSON.stringify(curated, null, 2))
|
|
8
|
-
.replace('{{payload}}', () => payload);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function readOutputText(result) {
|
|
12
|
-
if (result?.output_text) return result.output_text;
|
|
13
|
-
const output = Array.isArray(result?.output) ? result.output : [];
|
|
14
|
-
const message = output.find((item) => item.type === 'message');
|
|
15
|
-
const content = Array.isArray(message?.content) ? message.content : [];
|
|
16
|
-
const part = content.find((c) => typeof c?.text === 'string');
|
|
17
|
-
return part?.text || '';
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function toClinicalMention(raw, { code, turnId, index, now, intakeSource, responseFormat, spanishByTerm = {} }) {
|
|
21
|
-
const rawTerm = (raw.ctcae_term || '').trim().toLowerCase();
|
|
22
|
-
const ctcaeTerm = !rawTerm || rawTerm === 'other' ? null : rawTerm;
|
|
23
|
-
const canonicalTerm = deriveCanonicalTerm(raw, ctcaeTerm, spanishByTerm);
|
|
24
|
-
if (!canonicalTerm) return null;
|
|
25
|
-
|
|
26
|
-
const mentionId = `men_${turnId}_${index}`;
|
|
27
|
-
const verbatim = raw.verbatim_quote ? [raw.verbatim_quote] : (raw.verbatim_quotes || []);
|
|
28
|
-
const observed = raw.observed_fields || {};
|
|
29
|
-
return new ClinicalMention({
|
|
30
|
-
fact: {
|
|
31
|
-
mentionId,
|
|
32
|
-
patientId: code,
|
|
33
|
-
category: raw.category,
|
|
34
|
-
canonicalTerm,
|
|
35
|
-
reportedAt: now,
|
|
36
|
-
assertion: raw.assertion || {},
|
|
37
|
-
ctcaeTerm,
|
|
38
|
-
temporality: raw.temporality || null,
|
|
39
|
-
observedFields: responseFormat === 'router'
|
|
40
|
-
? Object.fromEntries(Object.entries(observed).filter(([, v]) => v != null))
|
|
41
|
-
: observed,
|
|
42
|
-
},
|
|
43
|
-
provenance: {
|
|
44
|
-
mentionId,
|
|
45
|
-
patientId: code,
|
|
46
|
-
turnId,
|
|
47
|
-
recordedAt: now,
|
|
48
|
-
source: intakeSource || 'self',
|
|
49
|
-
verbatimQuotes: verbatim,
|
|
50
|
-
extractorConfidence: raw.extractor_confidence ?? raw.confidence ?? raw.assertion?.confidence ?? 1,
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function deriveCanonicalTerm(raw, ctcaeTerm, spanishByTerm) {
|
|
56
|
-
const emitted = (raw.canonical_term || '').trim().toLowerCase();
|
|
57
|
-
if (emitted) return emitted;
|
|
58
|
-
if (raw.category === 'symptom' && ctcaeTerm) {
|
|
59
|
-
return ((spanishByTerm[ctcaeTerm] || ctcaeTerm) || '').toLowerCase() || null;
|
|
60
|
-
}
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
module.exports = {
|
|
65
|
-
buildExtractorInput,
|
|
66
|
-
readOutputText,
|
|
67
|
-
toClinicalMention,
|
|
68
|
-
};
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
const { isInCohort } = require('../config/cohortConfig');
|
|
2
|
-
const { Thread } = require('../../models/threadModel');
|
|
3
|
-
|
|
4
|
-
const runtimeConfig = require('../../config/runtimeConfig');
|
|
5
|
-
const { logger } = require('../../utils/logger');
|
|
6
|
-
|
|
7
|
-
async function overlayCohortPreset(thread) {
|
|
8
|
-
const enrolled = await isInCohort(thread.code, 'preset');
|
|
9
|
-
if (!enrolled) return thread;
|
|
10
|
-
try {
|
|
11
|
-
const base = thread.toObject();
|
|
12
|
-
delete base._id;
|
|
13
|
-
const cohortThread = new Thread({ ...base, preset_id: runtimeConfig.get('COHORT_PRESET_ID', 'clinical_agent'), preset_version: null });
|
|
14
|
-
cohortThread.isInCohort = true;
|
|
15
|
-
return cohortThread;
|
|
16
|
-
} catch (error) {
|
|
17
|
-
logger.error('[cohortHelper] overlay failed; patient stays on the control path', { code: thread.code, error: error.message });
|
|
18
|
-
return thread;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
module.exports = { overlayCohortPreset };
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
const GRADE_MIN = 1;
|
|
2
|
-
const GRADE_MAX = 5;
|
|
3
|
-
const inGradeRange = (value) => Number.isInteger(value) && value >= GRADE_MIN && value <= GRADE_MAX;
|
|
4
|
-
|
|
5
|
-
function parseGradeEstimate(raw) {
|
|
6
|
-
const grade = { bestEstimate: null, confidence: null, possibleRange: null, reasoning: '' };
|
|
7
|
-
if (!raw || typeof raw !== 'object') return grade;
|
|
8
|
-
if (raw.best_estimate !== null && raw.best_estimate !== undefined) {
|
|
9
|
-
if (!inGradeRange(raw.best_estimate)) return { error: 'grade_out_of_range' };
|
|
10
|
-
grade.bestEstimate = raw.best_estimate;
|
|
11
|
-
}
|
|
12
|
-
if (['low', 'moderate', 'high'].includes(raw.confidence)) grade.confidence = raw.confidence;
|
|
13
|
-
if (raw.possible_range !== null && raw.possible_range !== undefined) {
|
|
14
|
-
const range = raw.possible_range;
|
|
15
|
-
if (!Array.isArray(range) || range.length !== 2 || !inGradeRange(range[0]) || !inGradeRange(range[1]) || range[0] > range[1]) {
|
|
16
|
-
return { error: 'grade_out_of_range' };
|
|
17
|
-
}
|
|
18
|
-
grade.possibleRange = [range[0], range[1]];
|
|
19
|
-
}
|
|
20
|
-
if (typeof raw.reasoning === 'string') grade.reasoning = raw.reasoning;
|
|
21
|
-
return grade;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
module.exports = {
|
|
25
|
-
GRADE_MIN,
|
|
26
|
-
GRADE_MAX,
|
|
27
|
-
inGradeRange,
|
|
28
|
-
parseGradeEstimate,
|
|
29
|
-
};
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
const DEGRADED_NUMERIC_REASON = 'observations_should_have_been_passed_text_contains_numbers — patient_verbatim contains a numeric+unit pattern (e.g. \'8.5 g/dL\'); pass these as `observations: [{code, value, unit, effective}]` so the audit lane emits FHIR Observation + Provenance per fact.';
|
|
2
|
-
|
|
3
|
-
const NUMERIC_VALUE_PATTERN = new RegExp(
|
|
4
|
-
'\\d+(?:\\.\\d+)?\\s*(?:'
|
|
5
|
-
+ 'g/dL|mg/dL|mmol/L|umol/L|mEq/L|ng/mL|pg/mL|ug/mL|ug/dL'
|
|
6
|
-
+ '|10\\^?\\*?[0-9]+/[a-zA-Z]+'
|
|
7
|
-
+ '|K/uL|/uL|/mm3'
|
|
8
|
-
+ '|U/L|IU/L'
|
|
9
|
-
+ '|mmHg|cmH2O'
|
|
10
|
-
+ '|bpm|/min|rpm'
|
|
11
|
-
+ '|°C|°F|Cel'
|
|
12
|
-
+ '|(?:kg|mg|ug|mL|dL|cm|mm)\\b'
|
|
13
|
-
+ '|%'
|
|
14
|
-
+ ')',
|
|
15
|
-
'i',
|
|
16
|
-
);
|
|
17
|
-
|
|
18
|
-
function patientVerbatimHasNumericValues(text) {
|
|
19
|
-
if (!text || typeof text !== 'string') return false;
|
|
20
|
-
return NUMERIC_VALUE_PATTERN.test(text);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function slugify(value, maxLen = 32) {
|
|
24
|
-
if (!value) return '';
|
|
25
|
-
const stripped = value.normalize('NFKD').replace(/[̀-ͯ]/g, '');
|
|
26
|
-
const hyphenated = stripped.replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-+|-+$/g, '').toLowerCase();
|
|
27
|
-
return hyphenated.slice(0, maxLen).replace(/^-+|-+$/g, '');
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
module.exports = {
|
|
31
|
-
DEGRADED_NUMERIC_REASON,
|
|
32
|
-
patientVerbatimHasNumericValues,
|
|
33
|
-
slugify,
|
|
34
|
-
};
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
const { logger } = require('../../utils/logger');
|
|
2
|
-
const MapCache = require('../../utils/MapCache');
|
|
3
|
-
const { isBridgeEnabled } = require('../flags/bridgeConfig');
|
|
4
|
-
|
|
5
|
-
const MAX_WORDS = 25;
|
|
6
|
-
const BRIDGE_RATE_LIMIT_MS = 60000;
|
|
7
|
-
const CONTEXT_MARKER_PREFIX_RX = /^\s*(\[\s*(?:puente|sistema)\b[^\]]*\]\s*)+/i;
|
|
8
|
-
|
|
9
|
-
const bridgeTimestamps = new MapCache({ maxSize: 1000, ttl: BRIDGE_RATE_LIMIT_MS });
|
|
10
|
-
|
|
11
|
-
function sanitizeBridgeText(text) {
|
|
12
|
-
const trimmed = (text || '').replace(CONTEXT_MARKER_PREFIX_RX, '').trim();
|
|
13
|
-
if (trimmed.length < 2) return null;
|
|
14
|
-
if (trimmed.split(/\s+/).length > MAX_WORDS) return null;
|
|
15
|
-
return trimmed;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function buildDedupNote(bridgeText) {
|
|
19
|
-
return '[sistema] Ya enviaste la siguiente burbuja al paciente como mensaje puente: ' +
|
|
20
|
-
`"${bridgeText}". Tu mensaje final NO debe repetir saludo, nombre del paciente, ` +
|
|
21
|
-
'ni acuse del inbound — eso ya fue entregado. Empieza directo con el contenido ' +
|
|
22
|
-
'clínico. El mensaje final es la SEGUNDA burbuja que verá el paciente, no la primera.';
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function maybeSendBridge({ narrationText, hasToolCalls, code, turnId = null, traceId = null, shouldContinue, sendMessage }) {
|
|
26
|
-
if (!isBridgeEnabled()) return { sent: false };
|
|
27
|
-
|
|
28
|
-
const skip = (reason) => {
|
|
29
|
-
logger.info('[bridge] skipped', { code, reason });
|
|
30
|
-
return { sent: false };
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
if (typeof shouldContinue === 'function' && !shouldContinue()) return skip('superseded');
|
|
34
|
-
if (!hasToolCalls) return skip('text_only_iter1_would_duplicate_final');
|
|
35
|
-
|
|
36
|
-
const text = sanitizeBridgeText(narrationText);
|
|
37
|
-
if (!text) return skip('no_usable_narration');
|
|
38
|
-
|
|
39
|
-
if (bridgeTimestamps.get(code) !== undefined) return skip('recent_bridge_in_thread');
|
|
40
|
-
|
|
41
|
-
bridgeTimestamps.set(code, true);
|
|
42
|
-
try {
|
|
43
|
-
Promise.resolve(sendMessage({ code, body: text, processed: true, origin: 'assistant', raw: { bridge: true }, turnId, traceId }))
|
|
44
|
-
.catch(err => logger.warn('[bridge] send failed', { code, error: err.message }));
|
|
45
|
-
} catch (err) {
|
|
46
|
-
logger.warn('[bridge] send failed', { code, error: err.message });
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
logger.info('[bridge] sent', { code });
|
|
50
|
-
return { sent: true, text, dedupNote: buildDedupNote(text) };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
module.exports = {
|
|
54
|
-
maybeSendBridge,
|
|
55
|
-
};
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
const { Monitoreo_ID, Estado_General_ID } = require('../../config/airtableConfig');
|
|
2
|
-
const airtableService = require('../../services/airtableService');
|
|
3
|
-
const { logger } = require('../../utils/logger');
|
|
4
|
-
|
|
5
|
-
const MEDICAL_URGENCY_TO_EMERGENCY_LEVEL = {
|
|
6
|
-
stat: 'Alta',
|
|
7
|
-
asap: 'Alta',
|
|
8
|
-
urgent: 'Media',
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
// The Airtable reminder.type singleSelect predates the clinical-agent enum.
|
|
12
|
-
const REMINDER_TYPE_TO_AIRTABLE = {
|
|
13
|
-
examen: 'exámen',
|
|
14
|
-
check_in: 'otros',
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
// intake.source only offers self|doctor; team_relay carries "según médico" authority.
|
|
18
|
-
const INTAKE_SOURCE_TO_AIRTABLE = {
|
|
19
|
-
self: 'self',
|
|
20
|
-
caregiver: 'self',
|
|
21
|
-
team_relay: 'doctor',
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
async function auditedWrite({ trace, code, table, write }) {
|
|
25
|
-
const entry = { table, at: new Date().toISOString(), ok: false };
|
|
26
|
-
try {
|
|
27
|
-
const result = await write();
|
|
28
|
-
entry.ok = true;
|
|
29
|
-
const record = Array.isArray(result) ? result[0] : result;
|
|
30
|
-
if (record?.id) entry.recordId = record.id;
|
|
31
|
-
} catch (err) {
|
|
32
|
-
entry.error = err?.message || String(err);
|
|
33
|
-
logger.error('[clinicalAirtableService] Airtable write failed', { table, code, error: entry.error });
|
|
34
|
-
}
|
|
35
|
-
if (trace?.setSignals) {
|
|
36
|
-
const existing = Array.isArray(trace.signals?.airtableWrites) ? trace.signals.airtableWrites : [];
|
|
37
|
-
trace.setSignals({ airtableWrites: [...existing, entry] });
|
|
38
|
-
}
|
|
39
|
-
return entry.ok;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async function findMonitoreoPatientRecordId(code) {
|
|
43
|
-
const patient = await airtableService.getRecordByFilter(Monitoreo_ID, 'estado_general', `{whatsapp_id} = "${code}"`);
|
|
44
|
-
return patient?.[0]?.recordID || null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function findIntakePatientRecordId(code) {
|
|
48
|
-
const patient = await airtableService.getRecordByFilter(Estado_General_ID, 'estado_general', `{whatsapp_id} = "${code}"`);
|
|
49
|
-
return patient?.[0]?.monitoreo_record_id || null;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async function recordUnresolvedRequest({ code, trace, question, details, resolutionAttempted = false, category = null, urgency = null }) {
|
|
53
|
-
return auditedWrite({
|
|
54
|
-
trace,
|
|
55
|
-
code,
|
|
56
|
-
table: 'unresolved_questions',
|
|
57
|
-
write: async () => {
|
|
58
|
-
const patientId = await findMonitoreoPatientRecordId(code);
|
|
59
|
-
const fields = {
|
|
60
|
-
customer_id: patientId ? [patientId] : [],
|
|
61
|
-
question: question || '',
|
|
62
|
-
details: details || '',
|
|
63
|
-
resolution_attempted: Boolean(resolutionAttempted),
|
|
64
|
-
message_tag: 'request',
|
|
65
|
-
};
|
|
66
|
-
if (category) fields.category = category;
|
|
67
|
-
if (urgency) fields.urgency = urgency;
|
|
68
|
-
return airtableService.addRecord(Monitoreo_ID, 'unresolved_questions', [{ fields }], code);
|
|
69
|
-
},
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function recordEmergency({ code, trace, symptom, suspectedCondition = '', recommendedAction = '', urgencyFhir = null, urgencyLevel = null, documentation = '' }) {
|
|
74
|
-
const level = urgencyLevel || MEDICAL_URGENCY_TO_EMERGENCY_LEVEL[urgencyFhir] || 'Alta';
|
|
75
|
-
return auditedWrite({
|
|
76
|
-
trace,
|
|
77
|
-
code,
|
|
78
|
-
table: 'emergencies',
|
|
79
|
-
write: async () => {
|
|
80
|
-
const patientId = await findMonitoreoPatientRecordId(code);
|
|
81
|
-
const fields = {
|
|
82
|
-
patient: patientId ? [patientId] : [],
|
|
83
|
-
symptom: symptom || '',
|
|
84
|
-
suspectedCondition: suspectedCondition || '',
|
|
85
|
-
urgencyLevel: level,
|
|
86
|
-
recommendedAction: recommendedAction || '',
|
|
87
|
-
};
|
|
88
|
-
if (urgencyFhir) fields.urgency_fhir = urgencyFhir;
|
|
89
|
-
if (documentation) fields['Documentación_emergencia'] = documentation;
|
|
90
|
-
return airtableService.addRecord(Monitoreo_ID, 'emergencies', [{ fields }], code);
|
|
91
|
-
},
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async function recordClinicalIntake({ code, trace, messageRaw, source = 'self' }) {
|
|
96
|
-
return auditedWrite({
|
|
97
|
-
trace,
|
|
98
|
-
code,
|
|
99
|
-
table: 'intake',
|
|
100
|
-
write: async () => {
|
|
101
|
-
const patientId = await findIntakePatientRecordId(code);
|
|
102
|
-
return airtableService.addRecord(Estado_General_ID, 'intake', [{
|
|
103
|
-
fields: {
|
|
104
|
-
patient: patientId ? [patientId] : [],
|
|
105
|
-
message_raw: messageRaw || '',
|
|
106
|
-
source: INTAKE_SOURCE_TO_AIRTABLE[source] || 'self',
|
|
107
|
-
},
|
|
108
|
-
}], code);
|
|
109
|
-
},
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async function recordReminder({ code, trace, actionType, reminderId, reminderDateUtc, reminderType, description }) {
|
|
114
|
-
const type = REMINDER_TYPE_TO_AIRTABLE[reminderType] || reminderType;
|
|
115
|
-
if (actionType === 'update') {
|
|
116
|
-
return auditedWrite({
|
|
117
|
-
trace,
|
|
118
|
-
code,
|
|
119
|
-
table: 'reminder',
|
|
120
|
-
write: async () => {
|
|
121
|
-
const updated = await airtableService.updateRecordByFilter(
|
|
122
|
-
Monitoreo_ID,
|
|
123
|
-
'reminder',
|
|
124
|
-
`AND({schedule_id} = "${reminderId}", IF({is_active}, "true", "false") = "true", IF({is_sent}, "true", "false") = "false")`,
|
|
125
|
-
{ reminder_date: reminderDateUtc, type, description },
|
|
126
|
-
code,
|
|
127
|
-
);
|
|
128
|
-
if (!updated?.length) throw new Error(`no active unsent reminder found for reminder_id ${reminderId}`);
|
|
129
|
-
return updated;
|
|
130
|
-
},
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
return auditedWrite({
|
|
134
|
-
trace,
|
|
135
|
-
code,
|
|
136
|
-
table: 'reminder',
|
|
137
|
-
write: () => airtableService.addLinkedRecord(
|
|
138
|
-
Monitoreo_ID,
|
|
139
|
-
'reminder',
|
|
140
|
-
{
|
|
141
|
-
schedule_id: reminderId || '',
|
|
142
|
-
whatsapp_id: code,
|
|
143
|
-
reminder_date: reminderDateUtc,
|
|
144
|
-
type,
|
|
145
|
-
description,
|
|
146
|
-
},
|
|
147
|
-
{
|
|
148
|
-
referenceTable: 'estado_general',
|
|
149
|
-
referenceFilter: `{whatsapp_id} = "${code}"`,
|
|
150
|
-
linkFieldName: 'patient_id',
|
|
151
|
-
},
|
|
152
|
-
code,
|
|
153
|
-
),
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
module.exports = {
|
|
158
|
-
recordUnresolvedRequest,
|
|
159
|
-
recordEmergency,
|
|
160
|
-
recordClinicalIntake,
|
|
161
|
-
recordReminder,
|
|
162
|
-
};
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
const { getSafetyFlagPatterns } = require('../config/divergenceConfig');
|
|
2
|
-
const { getExtractor } = require('../config/subAgentsConfig');
|
|
3
|
-
const { getCtcaeCatalog } = require('../stores/CtcaeCatalog');
|
|
4
|
-
const { storeClinicalMentions } = require('../../fhir');
|
|
5
|
-
|
|
6
|
-
const { Config_ID } = require('../../config/airtableConfig');
|
|
7
|
-
const { getRecordByFilter } = require('../../services/airtableService');
|
|
8
|
-
const MapCache = require('../../utils/MapCache');
|
|
9
|
-
const { safeParse, safeParseArray } = require('../../utils/jsonUtils');
|
|
10
|
-
const { logger } = require('../../utils/logger');
|
|
11
|
-
const { detectSafetyFlags } = require('../helpers/safetyFlagsHelper');
|
|
12
|
-
const { buildExtractorInput, readOutputText, toClinicalMention } = require('../helpers/clinicalMentionHelper');
|
|
13
|
-
|
|
14
|
-
const CONFIG_TABLE = 'registry';
|
|
15
|
-
const CACHE_TTL = 5 * 60 * 1000;
|
|
16
|
-
const CONFIG_CACHE_KEY = 'clinicalExtractorConfig';
|
|
17
|
-
|
|
18
|
-
const extractorConfigCache = new MapCache({ maxSize: 1, ttl: CACHE_TTL });
|
|
19
|
-
|
|
20
|
-
async function extractClinicalMentions({ text, turnId, code, provider, detectedTags = [], intakeSource = 'self', responseFormat = 'router' }) {
|
|
21
|
-
const [patterns, { presetId }] = await Promise.all([
|
|
22
|
-
getSafetyFlagPatterns(),
|
|
23
|
-
getExtractor(),
|
|
24
|
-
]);
|
|
25
|
-
|
|
26
|
-
const flags = new Set(detectSafetyFlags(text || '', patterns));
|
|
27
|
-
|
|
28
|
-
if (!text || !text.trim() || !presetId || !provider?.runStructured) {
|
|
29
|
-
return { clinicalMentions: [], safetyFlags: [...flags] };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
try {
|
|
33
|
-
const template = await getExtractorInputTemplate();
|
|
34
|
-
const curated = detectedTags.includes('symptom') ? getCtcaeCatalog().getCuratedSymptomsByTerm() : new Map();
|
|
35
|
-
const curatedRows = [...curated.values()].map(toExtractorRow);
|
|
36
|
-
const spanishByTerm = Object.fromEntries(curatedRows.filter((row) => row.ctcae_term).map((row) => [row.ctcae_term, row.spanish_label]));
|
|
37
|
-
|
|
38
|
-
const result = await provider.runStructured({
|
|
39
|
-
presetId,
|
|
40
|
-
input: [{ role: 'user', content: buildExtractorInput(text, detectedTags, curatedRows, template) }],
|
|
41
|
-
metadata: { extractor: 'clinical-mention', turnId },
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
if (result?.status && result.status !== 'completed') throw new Error(`clinical extraction incomplete: status=${result.status}`);
|
|
45
|
-
|
|
46
|
-
const outputText = readOutputText(result);
|
|
47
|
-
const parsed = safeParse(outputText, null);
|
|
48
|
-
if (outputText && outputText.trim() && parsed === null) {
|
|
49
|
-
logger.warn('[clinicalExtraction] model output did not parse as JSON; no mentions extracted', { turnId });
|
|
50
|
-
}
|
|
51
|
-
const rawMentions = Array.isArray(parsed?.mentions) ? parsed.mentions : [];
|
|
52
|
-
|
|
53
|
-
const now = new Date().toISOString();
|
|
54
|
-
const clinicalMentions = [];
|
|
55
|
-
rawMentions.forEach((raw, index) => {
|
|
56
|
-
if (!raw || typeof raw !== 'object') return;
|
|
57
|
-
const mention = toClinicalMention(raw, { code, turnId, index, now, intakeSource, responseFormat, spanishByTerm });
|
|
58
|
-
if (mention) clinicalMentions.push(mention);
|
|
59
|
-
for (const f of raw.safety_flags || []) flags.add(f);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
const patientMentions = clinicalMentions.filter((mention) => mention.fact.assertion.experiencer === 'patient');
|
|
63
|
-
if (patientMentions.length) {
|
|
64
|
-
await storeClinicalMentions({ patientId: code, mentions: patientMentions });
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return { clinicalMentions, safetyFlags: [...flags] };
|
|
68
|
-
} catch (err) {
|
|
69
|
-
logger.error('[clinicalExtraction] extraction failed; returning deterministic safety flags only', { turnId, error: err?.message });
|
|
70
|
-
return { clinicalMentions: [], safetyFlags: [...flags] };
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function toExtractorRow(symptom) {
|
|
75
|
-
return {
|
|
76
|
-
ctcae_term: symptom.ctcaeTerm,
|
|
77
|
-
spanish_label: symptom.spanishLabel,
|
|
78
|
-
patient_phrases_validated: symptom.patientPhrasesValidated,
|
|
79
|
-
observed_fields_schema: symptom.observedFieldsSchema,
|
|
80
|
-
critical_field: symptom.criticalField,
|
|
81
|
-
grades: symptom.grades,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async function loadExtractorConfig() {
|
|
86
|
-
const cached = extractorConfigCache.get(CONFIG_CACHE_KEY);
|
|
87
|
-
if (cached) return cached;
|
|
88
|
-
|
|
89
|
-
const records = await getRecordByFilter(Config_ID, CONFIG_TABLE, 'TRUE()');
|
|
90
|
-
if (!records) throw new Error('extractor config could not be read from the Config base');
|
|
91
|
-
const config = Object.fromEntries(records.map((r) => [r.config, r.value]));
|
|
92
|
-
extractorConfigCache.set(CONFIG_CACHE_KEY, config);
|
|
93
|
-
return config;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async function getExtractorInputTemplate() {
|
|
97
|
-
const config = await loadExtractorConfig();
|
|
98
|
-
return {
|
|
99
|
-
default: config.EXTRACTOR_INPUT_TEMPLATE_DEFAULT || '',
|
|
100
|
-
symptom: config.EXTRACTOR_INPUT_TEMPLATE_SYMPTOM || '',
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async function getConversationalTokens() {
|
|
105
|
-
const config = await loadExtractorConfig();
|
|
106
|
-
return safeParseArray(config.CONVERSATIONAL_TOKENS) || [];
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
module.exports = {
|
|
110
|
-
extractClinicalMentions,
|
|
111
|
-
getConversationalTokens,
|
|
112
|
-
};
|