@peopl-health/nexus 5.71.6 → 5.71.8
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/helpers/gradeEstimateHelper.js +1 -1
- package/lib/clinical/providers/BaseLLMProvider.js +1 -0
- package/lib/clinical/services/gradeAnchorService.js +1 -1
- package/lib/clinical/services/gradeClaimService.js +2 -2
- package/lib/clinical/tools/recordClinicalImpressionTool.js +7 -2
- package/lib/core/NexusMessaging.js +10 -1
- package/lib/index.d.ts +2 -0
- package/package.json +1 -1
|
@@ -77,7 +77,7 @@ function gradeEstimateSchema(description) {
|
|
|
77
77
|
source: {
|
|
78
78
|
type: 'string',
|
|
79
79
|
enum: GRADE_SOURCES,
|
|
80
|
-
description: 'Where this grade came from. Required whenever you set best_estimate. `proctcae_composite`: you are carrying over the number the PRO-CTCAE triage already computed — say this even when you agree with it. When what the patient tells you this turn contradicts that number (they can do today what the questionnaire said they could not), what they tell you now wins: grade it yourself as `agent_adjudicated`. `agent_adjudicated`: you read the grade_scale clauses yourself against what the patient said. `carried_forward`: the case\'s previous grade, unchanged, with no new evidence this turn.',
|
|
80
|
+
description: 'Where this grade came from. Required whenever you set best_estimate. `proctcae_composite`: you are carrying over the number the PRO-CTCAE triage already computed — say this even when you agree with it. When what the patient tells you this turn contradicts that number (they can do today what the questionnaire said they could not), what they tell you now wins: when the term has a grade_scale, grade it yourself as `agent_adjudicated`. `agent_adjudicated`: you read the grade_scale clauses yourself against what the patient said. `carried_forward`: the case\'s previous grade, unchanged, with no new evidence this turn.',
|
|
81
81
|
},
|
|
82
82
|
matched_grade: {
|
|
83
83
|
type: ['integer', 'null'],
|
|
@@ -377,6 +377,7 @@ class BaseLLMProvider {
|
|
|
377
377
|
'Si una contingencia activa ya cubre la preocupación, no la cruces con otro seguimiento. Si programas, primero usa schedulePatientReminder con reminder_type seguimiento y la misma utility_basis concreta; luego registra hold con el mismo next_check_at.',
|
|
378
378
|
'',
|
|
379
379
|
'Si escribes, que sea para ELLA: lo que te contó, lo que quedó abierto, lo que quieres saber ahora.',
|
|
380
|
+
'Tu texto llega dentro de una plantilla que ya empieza con «Hola {su nombre},»: no saludes ni escribas su nombre, entra directo a lo que quieres decirle, breve (menos de 500 caracteres) y con un emoji como máximo. Es un contacto sobre su cuidado: no le sumes invitaciones a charlas, sesiones ni actividades.',
|
|
380
381
|
'LÍMITE UTILITY: escribe sólo cuando puedas anclar el contacto en algo concreto que la paciente contó, preguntó o acordó, o en una interacción de cuidado todavía vigente. La inactividad por sí sola no basta. Si escribes, registra esa ancla en utility_basis; si sólo queda un saludo general para generar engagement, registra hold con hold_reason no_utility_anchor y utility_basis null.',
|
|
381
382
|
'No repitas un mensaje que ya le enviaste, y no menciones automatizaciones, listas ni seguimientos programados.',
|
|
382
383
|
);
|
|
@@ -70,7 +70,7 @@ function gradeAnchorNotice({ ctcaeTerm, grade, anchor }) {
|
|
|
70
70
|
const named = leaned === 'self_care' ? 'self-care' : 'instrumental';
|
|
71
71
|
if (rulesOutFunctionalPart(leaned, anchor)) {
|
|
72
72
|
return `Grade ${grade} of ${ctcaeTerm} can only stand on the non-functional part of its clause: the patient is not limited in ${named} ADL. `
|
|
73
|
-
+ '`reasoning` must name that criterion; if it is not met, record the grade below.';
|
|
73
|
+
+ '`reasoning` must name that criterion; if it is not met, record the grade below and name in `improvement_basis` what rules this grade out.';
|
|
74
74
|
}
|
|
75
75
|
return `Grade ${grade} of ${ctcaeTerm} was recorded without the ${named} ADL limitation its clause mentions. `
|
|
76
76
|
+ 'That is fine when another part of the clause is met or the whole picture makes it the most likely grade — `reasoning` must name which. '
|
|
@@ -52,9 +52,9 @@ const GUARDS = [
|
|
|
52
52
|
}),
|
|
53
53
|
];
|
|
54
54
|
|
|
55
|
-
function gradeClaimError({ ctcaeTerm, grade, stated = null, previous = null, restatesPriorEvidence = true,
|
|
55
|
+
function gradeClaimError({ ctcaeTerm, grade, stated = null, previous = null, restatesPriorEvidence = true, recordsOngoingRead = false, trace = null }) {
|
|
56
56
|
const resolved = grade || {};
|
|
57
|
-
const claim = { ctcaeTerm, grade: resolved, stated: stated || resolved, previous, restatesPriorEvidence,
|
|
57
|
+
const claim = { ctcaeTerm, grade: resolved, stated: stated || resolved, previous, restatesPriorEvidence, recordsOngoingRead, trace };
|
|
58
58
|
for (const guard of GUARDS) {
|
|
59
59
|
const error = guard(claim);
|
|
60
60
|
if (error) return error;
|
|
@@ -121,7 +121,6 @@ function buildAssessment(condition, previous, args, runtime, restatesPriorEviden
|
|
|
121
121
|
stated: parseGradeEstimate(args.grade_estimate),
|
|
122
122
|
previous: previous.grade,
|
|
123
123
|
restatesPriorEvidence,
|
|
124
|
-
continuesEpisode,
|
|
125
124
|
recordsOngoingRead: ONGOING_READ_EVENTS.includes(normalisedEvent(args)),
|
|
126
125
|
trace: runtime?.trace || null,
|
|
127
126
|
});
|
|
@@ -175,6 +174,10 @@ function requireCeilingGap(args, event) {
|
|
|
175
174
|
return error ? fail(error) : null;
|
|
176
175
|
}
|
|
177
176
|
|
|
177
|
+
function sameGradeAndAnchor(a, b) {
|
|
178
|
+
return !!a && !!b && a.bestEstimate === b.bestEstimate && a.functionalAnchor === b.functionalAnchor;
|
|
179
|
+
}
|
|
180
|
+
|
|
178
181
|
function sameGrade(a, b) {
|
|
179
182
|
return JSON.stringify(projectGradeRead(a)) === JSON.stringify(projectGradeRead(b));
|
|
180
183
|
}
|
|
@@ -351,7 +354,9 @@ async function handler(args = {}, context = {}) {
|
|
|
351
354
|
|
|
352
355
|
const { condition, latestAssessment } = result;
|
|
353
356
|
const consistency = inputConsistency(args);
|
|
354
|
-
|
|
357
|
+
// A hold of the same grade on the same anchor was already advised; repeating it re-asks the patient every turn.
|
|
358
|
+
const heldAsAdvised = event === 'add_evidence' && sameGradeAndAnchor(existing.latestAssessment?.grade, latestAssessment.grade);
|
|
359
|
+
const freshRead = ONGOING_READ_EVENTS.includes(event) && hasGradeSignal(args?.grade_estimate) && !isCarriedForward(args.grade_estimate) && !heldAsAdvised;
|
|
355
360
|
const gradeNotice = freshRead ? gradeClaimNotice({ ctcaeTerm: condition.ctcaeTerm, grade: latestAssessment.grade }) : null;
|
|
356
361
|
return JSON.stringify({
|
|
357
362
|
success: true,
|
|
@@ -38,7 +38,7 @@ const { ProcessingPipeline } = require('../core/ProcessingPipeline');
|
|
|
38
38
|
const { preProcessMessages, configureOpenAIProvider, AssistantProcessor, TURN_KIND } = require('../clinical');
|
|
39
39
|
const { deliveryOutcome } = require('../clinical/AssistantProcessor');
|
|
40
40
|
const { UNASSIGNED, alertOpsForClaimedMessage, coverIfUnassigned, coverUnansweredPatient } = require('../clinical/services/pausedSafetyNetService');
|
|
41
|
-
const { buildOutreachCarrier } = require('../clinical/services/outreachCarrierService');
|
|
41
|
+
const { buildOutreachCarrier, validateAgentText } = require('../clinical/services/outreachCarrierService');
|
|
42
42
|
const { routineOutreachCollisionReason, isWithinTriageProtectionWindow } = require('../clinical/services/outreachSweepService');
|
|
43
43
|
const {
|
|
44
44
|
readPendingReminderForPatient,
|
|
@@ -1037,6 +1037,14 @@ class NexusMessaging {
|
|
|
1037
1037
|
}
|
|
1038
1038
|
const accepted = Boolean(runResult.output) && delivery?.success === true && delivery?.deferred !== true;
|
|
1039
1039
|
const delivered = ['delivered', 'read'].includes(String(delivery?.status || '').toLowerCase());
|
|
1040
|
+
let carrierRejection = null;
|
|
1041
|
+
if (dryRun && decision.decision === 'write') {
|
|
1042
|
+
try {
|
|
1043
|
+
validateAgentText(runResult.output, firstName);
|
|
1044
|
+
} catch (error) {
|
|
1045
|
+
carrierRejection = error.message;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1040
1048
|
if (!dryRun && (decision.decision === 'write') !== accepted) {
|
|
1041
1049
|
logger.warn('[processOutreach] decision and delivery disagree', {
|
|
1042
1050
|
code, decision: decision.decision, accepted, status: delivery?.status || null,
|
|
@@ -1049,6 +1057,7 @@ class NexusMessaging {
|
|
|
1049
1057
|
holdReason: decision.holdReason || null,
|
|
1050
1058
|
nextCheckAt: decision.nextCheckAt || null,
|
|
1051
1059
|
draft: dryRun ? (runResult.output || '') : null,
|
|
1060
|
+
carrierRejection,
|
|
1052
1061
|
accepted,
|
|
1053
1062
|
delivered,
|
|
1054
1063
|
deferred: Boolean(delivery?.deferred),
|
package/lib/index.d.ts
CHANGED
|
@@ -475,6 +475,8 @@ declare module '@peopl-health/nexus' {
|
|
|
475
475
|
nextCheckAt: string | null;
|
|
476
476
|
/** Drafted patient-facing text. Present only on a dry run. */
|
|
477
477
|
draft?: string | null;
|
|
478
|
+
/** Why the out-of-window template would refuse the draft. Present only on a dry run. */
|
|
479
|
+
carrierRejection?: string | null;
|
|
478
480
|
/** Whether the provider accepted the fixed-template send. This is not proof of delivery. */
|
|
479
481
|
accepted?: boolean;
|
|
480
482
|
/** Whether the provider status is already terminal delivered/read. */
|