@peopl-health/nexus 5.68.4 → 5.68.6
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.
|
@@ -19,6 +19,7 @@ const { logger } = require('../../utils/logger');
|
|
|
19
19
|
const CONFIG_TABLE = 'registry';
|
|
20
20
|
const CACHE_TTL = 5 * 60 * 1000;
|
|
21
21
|
const CONFIG_CACHE_KEY = 'clinicalExtractorConfig';
|
|
22
|
+
const ENDING_STATUSES = new Set(['paused', 'completed']);
|
|
22
23
|
|
|
23
24
|
const extractorConfigCache = new MapCache({ maxSize: 1, ttl: CACHE_TTL });
|
|
24
25
|
|
|
@@ -37,6 +38,12 @@ function namesNothingSpecific(canonicalTerm, vagueWords) {
|
|
|
37
38
|
return tokens.length > 0 && tokens.every((token) => vagueWords.has(token));
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
function endsNamedLine(raw, fact) {
|
|
42
|
+
return fact.category === 'treatment'
|
|
43
|
+
&& Boolean(raw.treatment_line)
|
|
44
|
+
&& ENDING_STATUSES.has(fact.observedFields?.status);
|
|
45
|
+
}
|
|
46
|
+
|
|
40
47
|
async function loadPatientContext(code) {
|
|
41
48
|
try {
|
|
42
49
|
const { clinicalContext, contextAsk } = await getClinicalContext(code);
|
|
@@ -115,7 +122,8 @@ async function extractClinicalMentions({ text, turnId, code, provider, detectedT
|
|
|
115
122
|
unanchored += 1;
|
|
116
123
|
return;
|
|
117
124
|
}
|
|
118
|
-
if (!mention.provenance.needsConfirmation &&
|
|
125
|
+
if (!mention.provenance.needsConfirmation && !endsNamedLine(raw, mention.fact)
|
|
126
|
+
&& namesNothingSpecific(mention.fact.canonicalTerm, vagueWords)) {
|
|
119
127
|
forcedPending += 1;
|
|
120
128
|
mention = toClinicalMention(
|
|
121
129
|
{ ...raw, needs_confirmation: true, missing: raw.missing?.length ? raw.missing : ['name'] },
|
|
@@ -40,7 +40,7 @@ const MAX_CONTEXT_TURNS = 6;
|
|
|
40
40
|
|
|
41
41
|
const definition = {
|
|
42
42
|
name: 'extractClinicalInfo',
|
|
43
|
-
description: '**Does:** Extracts ALL structured clinical facts from the current patient message — symptoms, labs, medications, diagnoses, appointments, treatments — and returns them as a discriminated-union `clinical_facts[]` list keyed by `kind`. Call ONCE per turn with ALL detected tags in a single call — one call covers every category.\n\n**Required inputs:** `raw_message` (the patient\'s current turn text, VERBATIM — the full contents of `<patient_message>`, every line of it when the turn carries more than one), `detected_tags` (list, at least one of: `symptom`, `indication`, `treatment`, `adherence`, `medication`, `appointment`, `emotion`, `lab_value`, `diagnosis`, `allergy`, `comorbidity`, `performance_status`; empty only for a purely conversational message or when declaring `non_clinical_request`), `intake_source` (`self` | `caregiver` | `team_relay`).\n\n**When to call:** at the start of any turn where the patient message mentions, explicitly or implicitly, any of the 12 supported categories. The result drives every downstream router-side read (landscape / history / risk / patterns).\n\n**When NOT to call:**\n- When the patient SHARED raw numeric results / lab values / files — use `reportResultsReceived` instead (it both logs the share AND emits FHIR Observations directly).\n- More than once per turn — a single call covers every category.\n\n**Returns:** `data.clinical_facts[]` — a discriminated-union list where each record has `kind` (observation | medication | condition | procedure | appointment | allergy) plus common fields `{id, code, effective, notes}` (null/default fields are OMITTED — e.g. `interpretation` appears only when non-default) and kind-specific fields:\n- `kind=observation`: `code_ctcae`, `value`, `grade_scale`, `episode_id`, `trend`. Read `grade_scale` to anchor the grade assigned in `recordClinicalImpression`. It is present whenever the term has a curated CTCAE ladder, and ABSENT when it does not — an absent `grade_scale` means there is no rubric for this term, not that one was withheld. An absent `grade_scale` means the term has no curated rubric, not that one was withheld — so there is nothing to grade against from memory. Call `lookupCtcaeEntry` when you suspect the term itself is wrong; otherwise say where the grade came from in `grade_estimate.source`.\n- `kind=medication`: `adherence_status` (`started` | `ongoing` | `non_adherent` | …).\n- `kind=condition`: `clinical_status` (`active` | `resolved` | …) — patient self-report of an existing diagnosis (distinct from `openCondition` which commits a new agent-tracked case).\n- `kind=procedure`: patient-reported procedure done elsewhere (distinct from `recordIntervention` which is agent-authored); `venue`/`outcome` may be null in the current extraction phase.\n- `kind=appointment`: `scheduled_for`, `purpose` (may be null in the current extraction phase).\nThe system writes the matching FHIR resource (Observation / MedicationStatement / Condition / Procedure / Appointment) + `Provenance` per fact on the audit lane; `allergy`, `comorbidity`, and `performance_status` are captured in a dedicated aggregate, not as a per-fact resource.\n\n**When the result asks you to confirm:** `data.pending_confirmation[]` means the patient reported a REAL clinical event but named it only generically («me suspendieron el tratamiento»), so it was deliberately NOT recorded. Each entry carries `{term, category, missing, quote}`; the list holds EVERY fact withheld this turn, but ask about the FIRST one only. Ask the patient for exactly what its `missing` names, in ONE short natural question, and never state or imply that anything was recorded. When they answer, call `extractClinicalInfo` again on their reply and it will be recorded then. `data.context_gaps[]` — only ever sent when there is nothing to confirm — names ONE stored variable that is stale or incomplete; raise it only if the conversation allows it naturally. Both lists are empty whenever the turn carries a safety flag: never ask housekeeping questions during a safety event.\n\n**Side effects:** System emits a FHIR resource + `Provenance` per fact on the audit lane, except `allergy`/`comorbidity`/`performance_status` (aggregated). Facts listed in `pending_confirmation` are NOT
|
|
43
|
+
description: '**Does:** Extracts ALL structured clinical facts from the current patient message — symptoms, labs, medications, diagnoses, appointments, treatments — and returns them as a discriminated-union `clinical_facts[]` list keyed by `kind`. Call ONCE per turn with ALL detected tags in a single call — one call covers every category.\n\n**Required inputs:** `raw_message` (the patient\'s current turn text, VERBATIM — the full contents of `<patient_message>`, every line of it when the turn carries more than one), `detected_tags` (list, at least one of: `symptom`, `indication`, `treatment`, `adherence`, `medication`, `appointment`, `emotion`, `lab_value`, `diagnosis`, `allergy`, `comorbidity`, `performance_status`; empty only for a purely conversational message or when declaring `non_clinical_request`), `intake_source` (`self` | `caregiver` | `team_relay`).\n\n**When to call:** at the start of any turn where the patient message mentions, explicitly or implicitly, any of the 12 supported categories. The result drives every downstream router-side read (landscape / history / risk / patterns).\n\n**When NOT to call:**\n- When the patient SHARED raw numeric results / lab values / files — use `reportResultsReceived` instead (it both logs the share AND emits FHIR Observations directly).\n- More than once per turn — a single call covers every category.\n\n**Returns:** `data.clinical_facts[]` — a discriminated-union list where each record has `kind` (observation | medication | condition | procedure | appointment | allergy) plus common fields `{id, code, effective, notes}` (null/default fields are OMITTED — e.g. `interpretation` appears only when non-default) and kind-specific fields:\n- `kind=observation`: `code_ctcae`, `value`, `grade_scale`, `episode_id`, `trend`. Read `grade_scale` to anchor the grade assigned in `recordClinicalImpression`. It is present whenever the term has a curated CTCAE ladder, and ABSENT when it does not — an absent `grade_scale` means there is no rubric for this term, not that one was withheld. An absent `grade_scale` means the term has no curated rubric, not that one was withheld — so there is nothing to grade against from memory. Call `lookupCtcaeEntry` when you suspect the term itself is wrong; otherwise say where the grade came from in `grade_estimate.source`.\n- `kind=medication`: `adherence_status` (`started` | `ongoing` | `non_adherent` | …).\n- `kind=condition`: `clinical_status` (`active` | `resolved` | …) — patient self-report of an existing diagnosis (distinct from `openCondition` which commits a new agent-tracked case).\n- `kind=procedure`: patient-reported procedure done elsewhere (distinct from `recordIntervention` which is agent-authored); `venue`/`outcome` may be null in the current extraction phase.\n- `kind=appointment`: `scheduled_for`, `purpose` (may be null in the current extraction phase).\nThe system writes the matching FHIR resource (Observation / MedicationStatement / Condition / Procedure / Appointment) + `Provenance` per fact on the audit lane; `allergy`, `comorbidity`, and `performance_status` are captured in a dedicated aggregate, not as a per-fact resource.\n\n**When the result asks you to confirm:** `data.pending_confirmation[]` means the patient reported a REAL clinical event but named it only generically («me suspendieron el tratamiento»), so it was deliberately NOT recorded. Each entry carries `{term, category, missing, quote}`; the list holds EVERY fact withheld this turn, but ask about the FIRST one only. Ask the patient for exactly what its `missing` names, in ONE short natural question, and never state or imply that anything was recorded. When they answer, call `extractClinicalInfo` again on their reply and it will be recorded then. `data.context_gaps[]` — only ever sent when there is nothing to confirm — names ONE stored variable that is stale or incomplete; raise it only if the conversation allows it naturally. Both lists are empty whenever the turn carries a safety flag: never ask housekeeping questions during a safety event.\n\n**Side effects:** System emits a FHIR resource + `Provenance` per fact on the audit lane, except `allergy`/`comorbidity`/`performance_status` (aggregated). Facts listed in `pending_confirmation` are NOT recorded as facts — they never reach the clinical intake or the patient\'s chart; FHIR keeps them only as unconfirmed drafts until the patient confirms them.',
|
|
44
44
|
strict: true,
|
|
45
45
|
parameters: {
|
|
46
46
|
type: 'object',
|
|
@@ -5,6 +5,7 @@ const { reference, patientReference, codeableConcept, extension, toInstant } = r
|
|
|
5
5
|
const { baseResource, problemListCategory, observationCategory } = require('../helpers/resourceHelper');
|
|
6
6
|
const { MENTION_SLUG } = require('../constants/projectionSlugs');
|
|
7
7
|
const { MENTION_EXT } = require('../constants/extensionSlugs');
|
|
8
|
+
const { PROCEDURE_STATUS, statusFrom } = require('./contextFactProjection');
|
|
8
9
|
|
|
9
10
|
const ACTIVITY_TEXT = 'clinical-mention-extraction';
|
|
10
11
|
const OBSERVED_FIELD_SYSTEM_KEY = 'observedField';
|
|
@@ -137,7 +138,8 @@ function adherenceFor(assertion) {
|
|
|
137
138
|
|
|
138
139
|
function procedureStatus(fact, provenance) {
|
|
139
140
|
if (fact.assertion.negated) return 'not-done';
|
|
140
|
-
|
|
141
|
+
if (provenance.needsConfirmation) return 'unknown';
|
|
142
|
+
return statusFrom(PROCEDURE_STATUS, fact.observedFields?.status, { absent: 'completed', unrecognised: 'completed' });
|
|
141
143
|
}
|
|
142
144
|
|
|
143
145
|
function procedureResource({ fact, provenance }) {
|
|
@@ -33,7 +33,7 @@ const PROCEDURE_STATUS = {
|
|
|
33
33
|
ended: 'completed', realizada: 'completed', realizado: 'completed',
|
|
34
34
|
stopped: 'stopped', suspendido: 'stopped', suspendida: 'stopped',
|
|
35
35
|
suspended: 'stopped', discontinued: 'stopped', not_active: 'stopped',
|
|
36
|
-
paused: 'on-hold',
|
|
36
|
+
paused: 'on-hold', started: 'in-progress',
|
|
37
37
|
finalizado: 'completed', end: 'completed', past: 'completed', previous: 'completed',
|
|
38
38
|
historical: 'completed', historico: 'completed',
|
|
39
39
|
cancelled: 'not-done', cancelado: 'not-done', cancelada: 'not-done',
|
|
@@ -363,6 +363,8 @@ function fromFhir(resources) {
|
|
|
363
363
|
|
|
364
364
|
module.exports = {
|
|
365
365
|
CONTEXT_RESOURCE_TYPES,
|
|
366
|
+
PROCEDURE_STATUS,
|
|
367
|
+
statusFrom,
|
|
366
368
|
toFhir,
|
|
367
369
|
fromFhir,
|
|
368
370
|
conceptKeyOf,
|