@peopl-health/nexus 5.68.7 → 5.68.9
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/cohortConfig.js +1 -1
- package/lib/clinical/context/ToolRuntimeContext.js +5 -2
- package/lib/clinical/flags/flagSnapshot.js +59 -0
- package/lib/clinical/providers/AnthropicProvider.js +1 -1
- package/lib/clinical/providers/BaseLLMProvider.js +8 -2
- package/lib/clinical/services/promptComposerService.js +4 -1
- package/lib/models/escalationReviewModel.js +1 -0
- package/lib/services/escalationReviewService.js +15 -8
- package/lib/services/reviewDraftSweepService.js +1 -1
- package/package.json +1 -1
|
@@ -56,6 +56,7 @@ class ToolRuntimeContext {
|
|
|
56
56
|
this.skillActivationIteration = new Map();
|
|
57
57
|
this.currentIteration = null;
|
|
58
58
|
this.toolHistory = [];
|
|
59
|
+
this.toolRejections = [];
|
|
59
60
|
this.safetyArtifact = null;
|
|
60
61
|
this.palliativeAssessment = null;
|
|
61
62
|
this.careplan = null;
|
|
@@ -93,9 +94,11 @@ class ToolRuntimeContext {
|
|
|
93
94
|
this.trace.addStep({ iteration, ...toolData });
|
|
94
95
|
if (toolData.success) this.toolHistory.push(toolData);
|
|
95
96
|
if (!toolData.success || isToolFailure(toolData.tool_output)) {
|
|
97
|
+
const code = refusalCode(toolData.tool_output);
|
|
98
|
+
this.toolRejections.push({ tool: toolData.tool_name, code, iteration: iteration ?? null });
|
|
96
99
|
logger.warn('[toolRuntime] tool call rejected', {
|
|
97
100
|
tool: toolData.tool_name,
|
|
98
|
-
reason:
|
|
101
|
+
reason: code,
|
|
99
102
|
iteration: iteration ?? null,
|
|
100
103
|
turnId: this.turnId || null,
|
|
101
104
|
});
|
|
@@ -108,7 +111,7 @@ class ToolRuntimeContext {
|
|
|
108
111
|
}
|
|
109
112
|
|
|
110
113
|
async store({ outbound = null, completed = false, predictionTimeMs = null, error = null } = {}) {
|
|
111
|
-
this.trace.setSignals({ activatedSkills: [...this.activatedSkills].sort() });
|
|
114
|
+
this.trace.setSignals({ activatedSkills: [...this.activatedSkills].sort(), toolRejections: [...this.toolRejections] });
|
|
112
115
|
this.trace.setOutbound(outbound || this.trace.outbound || null);
|
|
113
116
|
const hasError = error !== null && error !== undefined;
|
|
114
117
|
const isSuperseded = hasError ? false : !this.isLive();
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const { isCohortEnabled } = require('../config/cohortConfig');
|
|
2
|
+
const { isBridgeEnabled } = require('./bridgeConfig');
|
|
3
|
+
const { contextReadLive, contextMirrorLive, contextVerifierLive } = require('./clinicalContextFlags');
|
|
4
|
+
const { clinicalPatternFloorLive } = require('./clinicalPatternFloorFlags');
|
|
5
|
+
const { isComposerEnabled, isAgentSelfRenderEnabled } = require('./composerFlags');
|
|
6
|
+
const { isContingencyCancelOnReplyLive, isArmOnOpenCeilingLive, isContingencyRuleBlockLive } = require('./contingencyFlags');
|
|
7
|
+
const { escalationAssessmentLive, escalationUpdateModeLive } = require('./escalationFlags');
|
|
8
|
+
const { inboundRecoveryLive, coldInboundMode } = require('./inboundRecoveryFlags');
|
|
9
|
+
const { isLlmAuditEnabled } = require('./llmAuditConfig');
|
|
10
|
+
const { isLlmMonitorEnabled } = require('./llmMonitorConfig');
|
|
11
|
+
const { patternConsultLive } = require('./patternConsultFlags');
|
|
12
|
+
const { recommendationRetryLive } = require('./recommendationRecoveryFlags');
|
|
13
|
+
const { isShadowEnabled } = require('./shadowConfig');
|
|
14
|
+
const { supportLightFromChatLive } = require('./supportLightFlags');
|
|
15
|
+
const { triageGateFloorLive } = require('./triageGateFloorFlags');
|
|
16
|
+
const { triageRecommendationLive, rankComparatorLive } = require('./triageRecommendationFlags');
|
|
17
|
+
const { isConversationalDeclarationLive, isConversationalDraftReuseLive, isDeclarationModelCheckLive } = require('./turnEfficiencyFlags');
|
|
18
|
+
|
|
19
|
+
const FLAG_READERS = {
|
|
20
|
+
cohort: isCohortEnabled,
|
|
21
|
+
bridge: isBridgeEnabled,
|
|
22
|
+
contextRead: contextReadLive,
|
|
23
|
+
contextMirror: contextMirrorLive,
|
|
24
|
+
contextVerifier: contextVerifierLive,
|
|
25
|
+
clinicalPatternFloor: clinicalPatternFloorLive,
|
|
26
|
+
composer: isComposerEnabled,
|
|
27
|
+
agentSelfRender: isAgentSelfRenderEnabled,
|
|
28
|
+
contingencyCancelOnReply: isContingencyCancelOnReplyLive,
|
|
29
|
+
armOnOpenCeiling: isArmOnOpenCeilingLive,
|
|
30
|
+
contingencyRuleBlock: isContingencyRuleBlockLive,
|
|
31
|
+
escalationAssessment: escalationAssessmentLive,
|
|
32
|
+
escalationUpdateMode: escalationUpdateModeLive,
|
|
33
|
+
inboundRecovery: inboundRecoveryLive,
|
|
34
|
+
coldInboundMode,
|
|
35
|
+
llmAudit: isLlmAuditEnabled,
|
|
36
|
+
llmMonitor: isLlmMonitorEnabled,
|
|
37
|
+
patternConsult: patternConsultLive,
|
|
38
|
+
recommendationRetry: recommendationRetryLive,
|
|
39
|
+
shadow: isShadowEnabled,
|
|
40
|
+
supportLightFromChat: supportLightFromChatLive,
|
|
41
|
+
triageGateFloor: triageGateFloorLive,
|
|
42
|
+
triageRecommendation: triageRecommendationLive,
|
|
43
|
+
rankComparator: rankComparatorLive,
|
|
44
|
+
conversationalDeclaration: isConversationalDeclarationLive,
|
|
45
|
+
conversationalDraftReuse: isConversationalDraftReuseLive,
|
|
46
|
+
declarationModelCheck: isDeclarationModelCheckLive,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function snapshotClinicalFlags() {
|
|
50
|
+
return Object.fromEntries(Object.entries(FLAG_READERS).map(([name, read]) => {
|
|
51
|
+
try {
|
|
52
|
+
return [name, read()];
|
|
53
|
+
} catch (error) {
|
|
54
|
+
return [name, null];
|
|
55
|
+
}
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { snapshotClinicalFlags };
|
|
@@ -240,7 +240,7 @@ class AnthropicProvider extends BaseLLMProvider {
|
|
|
240
240
|
if (lastUserTurn?.role === 'user') {
|
|
241
241
|
deliveryNudges++;
|
|
242
242
|
previousRoundTools = [];
|
|
243
|
-
trace?.setSignals?.({ deliveryNudges });
|
|
243
|
+
trace?.setSignals?.({ deliveryNudges, nudgeKind: canDecide ? 'outreach' : 'delivery' });
|
|
244
244
|
logger.warn('[AnthropicProvider] Turn will not close — nudging', { tool: canDecide ? OUTREACH_DECISION_TOOL : 'DeliverPatientMessage', deliveryNudges, hasEnded, isBudgetSpent });
|
|
245
245
|
const turnContent = Array.isArray(lastUserTurn.content)
|
|
246
246
|
? lastUserTurn.content
|
|
@@ -10,6 +10,7 @@ const { resolveSkillCatalog } = require('../services/skillService');
|
|
|
10
10
|
const { RISK_HEADLINE_MARKER } = require('../services/riskHeadlineService');
|
|
11
11
|
const { resolveClinicalContext } = require('../services/clinicalContextReadService');
|
|
12
12
|
const { isContingencyRuleBlockLive } = require('../flags/contingencyFlags');
|
|
13
|
+
const { snapshotClinicalFlags } = require('../flags/flagSnapshot');
|
|
13
14
|
const { TURN_KIND } = require('../context/turnContext');
|
|
14
15
|
const { extractPatientMessage } = require('../../helpers/patientMessageHelper');
|
|
15
16
|
const runtimeConfig = require('../../config/runtimeConfig');
|
|
@@ -102,7 +103,7 @@ class BaseLLMProvider {
|
|
|
102
103
|
presetId: resolvedPresetId, presetVersion: resolvedPresetVersion, modelConfig,
|
|
103
104
|
} = await composePrompt({ presetId, presetVersion, promptId: assistantId, variables: promptVariables });
|
|
104
105
|
|
|
105
|
-
const { toolIds, filtered, descriptions: toolDescriptions } = await resolveTools({
|
|
106
|
+
const { toolIds, filtered, descriptions: toolDescriptions, unregisteredToolIds = [] } = await resolveTools({
|
|
106
107
|
promptId: resolvedPromptId || assistantId, assistant, presetToolIds,
|
|
107
108
|
});
|
|
108
109
|
const requestedAdditionalToolIds = Array.isArray(prePromptResult?.additionalToolIds)
|
|
@@ -200,7 +201,12 @@ class BaseLLMProvider {
|
|
|
200
201
|
}
|
|
201
202
|
|
|
202
203
|
trace?.setPrompt({ resolvedPresetId, resolvedPresetVersion });
|
|
203
|
-
trace?.setSignals?.({
|
|
204
|
+
trace?.setSignals?.({
|
|
205
|
+
offeredToolIds: effectiveToolIds,
|
|
206
|
+
enabledSkills,
|
|
207
|
+
flags: snapshotClinicalFlags(),
|
|
208
|
+
...(unregisteredToolIds.length > 0 && { unregisteredToolIds }),
|
|
209
|
+
});
|
|
204
210
|
|
|
205
211
|
const { response, toolsExecuted, retries, usage } = await this._invokeModel({
|
|
206
212
|
instructions: instructions || devContent || '',
|
|
@@ -191,6 +191,7 @@ async function fetchToolsByRecordIds(recordIds) {
|
|
|
191
191
|
return tools;
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
const BLANK_TOOL_ID = '(blank tool_id)';
|
|
194
195
|
const warnedUnlinked = new Set();
|
|
195
196
|
|
|
196
197
|
function warnUnlinkedRegisteredTools(linkedToolIds, promptId) {
|
|
@@ -228,6 +229,7 @@ async function resolveTools({ promptId, assistant, presetToolIds = null, status
|
|
|
228
229
|
const activeTools = mappedTools.filter(t => !t.status || t.status === 'prod' || (!isProd && t.status === 'dev'));
|
|
229
230
|
|
|
230
231
|
const validToolIds = [];
|
|
232
|
+
const unregisteredToolIds = [];
|
|
231
233
|
const descriptions = {};
|
|
232
234
|
|
|
233
235
|
for (const tool of activeTools) {
|
|
@@ -237,6 +239,7 @@ async function resolveTools({ promptId, assistant, presetToolIds = null, status
|
|
|
237
239
|
// An Airtable column retyped away from long text would otherwise reach the model as [object Object].
|
|
238
240
|
if (typeof tool.description === 'string' && tool.description) descriptions[id] = tool.description;
|
|
239
241
|
} else {
|
|
242
|
+
unregisteredToolIds.push(id || BLANK_TOOL_ID);
|
|
240
243
|
logger.error('[promptComposer] Tool mapped in Airtable but not registered in code', { toolId: id, promptId });
|
|
241
244
|
}
|
|
242
245
|
}
|
|
@@ -252,7 +255,7 @@ async function resolveTools({ promptId, assistant, presetToolIds = null, status
|
|
|
252
255
|
|
|
253
256
|
logger.debug('[promptComposer] Tools resolved', { promptId, mapped: activeTools.length, valid: validToolIds.length, filtered: true });
|
|
254
257
|
|
|
255
|
-
return { toolIds: validToolIds, filtered: true, descriptions };
|
|
258
|
+
return { toolIds: validToolIds, filtered: true, descriptions, unregisteredToolIds };
|
|
256
259
|
}
|
|
257
260
|
|
|
258
261
|
function clearCache() {
|
|
@@ -98,6 +98,7 @@ const draftSchema = new mongoose.Schema({
|
|
|
98
98
|
missingCharacterization: { type: [String], default: undefined },
|
|
99
99
|
suggestCall: { type: suggestCallSchema, default: undefined },
|
|
100
100
|
sectionsUnavailable: { type: [String], default: undefined },
|
|
101
|
+
snapshotAt: { type: Date, default: undefined },
|
|
101
102
|
}, { timestamps: { createdAt: true, updatedAt: false } });
|
|
102
103
|
|
|
103
104
|
const doctorHandoffSchema = new mongoose.Schema({
|
|
@@ -44,7 +44,7 @@ const AUDIT_SOURCE = 'escalation_review';
|
|
|
44
44
|
const RECENT_REVIEW_LIMIT = 20;
|
|
45
45
|
const TIMELINE_LIMIT = 500;
|
|
46
46
|
const MAX_REVIEW_UPDATES = 50;
|
|
47
|
-
const DRAFT_DEBOUNCE_MS =
|
|
47
|
+
const DRAFT_DEBOUNCE_MS = 20 * 1000;
|
|
48
48
|
const DRAFT_OPERATOR = 'bot:escalationDraft';
|
|
49
49
|
const DIRECT_CHAT_ATTEMPTS = 3;
|
|
50
50
|
const RECHECK_DELAY_MS = 60 * 1000;
|
|
@@ -1098,6 +1098,7 @@ async function settleFollowUp(snapshot, { background = true } = {}) {
|
|
|
1098
1098
|
}
|
|
1099
1099
|
|
|
1100
1100
|
async function generateBotDraft({ id, lease = null } = {}) {
|
|
1101
|
+
const startedAt = new Date();
|
|
1101
1102
|
const review = await loadReview(reviewIdOf(id));
|
|
1102
1103
|
if (review.closedAt || !DOCUMENTING_STATUSES.includes(review.status)) return { review: null, retry: false };
|
|
1103
1104
|
const latest = (review.drafts ?? []).at(-1);
|
|
@@ -1127,6 +1128,7 @@ async function generateBotDraft({ id, lease = null } = {}) {
|
|
|
1127
1128
|
}
|
|
1128
1129
|
|
|
1129
1130
|
const entry = {
|
|
1131
|
+
snapshotAt: startedAt,
|
|
1130
1132
|
doctorText: withIdentity({ identity, review, doctorText: draft.doctorText }),
|
|
1131
1133
|
patientMessage: draft.patientMessage,
|
|
1132
1134
|
writtenBy: DRAFT_OPERATOR,
|
|
@@ -1137,13 +1139,16 @@ async function generateBotDraft({ id, lease = null } = {}) {
|
|
|
1137
1139
|
...(draft.suggestCall && { suggestCall: draft.suggestCall }),
|
|
1138
1140
|
...(draft.sectionsUnavailable?.length && { sectionsUnavailable: draft.sectionsUnavailable }),
|
|
1139
1141
|
};
|
|
1140
|
-
const
|
|
1141
|
-
|
|
1142
|
-
{
|
|
1142
|
+
const EscalationReview = getEscalationReview();
|
|
1143
|
+
const saved = await EscalationReview.findOneAndUpdate(
|
|
1144
|
+
{ _id: review._id, status: { $in: DOCUMENTING_STATUSES }, draftDueAt: { $lte: lease }, drafts: { $size: (review.drafts ?? []).length } },
|
|
1145
|
+
{ $push: { drafts: entry }, $set: { status: 'draft_ready', draftFailedAt: null }, $unset: { identityBlock: '' } },
|
|
1143
1146
|
{ new: true },
|
|
1144
1147
|
).lean();
|
|
1145
|
-
if (saved)
|
|
1146
|
-
|
|
1148
|
+
if (!saved) return { review: null, retry: false };
|
|
1149
|
+
const released = await EscalationReview.updateOne({ _id: review._id, draftDueAt: lease }, { $set: { draftDueAt: null } }, BOOKKEEPING);
|
|
1150
|
+
await audit(saved, DRAFT_OPERATOR, null, { action: 'draft_saved', draftNumber: saved.drafts.length });
|
|
1151
|
+
return { review: released.matchedCount ? { ...saved, draftDueAt: null } : saved, retry: false };
|
|
1147
1152
|
}
|
|
1148
1153
|
|
|
1149
1154
|
async function noteFactsContext(review) {
|
|
@@ -1361,7 +1366,7 @@ async function handOffToDoctor({ id, mode, physicianEmail = null, sentBy, reques
|
|
|
1361
1366
|
if (!approver) throw httpError(403, 'The physician is not an escalation review approver');
|
|
1362
1367
|
}
|
|
1363
1368
|
|
|
1364
|
-
const draftedAt = draft
|
|
1369
|
+
const draftedAt = draftTimeOf(draft) ? new Date(draftTimeOf(draft)).getTime() : null;
|
|
1365
1370
|
if (draftedAt && (review.alarms ?? []).some((alarm) => new Date(alarm.raisedAt).getTime() > draftedAt)) {
|
|
1366
1371
|
throw httpError(409, 'A new alarm arrived after this draft was written; review it and save a new draft before sending it to the doctor');
|
|
1367
1372
|
}
|
|
@@ -1457,7 +1462,7 @@ async function recordDoctorDecision({
|
|
|
1457
1462
|
// Truthiness, not != null: an empty textarea is a decline with nothing to say.
|
|
1458
1463
|
if (decision === 'needs_intervention' && finalText) approvedText = requiredText(finalText, 'finalText');
|
|
1459
1464
|
|
|
1460
|
-
if (APPROVALS.includes(decision) && arrivedAfter(review, draft
|
|
1465
|
+
if (APPROVALS.includes(decision) && arrivedAfter(review, draftTimeOf(draft))) {
|
|
1461
1466
|
if (review.status === 'awaiting_doctor') {
|
|
1462
1467
|
const redraft = draft.writtenBy === DRAFT_OPERATOR ? { draftDueAt: new Date(), draftAttempts: 0 } : {};
|
|
1463
1468
|
const returned = await updateReview(reviewId, ['awaiting_doctor'], { $set: { status: 'documenting', ...redraft } }, {
|
|
@@ -1572,6 +1577,8 @@ const latestUpdateGuard = (review) => {
|
|
|
1572
1577
|
return { updates: { $size: updates.length }, [`updates.${updates.length - 1}.turnId`]: updates.at(-1).turnId };
|
|
1573
1578
|
};
|
|
1574
1579
|
|
|
1580
|
+
const draftTimeOf = (draft) => draft.snapshotAt ?? draft.createdAt;
|
|
1581
|
+
|
|
1575
1582
|
const arrivedAfter = (review, since) => Boolean(since) && [
|
|
1576
1583
|
...(review.alarms ?? []).map((alarm) => alarm.raisedAt),
|
|
1577
1584
|
...(review.updates ?? []).map((update) => update.at),
|
|
@@ -6,7 +6,7 @@ const { getEscalationReview } = require('../models/escalationReviewModel');
|
|
|
6
6
|
const { BOOKKEEPING, DOCUMENTING_STATUSES, generateBotDraft } = require('./escalationReviewService');
|
|
7
7
|
|
|
8
8
|
const SWEEP_LIMIT = 1;
|
|
9
|
-
const DEFAULT_SWEEP_INTERVAL_MS =
|
|
9
|
+
const DEFAULT_SWEEP_INTERVAL_MS = 10 * 1000;
|
|
10
10
|
const RETRY_DELAY_MS = 5 * 60 * 1000;
|
|
11
11
|
const MAX_DRAFT_ATTEMPTS = 3;
|
|
12
12
|
const LEASE_MS = 10 * 60 * 1000;
|