@peopl-health/nexus 5.71.2 → 5.71.3
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.
|
@@ -10,6 +10,10 @@ function contextVerifierLive() {
|
|
|
10
10
|
return flagOn('CLINICAL_CONTEXT_VERIFIER_LIVE');
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
function chemoContextLive() {
|
|
14
|
+
return flagOn('CHEMO_CONTEXT_LIVE');
|
|
15
|
+
}
|
|
16
|
+
|
|
13
17
|
function profileFromMongo() {
|
|
14
18
|
return flagOn('PATIENT_PROFILE_FROM_MONGO');
|
|
15
19
|
}
|
|
@@ -23,6 +27,7 @@ function contextMirrorLive() {
|
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
module.exports = {
|
|
30
|
+
chemoContextLive,
|
|
26
31
|
contextReadLive,
|
|
27
32
|
contextMirrorLive,
|
|
28
33
|
contextVerifierLive,
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
const { mexicoDay } = require('../../utils/dateUtils');
|
|
2
|
+
const { foldAccents } = require('../../utils/formatUtils');
|
|
3
|
+
const { safeParse, isPlainObject } = require('../../utils/jsonUtils');
|
|
4
|
+
const { logger } = require('../../utils/logger');
|
|
5
|
+
|
|
6
|
+
const ACTIVE_DAYS = 35;
|
|
7
|
+
const HEADER = 'Quimioterapia (registro de la unidad de quimioterapia; úsalo para contextualizar y priorizar síntomas, nunca para bajar la urgencia; no comuniques dosis ni ajustes de tratamiento; si el paciente dice otra cosa, créele y avisa al equipo)';
|
|
8
|
+
const ISO_DAY = /^\d{4}-\d{2}-\d{2}$/;
|
|
9
|
+
const NO_ALLERGY = /^(ninguna?s?|no|negadas?|desconocidas?|(niega|no refiere|sin|no tiene|no presenta) alergias?( conocidas| a medicamentos)?)$/;
|
|
10
|
+
const NEGATION = /\b(sin|niega|no|ninguna?)\b/;
|
|
11
|
+
|
|
12
|
+
const isDay = (value) => typeof value === 'string' && ISO_DAY.test(value) && !Number.isNaN(Date.parse(`${value}T00:00:00Z`));
|
|
13
|
+
const dmy = (iso) => `${iso.slice(8, 10)}/${iso.slice(5, 7)}/${iso.slice(0, 4)}`;
|
|
14
|
+
const daysBetween = (from, to) => Math.round((Date.parse(`${to}T00:00:00Z`) - Date.parse(`${from}T00:00:00Z`)) / 86400000);
|
|
15
|
+
const addDays = (iso, days) => new Date(Date.parse(`${iso}T00:00:00Z`) + days * 86400000).toISOString().slice(0, 10);
|
|
16
|
+
const fold = (value) => foldAccents(String(value || '')).toLowerCase().trim();
|
|
17
|
+
const text = (value) => (typeof value === 'string' ? value.trim() : '');
|
|
18
|
+
const list = (value) => (Array.isArray(value) ? value : []);
|
|
19
|
+
const texts = (value) => list(value).map(text).filter(Boolean);
|
|
20
|
+
const dayOr = (value) => (isDay(value) ? value : null);
|
|
21
|
+
|
|
22
|
+
function normalize(raw) {
|
|
23
|
+
const last = isPlainObject(raw.ultimaAplicacion) && isDay(raw.ultimaAplicacion.fecha) ? raw.ultimaAplicacion : null;
|
|
24
|
+
const nadir = Array.isArray(raw.nadirDias) && raw.nadirDias.length === 2 && raw.nadirDias.every(Number.isInteger) ? raw.nadirDias : null;
|
|
25
|
+
const dated = (value, keys) => (isPlainObject(value) && isDay(value.fecha) && keys.every((k) => k in value) ? value : null);
|
|
26
|
+
return {
|
|
27
|
+
ultimaAplicacion: last,
|
|
28
|
+
esquema: list(raw.esquema).filter(isPlainObject).filter((e) => text(e.farmaco)),
|
|
29
|
+
vigilancia: texts(raw.vigilancia),
|
|
30
|
+
nadirDias: nadir,
|
|
31
|
+
premedicacion: texts(raw.premedicacion),
|
|
32
|
+
enCasa: list(raw.enCasa).filter(isPlainObject).filter((m) => text(m.farmaco)),
|
|
33
|
+
inicioEsquema: dated(raw.inicioEsquema, ['anteriorAlRegistro']),
|
|
34
|
+
proximaSesion: dated(raw.proximaSesion, ['tipo']),
|
|
35
|
+
noAplicadas: list(raw.noAplicadas).filter(isDay),
|
|
36
|
+
reaccionInfusion: dated(raw.reaccionInfusion, ['texto']),
|
|
37
|
+
alergias: dated(raw.alergias, ['estado', 'texto']),
|
|
38
|
+
acceso: text(raw.acceso),
|
|
39
|
+
peso: dated(raw.peso, ['kg']),
|
|
40
|
+
signosAnormales: dated(raw.signosAnormales, ['hallazgos']),
|
|
41
|
+
ajusteProximoCiclo: dated(raw.ajusteProximoCiclo, ['valor']),
|
|
42
|
+
esquemaPrevio: isPlainObject(raw.esquemaPrevio) && isDay(raw.esquemaPrevio.hasta) ? { farmacos: texts(raw.esquemaPrevio.farmacos), hasta: raw.esquemaPrevio.hasta } : null,
|
|
43
|
+
visitaSoporte: dated(raw.visitaSoporte, ['farmacos']),
|
|
44
|
+
antecedentes: dated(raw.antecedentes, []),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseChemoContext(raw) {
|
|
49
|
+
const parsed = safeParse(raw, null);
|
|
50
|
+
return isPlainObject(parsed) && parsed.version === 1 ? normalize(parsed) : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function chemoStatus(context, today) {
|
|
54
|
+
const last = context.ultimaAplicacion?.fecha;
|
|
55
|
+
if (!last) return { active: false, days: null };
|
|
56
|
+
const days = daysBetween(last, today);
|
|
57
|
+
if (days < 0) return { active: true, days: null };
|
|
58
|
+
const scheduled = context.proximaSesion && context.proximaSesion.fecha >= today;
|
|
59
|
+
return { active: days <= ACTIVE_DAYS || Boolean(scheduled), days };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function regimenText(context) {
|
|
63
|
+
return context.esquema.map((e) => [e.farmaco, e.via, e.tiempo].map(text).filter(Boolean).join(' ')).join('; ');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function statusLine(last, active, days) {
|
|
67
|
+
const cycle = [last.ciclo ? `ciclo ${last.ciclo}` : '', last.sesion ? `sesión ${last.sesion}${last.total ? `/${last.total}` : ''}` : ''].filter(Boolean).join(', ');
|
|
68
|
+
const when = days === null ? `última aplicación registrada ${dmy(last.fecha)}` : `última aplicación ${dmy(last.fecha)} (hace ${days} días)`;
|
|
69
|
+
return active
|
|
70
|
+
? `Estado: en quimioterapia activa; ${when}${cycle ? `, ${cycle}` : ''}`
|
|
71
|
+
: `Estado: sin aplicaciones registradas en la unidad desde ${dmy(last.fecha)} (hace ${days} días); confirma con el paciente si sigue en tratamiento`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function chemoLines(context, today) {
|
|
75
|
+
const lines = [];
|
|
76
|
+
const last = context.ultimaAplicacion;
|
|
77
|
+
const { active, days } = chemoStatus(context, today);
|
|
78
|
+
if (last) {
|
|
79
|
+
lines.push(statusLine(last, active, days));
|
|
80
|
+
const regimen = regimenText(context);
|
|
81
|
+
if (regimen) lines.push(`${active ? 'Esquema actual' : 'Último esquema'}: ${regimen}`);
|
|
82
|
+
if (context.vigilancia.length) lines.push(`Vigilar por sus fármacos: ${context.vigilancia.join('; ')}`);
|
|
83
|
+
if (active && days !== null && context.nadirDias && days <= context.nadirDias[1]) {
|
|
84
|
+
const [from, to] = context.nadirDias;
|
|
85
|
+
lines.push(`Mayor riesgo de defensas bajas: días ${from} a ${to} después de la aplicación (${dmy(addDays(last.fecha, from))} al ${dmy(addDays(last.fecha, to))}); hoy es el día ${days}. Fiebre de 38 °C o más es urgente cualquier día`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (context.enCasa.length) {
|
|
89
|
+
lines.push(`Se fue a casa con: ${context.enCasa.map((m) => `${text(m.farmaco)}${text(m.indicacion) ? ` (${text(m.indicacion)})` : ''}${m.esteroide ? ' [esteroide: puede ocultar la fiebre]' : ''}`).join('; ')}`);
|
|
90
|
+
}
|
|
91
|
+
if (context.premedicacion.length) lines.push(`Premedicación en la unidad: ${context.premedicacion.join(', ')}`);
|
|
92
|
+
if (context.inicioEsquema) {
|
|
93
|
+
lines.push(context.inicioEsquema.anteriorAlRegistro
|
|
94
|
+
? `Inicio de este esquema: antes del ${dmy(context.inicioEsquema.fecha)} (inicio del registro)`
|
|
95
|
+
: `Inicio de este esquema: ${dmy(context.inicioEsquema.fecha)}`);
|
|
96
|
+
}
|
|
97
|
+
if (context.proximaSesion) {
|
|
98
|
+
lines.push(context.proximaSesion.tipo === 'agendada'
|
|
99
|
+
? `Próxima sesión: ${dmy(context.proximaSesion.fecha)} (agendada)`
|
|
100
|
+
: `Próxima sesión: ${dmy(context.proximaSesion.fecha)} (estimada; no la confirmes como cita)`);
|
|
101
|
+
}
|
|
102
|
+
if (context.noAplicadas.length) lines.push(`Sesiones no aplicadas en los últimos 90 días: ${context.noAplicadas.map(dmy).join(', ')}`);
|
|
103
|
+
if (text(context.reaccionInfusion?.texto)) lines.push(`Reacción durante la infusión registrada: ${text(context.reaccionInfusion.texto)} (${dmy(context.reaccionInfusion.fecha)})`);
|
|
104
|
+
if (context.acceso) lines.push(`Acceso venoso: ${context.acceso}`);
|
|
105
|
+
if (Number.isFinite(context.peso?.kg)) {
|
|
106
|
+
const pct = context.peso.cambioPct;
|
|
107
|
+
const change = Number.isFinite(pct) && isDay(context.peso.desde) ? `; cambio ${pct > 0 ? '+' : ''}${pct}% desde ${dmy(context.peso.desde)}` : '';
|
|
108
|
+
lines.push(`Peso: ${context.peso.kg} kg (${dmy(context.peso.fecha)})${change}`);
|
|
109
|
+
}
|
|
110
|
+
const findings = texts(context.signosAnormales?.hallazgos);
|
|
111
|
+
if (findings.length) lines.push(`Signos fuera de rango en la última sesión (${dmy(context.signosAnormales.fecha)}): ${findings.join(', ')}`);
|
|
112
|
+
if (context.ajusteProximoCiclo?.valor === true) lines.push(`El médico indicó un ajuste para el próximo ciclo (${dmy(context.ajusteProximoCiclo.fecha)}): no lo comuniques, lo explica el médico`);
|
|
113
|
+
if (context.esquemaPrevio?.farmacos.length) lines.push(`Esquema previo: ${context.esquemaPrevio.farmacos.join(', ')}, hasta ${dmy(context.esquemaPrevio.hasta)}`);
|
|
114
|
+
if (context.visitaSoporte) {
|
|
115
|
+
const drugs = texts(context.visitaSoporte.farmacos);
|
|
116
|
+
lines.push(`Última visita a la unidad sin quimioterapia: ${dmy(context.visitaSoporte.fecha)}${drugs.length ? ` (${drugs.join(', ')})` : ''}`);
|
|
117
|
+
}
|
|
118
|
+
const h = context.antecedentes;
|
|
119
|
+
if (h) {
|
|
120
|
+
const parts = [['diabetes', h.diabetes], ['hipertensión', h.hipertension], ['otros', h.otros], ['histología', h.histologia], ['tratamiento previo', h.tratamientoPrevio]]
|
|
121
|
+
.filter(([, value]) => text(value)).map(([label, value]) => `${label}: ${text(value)}`);
|
|
122
|
+
if (parts.length) lines.push(`Antecedentes (historia de primera vez, enfermería, ${dmy(h.fecha)}): ${parts.join('; ')}`);
|
|
123
|
+
}
|
|
124
|
+
return lines;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function renderChemoContext(context, today = mexicoDay(new Date())) {
|
|
128
|
+
if (!context) return '';
|
|
129
|
+
try {
|
|
130
|
+
const lines = chemoLines(context, today);
|
|
131
|
+
return lines.length ? `${HEADER}:\n- ${lines.join('\n- ')}` : '';
|
|
132
|
+
} catch (error) {
|
|
133
|
+
logger.warn('[chemoContext] section left out', { error: error.message });
|
|
134
|
+
return '';
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function listsAllergy(existing, allergy) {
|
|
139
|
+
const held = fold(existing);
|
|
140
|
+
if (NEGATION.test(held)) return false;
|
|
141
|
+
return new RegExp(`(^|[^a-z0-9])${fold(allergy).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^a-z0-9]|$)`).test(held);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function withChemoContext(clinical, context, today = mexicoDay(new Date())) {
|
|
145
|
+
if (!context) return clinical || {};
|
|
146
|
+
try {
|
|
147
|
+
const merged = { ...(clinical || {}) };
|
|
148
|
+
if (!text(merged.tratamiento_actual) && chemoStatus(context, today).active && context.esquema.length) {
|
|
149
|
+
merged.tratamiento_actual = `${regimenText(context)} (según la unidad de quimioterapia)`;
|
|
150
|
+
}
|
|
151
|
+
const allergies = context.alergias;
|
|
152
|
+
const nursing = text(allergies?.texto);
|
|
153
|
+
if (allergies?.estado === 'si' && nursing && !listsAllergy(merged.alergias_medicamentos, nursing)) {
|
|
154
|
+
const recorded = `${nursing} (registrado por enfermería, ${dmy(allergies.fecha)})`;
|
|
155
|
+
const held = text(merged.alergias_medicamentos);
|
|
156
|
+
merged.alergias_medicamentos = !held || NO_ALLERGY.test(fold(held)) ? recorded : `${held}; ${recorded}`;
|
|
157
|
+
}
|
|
158
|
+
return merged;
|
|
159
|
+
} catch (error) {
|
|
160
|
+
logger.warn('[chemoContext] merge skipped', { error: error.message });
|
|
161
|
+
return clinical || {};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = { parseChemoContext, renderChemoContext, withChemoContext };
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
const { pairTriageSymptoms } = require('../helpers/triageSymptomsHelper');
|
|
2
|
+
const { parseChemoContext, renderChemoContext, withChemoContext } = require('../helpers/chemoContextHelper');
|
|
2
3
|
const { handlePendingFunctionCalls: handlePendingFunctionCallsUtil } = require('../providers/OpenAIResponsesProviderTools');
|
|
3
4
|
|
|
4
5
|
const { assembleRiskHeadline } = require('../services/riskHeadlineService');
|
|
5
6
|
const { resolveNutritionPlan } = require('../services/nutritionPlanService');
|
|
6
7
|
const { identityError } = require('../services/patientIdentityService');
|
|
7
8
|
const { resolveProfileIdentity } = require('../services/patientProfileService');
|
|
9
|
+
const { chemoContextLive } = require('../flags/clinicalContextFlags');
|
|
8
10
|
const { Follow_Up_ID } = require('../../config/airtableConfig');
|
|
9
11
|
const runtimeConfig = require('../../config/runtimeConfig');
|
|
10
12
|
|
|
@@ -279,13 +281,14 @@ class DefaultMemoryManager extends MemoryManager {
|
|
|
279
281
|
? JSON.parse(record['clinical-context-json'])
|
|
280
282
|
: null;
|
|
281
283
|
const treatingPhysician = sanitizeInlineText(record.doctor || '', Infinity);
|
|
284
|
+
const chemo = chemoContextLive() ? parseChemoContext(record.quimio_context) : null;
|
|
282
285
|
|
|
283
286
|
const sortedSymptoms = symptomsRecords?.filter(r => r.Date)
|
|
284
287
|
.sort((a, b) => new Date(b.Date) - new Date(a.Date)).slice(0, 3) || [];
|
|
285
288
|
|
|
286
289
|
const symptoms = this._parseSymptoms(sortedSymptoms);
|
|
287
290
|
|
|
288
|
-
return { ...clinicalContext, symptoms, treatingPhysician };
|
|
291
|
+
return { ...clinicalContext, symptoms, treatingPhysician, chemo };
|
|
289
292
|
} catch (error) {
|
|
290
293
|
logger.error('[DefaultMemoryManager] Error fetching clinical context', { error: error.message });
|
|
291
294
|
return null;
|
|
@@ -332,7 +335,7 @@ class DefaultMemoryManager extends MemoryManager {
|
|
|
332
335
|
|
|
333
336
|
const contextParts = [];
|
|
334
337
|
const d = data.demografia;
|
|
335
|
-
const c = data.datos_clinicos;
|
|
338
|
+
const c = withChemoContext(data.datos_clinicos, data.chemo);
|
|
336
339
|
if (d?.edad) contextParts.push(`Edad: ${d.edad}`);
|
|
337
340
|
if (d?.sexo) contextParts.push(`Sexo: ${d.sexo}`);
|
|
338
341
|
if (data.treatingPhysician) contextParts.push(`Médico tratante: ${data.treatingPhysician}`);
|
|
@@ -351,7 +354,7 @@ class DefaultMemoryManager extends MemoryManager {
|
|
|
351
354
|
}).filter(Boolean);
|
|
352
355
|
|
|
353
356
|
return {
|
|
354
|
-
clinicalContext: [contextParts.join('. '), riskHeadline].filter(Boolean).join('\n\n'),
|
|
357
|
+
clinicalContext: [contextParts.join('. '), renderChemoContext(data.chemo), riskHeadline].filter(Boolean).join('\n\n'),
|
|
355
358
|
lastSymptoms: symptomParts.join('; ') || 'Sin síntomas reportados recientemente',
|
|
356
359
|
nutritionPlan: await resolveNutritionPlan(data.nutricion),
|
|
357
360
|
};
|