@peopl-health/nexus 5.67.0-dev.11929 → 5.67.0-dev.11931
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/providers/AnthropicProvider.js +6 -3
- package/lib/clinical/providers/BaseLLMProvider.js +5 -0
- package/lib/clinical/providers/GatewayProvider.js +5 -0
- package/lib/clinical/services/nutritionPlanService.js +36 -31
- package/lib/clinical/services/promptComposerService.js +2 -11
- package/lib/eval/EvalProvider.js +6 -2
- package/package.json +1 -1
|
@@ -475,14 +475,17 @@ class AnthropicProvider extends BaseLLMProvider {
|
|
|
475
475
|
// Split placement: the inline copy carries recency on single-round turns, the hoisted developer
|
|
476
476
|
// turn persists once tool results are appended after it on multi-round turns.
|
|
477
477
|
_buildInput({ additionalInstructions, ...rest }) {
|
|
478
|
-
const
|
|
479
|
-
const pinned = { role: 'developer', content: `${RISK_HEADLINE_MARKER}\n${RISK_HEADLINE_DISCLAIMER}` };
|
|
480
|
-
const ordered = [...input.filter(i => !i[TAIL_PLACEMENT]), pinned, ...input.filter(i => i[TAIL_PLACEMENT])];
|
|
478
|
+
const ordered = this._orderRuntimeMessages(super._buildInput({ additionalInstructions, ...rest }));
|
|
481
479
|
if (!additionalInstructions) return ordered;
|
|
482
480
|
const { content } = this._operatorInstructionTurn(additionalInstructions);
|
|
483
481
|
return [...ordered, { role: 'user', content: `${OPERATOR_TURN_PREFIX} ${content}`, type: 'message' }];
|
|
484
482
|
}
|
|
485
483
|
|
|
484
|
+
_orderRuntimeMessages(input) {
|
|
485
|
+
const pinned = { role: 'developer', content: `${RISK_HEADLINE_MARKER}\n${RISK_HEADLINE_DISCLAIMER}` };
|
|
486
|
+
return [...input.filter(i => !i[TAIL_PLACEMENT]), pinned, ...input.filter(i => i[TAIL_PLACEMENT])];
|
|
487
|
+
}
|
|
488
|
+
|
|
486
489
|
_mapModelConfig(modelConfig) {
|
|
487
490
|
const mapped = { ...(modelConfig || {}) };
|
|
488
491
|
if (mapped.max_tokens == null && mapped.max_output_tokens != null) mapped.max_tokens = mapped.max_output_tokens;
|
|
@@ -783,6 +783,11 @@ class BaseLLMProvider {
|
|
|
783
783
|
};
|
|
784
784
|
}
|
|
785
785
|
|
|
786
|
+
// Providers that pin runtime messages around the tail-placed block override this.
|
|
787
|
+
_orderRuntimeMessages(input) {
|
|
788
|
+
return input;
|
|
789
|
+
}
|
|
790
|
+
|
|
786
791
|
_sanitizeInput(input) {
|
|
787
792
|
const isMessage = (item) => !item.type || item.type === 'message';
|
|
788
793
|
const items = (input || []).filter(item => isMessage(item) ? this._hasTextContent(item.content) : true);
|
|
@@ -38,6 +38,11 @@ class GatewayProvider extends BaseLLMProvider {
|
|
|
38
38
|
return this._sub(vendor)._buildInput(config);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
_orderRuntimeMessages(input, modelConfig = null) {
|
|
42
|
+
const { vendor } = this._route(modelConfig?.model);
|
|
43
|
+
return this._sub(vendor)._orderRuntimeMessages(input);
|
|
44
|
+
}
|
|
45
|
+
|
|
41
46
|
_route(model) {
|
|
42
47
|
if (!model) return { vendor: 'openai', model: undefined };
|
|
43
48
|
const slash = model.indexOf('/');
|
|
@@ -1,13 +1,18 @@
|
|
|
1
|
+
const { Config_ID } = require('../../config/airtableConfig');
|
|
2
|
+
const { fieldEquals } = require('../../helpers/airtableFormulaHelper');
|
|
3
|
+
const { getRecordByFilter } = require('../../services/airtableService');
|
|
1
4
|
const { safeParse, isPlainObject } = require('../../utils/jsonUtils');
|
|
2
5
|
const { logger } = require('../../utils/logger');
|
|
3
6
|
const { sanitizeInlineText } = require('../../utils/sanitizerUtils');
|
|
4
|
-
const {
|
|
7
|
+
const { withTimeout } = require('../../utils/timeoutUtils');
|
|
5
8
|
|
|
6
9
|
const NUTRITION_GUIDE_PROMPT_ID = 'nutrition_plan_guide';
|
|
7
10
|
const PLAN_MAX_CHARS = 1500;
|
|
8
11
|
const GUIDE_MAX_CHARS = 2000;
|
|
9
12
|
const ISO_DATE_RX = /^\d{4}-\d{2}-\d{2}$/;
|
|
10
13
|
const GUIDE_TIMEOUT_MS = 3000;
|
|
14
|
+
const GUIDE_REFRESH_MS = 5 * 60 * 1000;
|
|
15
|
+
const GUIDE_RETRY_MS = 30 * 1000;
|
|
11
16
|
|
|
12
17
|
// Each line of a plan or guide is one kind of indication, so the breaks survive and only each line is flattened.
|
|
13
18
|
function keepLines(text, maxChars) {
|
|
@@ -31,44 +36,42 @@ function readPlanSection(section) {
|
|
|
31
36
|
return { text: typeof text === 'string' ? text : '', reviewBy };
|
|
32
37
|
}
|
|
33
38
|
|
|
34
|
-
|
|
39
|
+
// The guide is served from memory and refreshed in the background: only the very first read can hold a
|
|
40
|
+
// turn (up to GUIDE_TIMEOUT_MS), and there is never more than one Airtable read in flight.
|
|
41
|
+
let guide = { text: '', freshUntil: 0 };
|
|
35
42
|
let guideRead = null;
|
|
36
|
-
let readSeq = 0;
|
|
37
|
-
let appliedSeq = 0;
|
|
38
43
|
|
|
39
|
-
function
|
|
44
|
+
function refreshGuide() {
|
|
40
45
|
if (guideRead) return guideRead;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
.then((
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return lastGuide;
|
|
46
|
+
// Read straight from Airtable: the in-memory copy is this guide's only cache.
|
|
47
|
+
guideRead = getRecordByFilter(Config_ID, 'responses', fieldEquals('prompt_id', NUTRITION_GUIDE_PROMPT_ID))
|
|
48
|
+
.then((records) => {
|
|
49
|
+
const text = keepLines(records?.[0]?.content || '', GUIDE_MAX_CHARS);
|
|
50
|
+
// A missing row reads as empty: keep the last guide and look again soon rather than in five minutes.
|
|
51
|
+
guide = text
|
|
52
|
+
? { text, freshUntil: Date.now() + GUIDE_REFRESH_MS }
|
|
53
|
+
: { ...guide, freshUntil: Date.now() + GUIDE_RETRY_MS };
|
|
50
54
|
})
|
|
51
55
|
.catch((error) => {
|
|
52
|
-
logger.warn('[nutritionPlanService] nutrition guide unreadable;
|
|
53
|
-
|
|
56
|
+
logger.warn('[nutritionPlanService] nutrition guide unreadable; keeping the last one read', { error: error.message });
|
|
57
|
+
guide = { ...guide, freshUntil: Date.now() + GUIDE_RETRY_MS };
|
|
54
58
|
})
|
|
55
|
-
.finally(() => {
|
|
56
|
-
guideRead
|
|
57
|
-
return read;
|
|
59
|
+
.finally(() => { guideRead = null; });
|
|
60
|
+
return guideRead;
|
|
58
61
|
}
|
|
59
62
|
|
|
60
|
-
// The guide only adds to the plan, so a slow read must not hold the patient's turn,
|
|
61
|
-
// and a read that never settles must not hold every later turn either.
|
|
62
63
|
async function loadNutritionGuide() {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
64
|
+
if (guide.freshUntil) {
|
|
65
|
+
if (Date.now() >= guide.freshUntil) refreshGuide();
|
|
66
|
+
return guide.text;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
await withTimeout(refreshGuide(), GUIDE_TIMEOUT_MS, 'nutrition guide read');
|
|
70
|
+
} catch {
|
|
71
|
+
logger.warn('[nutritionPlanService] first nutrition guide read timed out; plan sent without it for now', { timeoutMs: GUIDE_TIMEOUT_MS });
|
|
72
|
+
if (!guide.freshUntil) guide = { ...guide, freshUntil: Date.now() + GUIDE_RETRY_MS };
|
|
73
|
+
}
|
|
74
|
+
return guide.text;
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
async function resolveNutritionPlan(section, { now = new Date() } = {}) {
|
|
@@ -87,6 +90,8 @@ async function resolveNutritionPlan(section, { now = new Date() } = {}) {
|
|
|
87
90
|
module.exports = {
|
|
88
91
|
NUTRITION_GUIDE_PROMPT_ID,
|
|
89
92
|
GUIDE_TIMEOUT_MS,
|
|
93
|
+
GUIDE_REFRESH_MS,
|
|
94
|
+
GUIDE_RETRY_MS,
|
|
90
95
|
resolveNutritionPlan,
|
|
91
|
-
_test: { reset: () => {
|
|
96
|
+
_test: { reset: () => { guide = { text: '', freshUntil: 0 }; guideRead = null; } },
|
|
92
97
|
};
|
|
@@ -14,8 +14,6 @@ const promptCache = new MapCache({ maxSize: 50, ttl: CACHE_TTL });
|
|
|
14
14
|
const snippetCache = new MapCache({ maxSize: 200, ttl: CACHE_TTL });
|
|
15
15
|
const toolCache = new MapCache({ maxSize: 50, ttl: CACHE_TTL });
|
|
16
16
|
const presetCache = new MapCache({ maxSize: 50, ttl: CACHE_TTL });
|
|
17
|
-
let promptReadClock = 0;
|
|
18
|
-
let promptCacheEpoch = 0;
|
|
19
17
|
|
|
20
18
|
async function fetchConfigRecords(table, filter) {
|
|
21
19
|
const records = await getRecordByFilter(Config_ID, table, filter);
|
|
@@ -49,17 +47,11 @@ async function fetchPreset(presetId, version = null) {
|
|
|
49
47
|
async function fetchBasePrompt(promptId) {
|
|
50
48
|
const cacheKey = `prompt:${promptId}`;
|
|
51
49
|
const cached = promptCache.get(cacheKey);
|
|
52
|
-
if (cached) return cached
|
|
50
|
+
if (cached) return cached;
|
|
53
51
|
|
|
54
|
-
// A result may fill the cache unless a read started later already did, or the cache was cleared meanwhile.
|
|
55
|
-
const startedAt = ++promptReadClock;
|
|
56
|
-
const epoch = promptCacheEpoch;
|
|
57
52
|
const records = await getRecordByFilter(Config_ID, 'responses', `{prompt_id} = "${promptId}"`);
|
|
58
53
|
const record = records?.[0] || null;
|
|
59
|
-
|
|
60
|
-
if (record && epoch === promptCacheEpoch && !(current && current.startedAt > startedAt)) {
|
|
61
|
-
promptCache.set(cacheKey, { record, startedAt });
|
|
62
|
-
}
|
|
54
|
+
if (record) promptCache.set(cacheKey, record);
|
|
63
55
|
return record;
|
|
64
56
|
}
|
|
65
57
|
|
|
@@ -265,7 +257,6 @@ async function resolveTools({ promptId, assistant, presetToolIds = null, status
|
|
|
265
257
|
|
|
266
258
|
function clearCache() {
|
|
267
259
|
promptCache.clear();
|
|
268
|
-
promptCacheEpoch += 1;
|
|
269
260
|
snippetCache.clear();
|
|
270
261
|
toolCache.clear();
|
|
271
262
|
presetCache.clear();
|
package/lib/eval/EvalProvider.js
CHANGED
|
@@ -225,17 +225,21 @@ class EvalProvider {
|
|
|
225
225
|
const clinicalMessage = typeof this.provider._buildClinicalMessage === 'function'
|
|
226
226
|
? this.provider._buildClinicalMessage(promptVariables, interpolatedVariables)
|
|
227
227
|
: [];
|
|
228
|
-
const input = this.provider._sanitizeInput([...memoryMessage, ...clinicalMessage, ...convertedMessages]);
|
|
229
228
|
const isContextOnly = this.mode === 'context-only';
|
|
230
229
|
|
|
231
230
|
const shouldUsePromptRegistry = !presetId && this.provider.supportsPromptRegistry && !!assistantId;
|
|
231
|
+
const modelConfig = shouldUsePromptRegistry ? {} : this._modelConfig(presetModelConfig);
|
|
232
|
+
const input = this.provider._orderRuntimeMessages(
|
|
233
|
+
this.provider._sanitizeInput([...memoryMessage, ...clinicalMessage, ...convertedMessages]),
|
|
234
|
+
modelConfig,
|
|
235
|
+
);
|
|
232
236
|
|
|
233
237
|
return {
|
|
234
238
|
instructions: devContent || '',
|
|
235
239
|
input,
|
|
236
240
|
toolSchemas: isContextOnly ? [] : toolSchemas,
|
|
237
241
|
toolChoice: isContextOnly ? 'none' : this.toolChoice,
|
|
238
|
-
modelConfig
|
|
242
|
+
modelConfig,
|
|
239
243
|
assistant: isContextOnly ? null : assistant,
|
|
240
244
|
phiProcessor: this.phiProcessor,
|
|
241
245
|
promptVariables,
|