@peopl-health/nexus 5.67.0-dev.11849 → 5.67.0-dev.11889
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.
|
@@ -125,13 +125,13 @@ class BaseLLMProvider {
|
|
|
125
125
|
assistant.toolRuntimeContext.enabledSkills = enabledSkills;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
const resolvedSchemas = this._resolveToolSchemas({
|
|
128
|
+
const resolvedSchemas = this._scopeOutreachDecisionTool(this._resolveToolSchemas({
|
|
129
129
|
assistant, toolIds: resolvedToolIds, filtered, toolDescriptions,
|
|
130
|
-
});
|
|
130
|
+
}), isOutreach);
|
|
131
131
|
const { toolChoice: flooredToolChoice, toolSchemas, narrowed } = this._applyDeliveryFloor(
|
|
132
132
|
toolChoice, resolvedSchemas, Boolean(assistant?.toolRuntimeContext), turnKind);
|
|
133
133
|
const effectiveToolIds = toolSchemas.map(s => s.function?.name).filter(Boolean);
|
|
134
|
-
const instructionToolIds = narrowed ? effectiveToolIds : resolvedToolIds;
|
|
134
|
+
const instructionToolIds = narrowed || isOutreach ? effectiveToolIds : resolvedToolIds;
|
|
135
135
|
const requestedToolChoice = prePromptResult?.toolChoice;
|
|
136
136
|
const requestedToolName = requestedToolChoice?.name || requestedToolChoice?.function?.name || null;
|
|
137
137
|
const supportedGenericChoices = new Set(['auto', 'none', 'required', 'any']);
|
|
@@ -234,6 +234,17 @@ class BaseLLMProvider {
|
|
|
234
234
|
};
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
// The decision tool belongs to the turn kind, not to a preset: without it an outreach turn cannot
|
|
238
|
+
// close and the brief is dropped, and on any other turn it would invite a decision nobody asked for.
|
|
239
|
+
_scopeOutreachDecisionTool(schemas, isOutreach) {
|
|
240
|
+
const others = schemas.filter(s => s.function?.name !== OUTREACH_DECISION_TOOL);
|
|
241
|
+
if (!isOutreach) return others;
|
|
242
|
+
const decision = schemas.find(s => s.function?.name === OUTREACH_DECISION_TOOL)
|
|
243
|
+
|| getRegistrySchemas([OUTREACH_DECISION_TOOL])[0];
|
|
244
|
+
if (!decision) throw new Error('OUTREACH_DECISION_TOOL_UNAVAILABLE');
|
|
245
|
+
return [...others, decision];
|
|
246
|
+
}
|
|
247
|
+
|
|
237
248
|
_resolveToolSchemas({ assistant, toolIds, filtered, toolDescriptions }) {
|
|
238
249
|
const assistantSchemas = assistant?.getToolSchemas?.() || [];
|
|
239
250
|
const registrySchemas = getRegistrySchemas();
|
|
@@ -7,6 +7,7 @@ const NUTRITION_GUIDE_PROMPT_ID = 'nutrition_plan_guide';
|
|
|
7
7
|
const PLAN_MAX_CHARS = 1500;
|
|
8
8
|
const GUIDE_MAX_CHARS = 2000;
|
|
9
9
|
const ISO_DATE_RX = /^\d{4}-\d{2}-\d{2}$/;
|
|
10
|
+
const GUIDE_TIMEOUT_MS = 3000;
|
|
10
11
|
|
|
11
12
|
// Each line of a plan or guide is one kind of indication, so the breaks survive and only each line is flattened.
|
|
12
13
|
function keepLines(text, maxChars) {
|
|
@@ -30,14 +31,34 @@ function readPlanSection(section) {
|
|
|
30
31
|
return { text: typeof text === 'string' ? text : '', reviewBy };
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
let lastGuide = '';
|
|
35
|
+
let guideRead = null;
|
|
36
|
+
|
|
37
|
+
// One shared read; a failure keeps the last guide that was read successfully.
|
|
38
|
+
function readGuide() {
|
|
39
|
+
if (guideRead) return guideRead;
|
|
40
|
+
guideRead = composePrompt({ promptId: NUTRITION_GUIDE_PROMPT_ID })
|
|
41
|
+
.then(({ resolvedPrompt }) => {
|
|
42
|
+
lastGuide = keepLines(resolvedPrompt || '', GUIDE_MAX_CHARS);
|
|
43
|
+
return lastGuide;
|
|
44
|
+
})
|
|
45
|
+
.catch((error) => {
|
|
46
|
+
logger.warn('[nutritionPlanService] nutrition guide unreadable; using the last one read', { error: error.message });
|
|
47
|
+
return lastGuide;
|
|
48
|
+
})
|
|
49
|
+
.finally(() => { guideRead = null; });
|
|
50
|
+
return guideRead;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The guide only adds to the plan, so a slow read must not hold the patient's turn.
|
|
33
54
|
async function loadNutritionGuide() {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
55
|
+
let timer;
|
|
56
|
+
const timedOut = new Promise((resolve) => { timer = setTimeout(() => resolve(null), GUIDE_TIMEOUT_MS); });
|
|
57
|
+
const guide = await Promise.race([readGuide(), timedOut]);
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
if (guide !== null) return guide;
|
|
60
|
+
logger.warn('[nutritionPlanService] nutrition guide read timed out; using the last one read', { timeoutMs: GUIDE_TIMEOUT_MS });
|
|
61
|
+
return lastGuide;
|
|
41
62
|
}
|
|
42
63
|
|
|
43
64
|
async function resolveNutritionPlan(section, { now = new Date() } = {}) {
|
|
@@ -55,5 +76,7 @@ async function resolveNutritionPlan(section, { now = new Date() } = {}) {
|
|
|
55
76
|
|
|
56
77
|
module.exports = {
|
|
57
78
|
NUTRITION_GUIDE_PROMPT_ID,
|
|
79
|
+
GUIDE_TIMEOUT_MS,
|
|
58
80
|
resolveNutritionPlan,
|
|
81
|
+
_test: { reset: () => { lastGuide = ''; guideRead = null; } },
|
|
59
82
|
};
|
package/lib/eval/EvalProvider.js
CHANGED
|
@@ -75,7 +75,7 @@ class EvalProvider {
|
|
|
75
75
|
const { messages, promptVariables, lastUserMessage, contextSource } = await this._buildContext(numero, beforeCheckpoint);
|
|
76
76
|
|
|
77
77
|
const {
|
|
78
|
-
devContent, assistant, toolSchemas, resolvedPromptId, presetModelConfig, resolvedPresetId,
|
|
78
|
+
devContent, assistant, toolSchemas, resolvedPromptId, presetModelConfig, resolvedPresetId, interpolatedVariables,
|
|
79
79
|
} = await this._resolvePrompt(prompt, assistantId, thread, promptVariables);
|
|
80
80
|
|
|
81
81
|
const request = this._buildRequest({
|
|
@@ -83,6 +83,7 @@ class EvalProvider {
|
|
|
83
83
|
assistantId: resolvedPromptId || assistantId,
|
|
84
84
|
presetId: resolvedPresetId,
|
|
85
85
|
presetModelConfig,
|
|
86
|
+
interpolatedVariables,
|
|
86
87
|
});
|
|
87
88
|
|
|
88
89
|
const startTime = Date.now();
|
|
@@ -133,6 +134,7 @@ class EvalProvider {
|
|
|
133
134
|
|
|
134
135
|
async _resolvePrompt(fallbackPrompt, assistantId, thread, promptVariables) {
|
|
135
136
|
let devContent;
|
|
137
|
+
let interpolatedVariables = [];
|
|
136
138
|
let resolvedPromptId = assistantId;
|
|
137
139
|
let presetToolIds = null;
|
|
138
140
|
let presetModelConfig = null;
|
|
@@ -143,18 +145,21 @@ class EvalProvider {
|
|
|
143
145
|
if (this.promptSource === 'airtable' && (presetId || assistantId)) {
|
|
144
146
|
const {
|
|
145
147
|
resolvedPrompt, promptId, presetToolIds: toolIds, modelConfig, presetId: composedPresetId,
|
|
148
|
+
interpolatedVariables: interpolated,
|
|
146
149
|
} = await composePrompt({
|
|
147
150
|
presetId,
|
|
148
151
|
promptId: assistantId,
|
|
149
152
|
variables: promptVariables,
|
|
150
153
|
});
|
|
151
154
|
devContent = resolvedPrompt;
|
|
155
|
+
interpolatedVariables = interpolated || [];
|
|
152
156
|
resolvedPromptId = promptId || assistantId;
|
|
153
157
|
presetToolIds = toolIds;
|
|
154
158
|
presetModelConfig = modelConfig || null;
|
|
155
159
|
resolvedPresetId = composedPresetId || resolvedPresetId;
|
|
156
160
|
} else {
|
|
157
161
|
devContent = fallbackPrompt;
|
|
162
|
+
interpolatedVariables = [...new Set([...(devContent || '').matchAll(/\{\{(\w+)\}\}/g)].map((m) => m[1]))];
|
|
158
163
|
devContent = devContent.replace(/\{\{(\w+)\}\}/g, (_, key) => promptVariables[key] ?? '');
|
|
159
164
|
}
|
|
160
165
|
|
|
@@ -191,7 +196,7 @@ class EvalProvider {
|
|
|
191
196
|
}
|
|
192
197
|
}
|
|
193
198
|
|
|
194
|
-
return { devContent, assistant, toolSchemas, resolvedPromptId, presetModelConfig, resolvedPresetId };
|
|
199
|
+
return { devContent, assistant, toolSchemas, resolvedPromptId, presetModelConfig, resolvedPresetId, interpolatedVariables };
|
|
195
200
|
}
|
|
196
201
|
|
|
197
202
|
_modelConfig(presetModelConfig) {
|
|
@@ -204,7 +209,7 @@ class EvalProvider {
|
|
|
204
209
|
|
|
205
210
|
_buildRequest({
|
|
206
211
|
devContent, messages, promptVariables, toolSchemas, assistant, assistantId, presetId,
|
|
207
|
-
presetModelConfig = null,
|
|
212
|
+
presetModelConfig = null, interpolatedVariables = [],
|
|
208
213
|
}) {
|
|
209
214
|
const convertedMessages = this.provider._convertItemsToApiFormat(messages);
|
|
210
215
|
|
|
@@ -215,7 +220,11 @@ class EvalProvider {
|
|
|
215
220
|
? [{ role: 'developer', content: `Usa la siguiente información que conoces del paciente para personalizar tu respuesta. Adapta tu tono, contenido o recomendaciones según estas preferencias y contexto cuando sea relevante. No menciones que tienes esta información almacenada.\n\n${memoryParts.join('\n\n')}` }]
|
|
216
221
|
: [];
|
|
217
222
|
|
|
218
|
-
|
|
223
|
+
// The same runtime clinical block a live turn gets (context, symptoms, nutrition plan), unless the prompt interpolates it.
|
|
224
|
+
const clinicalMessage = typeof this.provider._buildClinicalMessage === 'function'
|
|
225
|
+
? this.provider._buildClinicalMessage(promptVariables, interpolatedVariables)
|
|
226
|
+
: [];
|
|
227
|
+
const input = this.provider._sanitizeInput([...memoryMessage, ...clinicalMessage, ...convertedMessages]);
|
|
219
228
|
const isContextOnly = this.mode === 'context-only';
|
|
220
229
|
|
|
221
230
|
const shouldUsePromptRegistry = !presetId && this.provider.supportsPromptRegistry && !!assistantId;
|