@peopl-health/nexus 5.9.0-dev.921 → 6.0.0-dev.611
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/AssistantProcessor.js +5 -34
- package/lib/clinical/config/divergenceConfig.js +1 -18
- package/lib/clinical/config/llmConfig.js +1 -9
- package/lib/clinical/context/ToolRuntimeContext.js +2 -1
- package/lib/clinical/helpers/divergenceHelper.js +0 -12
- package/lib/clinical/index.js +0 -2
- package/lib/clinical/memory/DefaultMemoryManager.js +9 -19
- package/lib/clinical/models/turnTraceModel.js +5 -18
- package/lib/clinical/providers/AnthropicProvider.js +29 -291
- package/lib/clinical/providers/BaseLLMProvider.js +24 -113
- package/lib/clinical/providers/GatewayProvider.js +3 -3
- package/lib/clinical/providers/OpenAIResponsesProvider.js +4 -46
- package/lib/clinical/services/divergenceService.js +5 -22
- package/lib/clinical/services/promptComposerService.js +15 -46
- package/lib/clinical/services/shadowMessageService.js +12 -28
- package/lib/clinical/services/shadowService.js +3 -12
- package/lib/clinical/stores/CtcaeCatalog.js +0 -20
- package/lib/config/airtableConfig.js +0 -3
- package/lib/core/BatchingManager.js +3 -10
- package/lib/core/NexusMessaging.js +13 -31
- package/lib/eval/EvalProvider.js +2 -3
- package/lib/fhir/config/fhirConfig.js +3 -34
- package/lib/fhir/constants/projectionSlugs.js +1 -41
- package/lib/fhir/helpers/elementHelper.js +0 -21
- package/lib/fhir/helpers/fhirHelper.js +2 -5
- package/lib/fhir/index.js +0 -24
- package/lib/fhir/models/envelopeModel.js +0 -5
- package/lib/fhir/projections/registerProjectors.js +0 -16
- package/lib/fhir/resources/Condition.js +2 -107
- package/lib/fhir/resources/Observation.js +1 -73
- package/lib/fhir/resources/Procedure.js +2 -34
- package/lib/fhir/resources/Provenance.js +2 -93
- package/lib/fhir/services/fhirService.js +2 -2
- package/lib/helpers/twilioHelper.js +1 -9
- package/lib/index.d.ts +0 -16
- package/lib/index.js +0 -9
- package/lib/models/messageModel.js +0 -6
- package/lib/routes/index.js +1 -21
- package/lib/services/airtableService.js +1 -2
- package/lib/shared/dtos/ContingencySafetyNet.js +3 -10
- package/lib/shared/dtos/DispatchedEscalation.js +8 -3
- package/lib/shared/dtos/Reminder.js +2 -2
- package/lib/storage/MongoStorage.js +0 -2
- package/package.json +1 -1
- package/lib/clinical/config/cohortConfig.js +0 -68
- package/lib/clinical/config/composerConfig.js +0 -42
- package/lib/clinical/config/subAgentsConfig.js +0 -44
- package/lib/clinical/flags/bridgeConfig.js +0 -14
- package/lib/clinical/flags/composerFlags.js +0 -14
- package/lib/clinical/helpers/clinicalFactsHelper.js +0 -108
- package/lib/clinical/helpers/clinicalMentionHelper.js +0 -68
- package/lib/clinical/helpers/cohortHelper.js +0 -22
- package/lib/clinical/helpers/gradeEstimateHelper.js +0 -29
- package/lib/clinical/helpers/resultsHelper.js +0 -34
- package/lib/clinical/helpers/safetyFlagsHelper.js +0 -10
- package/lib/clinical/services/bridgeService.js +0 -55
- package/lib/clinical/services/clinicalAirtableService.js +0 -162
- package/lib/clinical/services/clinicalExtractionService.js +0 -112
- package/lib/clinical/services/composerService.js +0 -59
- package/lib/clinical/services/dispatcherService.js +0 -137
- package/lib/clinical/services/reminderDispatchService.js +0 -36
- package/lib/clinical/services/skillService.js +0 -44
- package/lib/clinical/tools/analyzeSymptomPatternsTool.js +0 -279
- package/lib/clinical/tools/deliverPatientMessageTool.js +0 -202
- package/lib/clinical/tools/extractClinicalInfoTool.js +0 -167
- package/lib/clinical/tools/getActiveSymptomLandscapeTool.js +0 -141
- package/lib/clinical/tools/getPatientHistoryTool.js +0 -235
- package/lib/clinical/tools/getPatientRiskProfileTool.js +0 -71
- package/lib/clinical/tools/getRouterContextBundleTool.js +0 -93
- package/lib/clinical/tools/lookupCtcaeEntryTool.js +0 -35
- package/lib/clinical/tools/openClusterHypothesisTool.js +0 -160
- package/lib/clinical/tools/openConditionTool.js +0 -136
- package/lib/clinical/tools/recordClinicalImpressionTool.js +0 -251
- package/lib/clinical/tools/recordInterventionTool.js +0 -140
- package/lib/clinical/tools/registerClinicalTools.js +0 -39
- package/lib/clinical/tools/reportCrisisTool.js +0 -211
- package/lib/clinical/tools/reportMedicalEscalationTool.js +0 -187
- package/lib/clinical/tools/reportResultsReceivedTool.js +0 -153
- package/lib/clinical/tools/reportUnresolvedRequestv2Tool.js +0 -177
- package/lib/clinical/tools/schedulePatientReminderTool.js +0 -149
- package/lib/clinical/tools/setContingencyPlanTool.js +0 -176
- package/lib/clinical/tools/skillTool.js +0 -67
- package/lib/clinical/tools/submitRoutingDecisionTool.js +0 -291
- package/lib/clinical/tools/updateClusterHypothesisTool.js +0 -159
- package/lib/controllers/cohortController.js +0 -16
- package/lib/controllers/traceController.js +0 -26
- package/lib/core/workflowRunner.js +0 -35
- package/lib/fhir/config/fhirTablesConfig.js +0 -9
- package/lib/fhir/constants/severityPriority.js +0 -8
- package/lib/fhir/helpers/compositeHelper.js +0 -88
- package/lib/fhir/helpers/fhirReadHelper.js +0 -42
- package/lib/fhir/helpers/terminologyHelper.js +0 -260
- package/lib/fhir/projections/clusterProjection.js +0 -24
- package/lib/fhir/projections/contingencyProjection.js +0 -36
- package/lib/fhir/projections/dispatchedEscalationProjection.js +0 -25
- package/lib/fhir/projections/interventionProjection.js +0 -42
- package/lib/fhir/projections/proctcaeProjection.js +0 -179
- package/lib/fhir/projections/reminderProjection.js +0 -20
- package/lib/fhir/projections/riskProjection.js +0 -16
- package/lib/fhir/projections/routingProjection.js +0 -48
- package/lib/fhir/projections/symptomCaseProjection.js +0 -65
- package/lib/fhir/resources/CarePlan.js +0 -93
- package/lib/fhir/resources/ClinicalImpression.js +0 -118
- package/lib/fhir/resources/Communication.js +0 -65
- package/lib/fhir/resources/CommunicationRequest.js +0 -182
- package/lib/fhir/resources/Device.js +0 -24
- package/lib/fhir/resources/Patient.js +0 -18
- package/lib/fhir/resources/Questionnaire.js +0 -23
- package/lib/fhir/resources/QuestionnaireResponse.js +0 -29
- package/lib/fhir/resources/RequestOrchestration.js +0 -46
- package/lib/fhir/resources/RiskAssessment.js +0 -99
- package/lib/fhir/resources/Task.js +0 -106
- package/lib/fhir/resources/interventionExtensions.js +0 -28
- package/lib/fhir/services/clusterService.js +0 -77
- package/lib/fhir/services/contingencyService.js +0 -117
- package/lib/fhir/services/interventionService.js +0 -149
- package/lib/fhir/services/resultObservationService.js +0 -52
- package/lib/fhir/services/riskService.js +0 -101
- package/lib/fhir/services/routingService.js +0 -147
- package/lib/fhir/services/snapshotService.js +0 -28
- package/lib/fhir/services/symptomCaseService.js +0 -139
- package/lib/fhir/services/terminologyService.js +0 -58
- package/lib/fhir/services/triageService.js +0 -57
- package/lib/fhir/utils/normalizeUtils.js +0 -33
- package/lib/shared/dtos/GradedSymptom.js +0 -23
- package/lib/shared/dtos/TriageResponse.js +0 -35
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const { DefaultMemoryManager } = require('../memory/DefaultMemoryManager');
|
|
2
2
|
const { composePrompt, resolveTools } = require('../services/promptComposerService');
|
|
3
|
-
const { resolveEnabledSkills } = require('../services/skillService');
|
|
4
3
|
const runtimeConfig = require('../../config/runtimeConfig');
|
|
5
4
|
const { logger } = require('../../utils/logger');
|
|
6
5
|
const { getCurrentMexicoDateTime } = require('../../utils/dateUtils');
|
|
@@ -13,10 +12,6 @@ const { logBugReportToAirtable } = require('../../controllers/bugReportControlle
|
|
|
13
12
|
|
|
14
13
|
const DEFAULT_MAX_CONVERSATION_RETRIES = 3;
|
|
15
14
|
const CONVERSATION_CONTINUATION = '(continuar)';
|
|
16
|
-
const NEGATIVE_GUIDANCE_MARKER = '**When NOT to call';
|
|
17
|
-
const TERMINAL_DELIVERY_TOOLS = new Set(['DeliverPatientMessage']);
|
|
18
|
-
const DELIVERY_NUDGE = 'sistema: el turno no puede cerrar sin un mensaje para el paciente. Llama ahora a DeliverPatientMessage con message_text en español. No repitas herramientas clínicas ya ejecutadas en este turno.';
|
|
19
|
-
const MAX_DELIVERY_NUDGES = 1;
|
|
20
15
|
|
|
21
16
|
class BaseLLMProvider {
|
|
22
17
|
constructor(options = {}) {
|
|
@@ -41,13 +36,12 @@ class BaseLLMProvider {
|
|
|
41
36
|
instructions = null, additionalInstructions = null, metadata = {},
|
|
42
37
|
assistant, promptVersion = null, promptVariables = null,
|
|
43
38
|
toolChoice = 'auto', prePromptResult = null, phiProcessor = null,
|
|
44
|
-
shadow = false, shouldContinue = null
|
|
39
|
+
shadow = false, shouldContinue = null
|
|
45
40
|
} = config;
|
|
46
41
|
const trace = assistant?.toolRuntimeContext?.trace || null;
|
|
47
42
|
|
|
48
43
|
const {
|
|
49
|
-
resolvedPrompt,
|
|
50
|
-
presetToolIds, presetSkillIds,
|
|
44
|
+
resolvedPrompt, snippetIds, promptId: resolvedPromptId, presetToolIds,
|
|
51
45
|
presetId: resolvedPresetId, presetVersion: resolvedPresetVersion, modelConfig,
|
|
52
46
|
} = await composePrompt({ presetId, presetVersion, promptId: assistantId, variables: promptVariables });
|
|
53
47
|
|
|
@@ -55,18 +49,9 @@ class BaseLLMProvider {
|
|
|
55
49
|
promptId: resolvedPromptId || assistantId, assistant, presetToolIds,
|
|
56
50
|
});
|
|
57
51
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const resolvedSchemas = this._resolveToolSchemas({ assistant, toolIds, filtered, toolDescriptions });
|
|
63
|
-
const { toolChoice: flooredToolChoice, toolSchemas, narrowed } = this._applyDeliveryFloor(toolChoice, resolvedSchemas, prePromptResult?.toolChoice);
|
|
64
|
-
const effectiveToolIds = narrowed ? toolSchemas.map(s => s.function?.name).filter(Boolean) : toolIds;
|
|
65
|
-
const devContent = this._buildInstructions({ resolvedPrompt, toolSchemas, prePromptResult });
|
|
66
|
-
const auditedPrompt = additionalInstructions
|
|
67
|
-
? `${devContent}\n\n${this._operatorInstructionTurn(additionalInstructions, modelConfig).content}`
|
|
68
|
-
: devContent;
|
|
69
|
-
const input = this._buildInput({ context, prePromptResult, additionalInstructions, promptVariables, modelConfig, interpolatedVariables });
|
|
52
|
+
const toolSchemas = this._resolveToolSchemas({ assistant, toolIds, filtered, toolDescriptions });
|
|
53
|
+
const devContent = this._buildInstructions({ resolvedPrompt, toolSchemas, prePromptResult, additionalInstructions });
|
|
54
|
+
const input = this._buildInput({ context, prePromptResult, additionalInstructions, promptVariables, modelConfig });
|
|
70
55
|
|
|
71
56
|
// Capture before _invokeModel: the divergence oracle replays this shadow request shape.
|
|
72
57
|
if (shadow && assistant?.toolRuntimeContext) {
|
|
@@ -79,19 +64,19 @@ class BaseLLMProvider {
|
|
|
79
64
|
},
|
|
80
65
|
input,
|
|
81
66
|
model: modelConfig?.model || null,
|
|
82
|
-
toolIds: Array.isArray(
|
|
67
|
+
toolIds: Array.isArray(toolIds) ? toolIds : [],
|
|
83
68
|
additionalInstructions: additionalInstructions || null,
|
|
84
69
|
};
|
|
85
70
|
}
|
|
86
71
|
|
|
87
|
-
trace?.setPrompt({ resolved:
|
|
72
|
+
trace?.setPrompt({ resolved: devContent, resolvedPresetId, resolvedPresetVersion });
|
|
88
73
|
|
|
89
74
|
const { response, toolsExecuted, retries, usage } = await this._invokeModel({
|
|
90
75
|
instructions: instructions || devContent || '',
|
|
91
76
|
input,
|
|
92
77
|
toolSchemas,
|
|
93
|
-
toolChoice: prePromptResult?.toolChoice ||
|
|
94
|
-
followUpToolChoice:
|
|
78
|
+
toolChoice: prePromptResult?.toolChoice || toolChoice,
|
|
79
|
+
followUpToolChoice: toolChoice,
|
|
95
80
|
modelConfig,
|
|
96
81
|
metadata,
|
|
97
82
|
assistant,
|
|
@@ -103,7 +88,6 @@ class BaseLLMProvider {
|
|
|
103
88
|
trace,
|
|
104
89
|
shadow,
|
|
105
90
|
shouldContinue,
|
|
106
|
-
onFirstIteration,
|
|
107
91
|
});
|
|
108
92
|
|
|
109
93
|
return {
|
|
@@ -114,9 +98,9 @@ class BaseLLMProvider {
|
|
|
114
98
|
tools_executed: toolsExecuted,
|
|
115
99
|
retries,
|
|
116
100
|
usage,
|
|
117
|
-
resolved_prompt:
|
|
101
|
+
resolved_prompt: devContent,
|
|
118
102
|
snippet_ids: snippetIds,
|
|
119
|
-
tool_ids:
|
|
103
|
+
tool_ids: toolIds,
|
|
120
104
|
preset_id: resolvedPresetId,
|
|
121
105
|
preset_version: resolvedPresetVersion,
|
|
122
106
|
preset: resolvedPresetId ? { id: resolvedPresetId, version: resolvedPresetVersion } : null,
|
|
@@ -140,17 +124,13 @@ class BaseLLMProvider {
|
|
|
140
124
|
return activeToolSchemas.map(s => {
|
|
141
125
|
const name = s.function?.name;
|
|
142
126
|
if (name && toolDescriptions[name]) {
|
|
143
|
-
if (s.function.description?.includes(NEGATIVE_GUIDANCE_MARKER)
|
|
144
|
-
&& !toolDescriptions[name].includes(NEGATIVE_GUIDANCE_MARKER)) {
|
|
145
|
-
logger.warn('[BaseLLMProvider] Airtable description drops the when-NOT-to-call guidance written in code', { tool: name });
|
|
146
|
-
}
|
|
147
127
|
return { ...s, function: { ...s.function, description: toolDescriptions[name] } };
|
|
148
128
|
}
|
|
149
129
|
return s;
|
|
150
130
|
});
|
|
151
131
|
}
|
|
152
132
|
|
|
153
|
-
_buildInstructions({ resolvedPrompt, toolSchemas, prePromptResult }) {
|
|
133
|
+
_buildInstructions({ resolvedPrompt, toolSchemas, prePromptResult, additionalInstructions }) {
|
|
154
134
|
let devContent = resolvedPrompt;
|
|
155
135
|
if (toolSchemas.length > 0) {
|
|
156
136
|
const toolNames = toolSchemas.map(s => s.function?.name).join(', ');
|
|
@@ -159,14 +139,21 @@ class BaseLLMProvider {
|
|
|
159
139
|
if (prePromptResult?.additionalInstructions) {
|
|
160
140
|
devContent += `\n\n${prePromptResult.additionalInstructions}`;
|
|
161
141
|
}
|
|
142
|
+
if (additionalInstructions) {
|
|
143
|
+
devContent += this._operatorInstructionSuffix(additionalInstructions);
|
|
144
|
+
}
|
|
162
145
|
return devContent;
|
|
163
146
|
}
|
|
164
147
|
|
|
165
|
-
|
|
166
|
-
return
|
|
148
|
+
_operatorInstructionSuffix(additionalInstructions) {
|
|
149
|
+
return `\n\n[DIRECTIVA DEL OPERADOR (prioritaria) para tu próxima respuesta al paciente]: ${additionalInstructions}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
_operatorInstructionTurn() {
|
|
153
|
+
return { role: 'user', content: CONVERSATION_CONTINUATION };
|
|
167
154
|
}
|
|
168
155
|
|
|
169
|
-
_buildInput({ context, prePromptResult, additionalInstructions, promptVariables, modelConfig = null
|
|
156
|
+
_buildInput({ context, prePromptResult, additionalInstructions, promptVariables, modelConfig = null }) {
|
|
170
157
|
let messages = this._convertItemsToApiFormat(context || []);
|
|
171
158
|
if (Array.isArray(prePromptResult?.additionalMessages) && prePromptResult.additionalMessages.length > 0) {
|
|
172
159
|
messages = [...messages, ...this._convertItemsToApiFormat(prePromptResult.additionalMessages)];
|
|
@@ -174,35 +161,7 @@ class BaseLLMProvider {
|
|
|
174
161
|
if (additionalInstructions) {
|
|
175
162
|
messages = [...messages, this._operatorInstructionTurn(additionalInstructions, modelConfig)];
|
|
176
163
|
}
|
|
177
|
-
return this._sanitizeInput([
|
|
178
|
-
...this._buildMemoryMessage(promptVariables),
|
|
179
|
-
...this._buildClinicalMessage(promptVariables, interpolatedVariables),
|
|
180
|
-
...this._buildClockMessage(promptVariables),
|
|
181
|
-
...messages,
|
|
182
|
-
]);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
_buildClinicalMessage(promptVariables, interpolatedVariables = []) {
|
|
186
|
-
const inPrompt = new Set(interpolatedVariables);
|
|
187
|
-
const context = this._clinicalText(promptVariables?.clinical_context, 'clinical_context');
|
|
188
|
-
const symptoms = this._clinicalText(promptVariables?.last_symptoms, 'last_symptoms');
|
|
189
|
-
const parts = [];
|
|
190
|
-
if (context && !inPrompt.has('clinical_context')) {
|
|
191
|
-
parts.push(`## Contexto clínico del paciente\n${context}`);
|
|
192
|
-
}
|
|
193
|
-
if (symptoms && !inPrompt.has('last_symptoms')) {
|
|
194
|
-
parts.push(`## Síntomas recientes\n${symptoms}`);
|
|
195
|
-
}
|
|
196
|
-
if (parts.length === 0) return [];
|
|
197
|
-
return [{ role: 'developer', content: parts.join('\n\n') }];
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
_clinicalText(value, field) {
|
|
201
|
-
if (typeof value === 'string') return value.trim();
|
|
202
|
-
if (value != null) {
|
|
203
|
-
logger.warn('[BaseLLMProvider] Non-string clinical variable, omitting from runtime context', { field, type: typeof value });
|
|
204
|
-
}
|
|
205
|
-
return '';
|
|
164
|
+
return this._sanitizeInput([...this._buildMemoryMessage(promptVariables), ...messages]);
|
|
206
165
|
}
|
|
207
166
|
|
|
208
167
|
_buildMemoryMessage(promptVariables) {
|
|
@@ -213,14 +172,6 @@ class BaseLLMProvider {
|
|
|
213
172
|
return [{ 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')}` }];
|
|
214
173
|
}
|
|
215
174
|
|
|
216
|
-
// Delivers the clock after the breakpoint, so a minute-granular date stops rewriting the cached
|
|
217
|
-
// prefix. Off until {{current_date}} leaves the prompt body, or the date would be sent twice.
|
|
218
|
-
_buildClockMessage(promptVariables) {
|
|
219
|
-
if (runtimeConfig.get('CLOCK_OUTSIDE_PROMPT') !== 'true') return [];
|
|
220
|
-
if (!promptVariables?.current_date) return [];
|
|
221
|
-
return [{ role: 'developer', content: `Fecha y hora actual: ${promptVariables.current_date}` }];
|
|
222
|
-
}
|
|
223
|
-
|
|
224
175
|
async _invokeModel() {
|
|
225
176
|
throw new Error(`${this.constructor.name} must implement _invokeModel()`);
|
|
226
177
|
}
|
|
@@ -316,9 +267,7 @@ class BaseLLMProvider {
|
|
|
316
267
|
logger.info('[runConversation] Attempt', { attempt, maxRetries, threadId, assistantId });
|
|
317
268
|
|
|
318
269
|
const result = await this._executeConversation(config);
|
|
319
|
-
|
|
320
|
-
const delivered = this._extractDeliveredMessage(config, result);
|
|
321
|
-
const extractedOutput = delivered?.trim() ? delivered : this._extractMessageOutput(result);
|
|
270
|
+
const extractedOutput = this._extractMessageOutput(result);
|
|
322
271
|
|
|
323
272
|
if (extractedOutput?.trim()) {
|
|
324
273
|
result.output_text = extractedOutput;
|
|
@@ -352,41 +301,6 @@ class BaseLLMProvider {
|
|
|
352
301
|
}
|
|
353
302
|
}
|
|
354
303
|
|
|
355
|
-
_toolSucceeded(t) {
|
|
356
|
-
// The executor sets success=true unless the call threw; a tool can also fail by returning {success:false} in its output.
|
|
357
|
-
if (!t || t.success === false) return false;
|
|
358
|
-
if (typeof t.tool_output === 'string') {
|
|
359
|
-
try { if (JSON.parse(t.tool_output)?.success === false) return false; } catch { /* non-JSON output */ }
|
|
360
|
-
}
|
|
361
|
-
return true;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
_deliveryCommitted(toolsExecuted) {
|
|
365
|
-
return (toolsExecuted || []).some(t => TERMINAL_DELIVERY_TOOLS.has(t?.tool_name) && this._toolSucceeded(t));
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// On a preset whose only patient-visible output path is a delivery tool, 'none' leaves the turn nothing it may
|
|
369
|
-
// emit, so it retries on empty output until it throws. Read it as "only the delivery tool" instead — the
|
|
370
|
-
// side-effect guarantee the caller wanted survives the narrowing.
|
|
371
|
-
// Skipped when a pre-prompt hook forced a choice: that choice may name a tool the narrowing would drop.
|
|
372
|
-
_applyDeliveryFloor(toolChoice, toolSchemas, prePromptToolChoice = null) {
|
|
373
|
-
if (prePromptToolChoice) return { toolChoice, toolSchemas, narrowed: false };
|
|
374
|
-
if (toolChoice !== 'none') return { toolChoice, toolSchemas, narrowed: false };
|
|
375
|
-
const deliveryOnly = toolSchemas.filter(s => TERMINAL_DELIVERY_TOOLS.has(s.function?.name));
|
|
376
|
-
if (!deliveryOnly.length) return { toolChoice, toolSchemas, narrowed: false };
|
|
377
|
-
return { toolChoice: 'auto', toolSchemas: deliveryOnly, narrowed: true };
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
_extractDeliveredMessage(config, result) {
|
|
381
|
-
const executed = Array.isArray(result?.tools_executed) ? result.tools_executed : [];
|
|
382
|
-
// Only count a delivery from THIS run, so a stale/prior trace.outbound can't leak in.
|
|
383
|
-
const delivered = executed.some(t => t?.tool_name === 'DeliverPatientMessage' && this._toolSucceeded(t));
|
|
384
|
-
if (!delivered) return '';
|
|
385
|
-
// trace.outbound is the decoded/rendered outbound the handler wrote; message_text (raw tool arg) may be PHI-tokenized, so it is not used.
|
|
386
|
-
const traceOutbound = config?.assistant?.toolRuntimeContext?.trace?.outbound;
|
|
387
|
-
return (typeof traceOutbound === 'string' && traceOutbound.trim()) ? traceOutbound : '';
|
|
388
|
-
}
|
|
389
|
-
|
|
390
304
|
_normalizeThread(thread) {
|
|
391
305
|
return {
|
|
392
306
|
conversationId: thread.conversation_id || thread.getConversationId?.(),
|
|
@@ -523,7 +437,4 @@ class BaseLLMProvider {
|
|
|
523
437
|
module.exports = {
|
|
524
438
|
BaseLLMProvider,
|
|
525
439
|
CONVERSATION_CONTINUATION,
|
|
526
|
-
DELIVERY_NUDGE,
|
|
527
|
-
MAX_DELIVERY_NUDGES,
|
|
528
|
-
TERMINAL_DELIVERY_TOOLS,
|
|
529
440
|
};
|
|
@@ -74,10 +74,10 @@ class GatewayProvider extends BaseLLMProvider {
|
|
|
74
74
|
model = composed.modelConfig?.model || null;
|
|
75
75
|
}
|
|
76
76
|
const { vendor, model: routedModel } = this._route(model);
|
|
77
|
-
if (vendor !== 'openai'
|
|
78
|
-
throw new Error(`runStructured is only supported for OpenAI
|
|
77
|
+
if (vendor !== 'openai') {
|
|
78
|
+
throw new Error(`runStructured is only supported for OpenAI models; routed model '${model}' resolved to '${vendor}'`);
|
|
79
79
|
}
|
|
80
|
-
return this._sub(
|
|
80
|
+
return this._sub('openai').runStructured({ ...params, model: routedModel });
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -7,23 +7,10 @@ const { safeParse } = require('../../utils/jsonUtils');
|
|
|
7
7
|
|
|
8
8
|
const { logger } = require('../../utils/logger');
|
|
9
9
|
|
|
10
|
-
const runtimeConfig = require('../../config/runtimeConfig');
|
|
11
|
-
|
|
12
10
|
const { BaseLLMProvider } = require('./BaseLLMProvider');
|
|
13
11
|
const { handleFunctionCalls } = require('./OpenAIResponsesProviderTools');
|
|
14
12
|
|
|
15
|
-
const DEFAULT_MAX_FUNCTION_ROUNDS =
|
|
16
|
-
|
|
17
|
-
function resolveMaxFunctionRounds(explicit) {
|
|
18
|
-
if (Number.isInteger(explicit) && explicit > 0) return explicit;
|
|
19
|
-
const raw = runtimeConfig.get('MAX_FUNCTION_ROUNDS');
|
|
20
|
-
const n = parseInt(raw, 10);
|
|
21
|
-
if (n > 0) return n;
|
|
22
|
-
if (raw !== undefined) {
|
|
23
|
-
logger.warn('[OpenAIResponsesProvider] Invalid MAX_FUNCTION_ROUNDS, falling back to default', { value: raw, default: DEFAULT_MAX_FUNCTION_ROUNDS });
|
|
24
|
-
}
|
|
25
|
-
return DEFAULT_MAX_FUNCTION_ROUNDS;
|
|
26
|
-
}
|
|
13
|
+
const DEFAULT_MAX_FUNCTION_ROUNDS = 5;
|
|
27
14
|
|
|
28
15
|
class OpenAIResponsesProvider extends BaseLLMProvider {
|
|
29
16
|
constructor(options = {}) {
|
|
@@ -33,7 +20,7 @@ class OpenAIResponsesProvider extends BaseLLMProvider {
|
|
|
33
20
|
organization,
|
|
34
21
|
client,
|
|
35
22
|
defaultModels = {},
|
|
36
|
-
maxFunctionRounds,
|
|
23
|
+
maxFunctionRounds = DEFAULT_MAX_FUNCTION_ROUNDS,
|
|
37
24
|
} = options;
|
|
38
25
|
|
|
39
26
|
if (!client && !apiKey) {
|
|
@@ -41,7 +28,7 @@ class OpenAIResponsesProvider extends BaseLLMProvider {
|
|
|
41
28
|
}
|
|
42
29
|
|
|
43
30
|
this.client = client || new OpenAI({ apiKey, organization });
|
|
44
|
-
this.maxFunctionRounds =
|
|
31
|
+
this.maxFunctionRounds = maxFunctionRounds;
|
|
45
32
|
this.defaults = {
|
|
46
33
|
responseModel: 'gpt-5',
|
|
47
34
|
chatModel: 'gpt-4o-mini',
|
|
@@ -66,7 +53,7 @@ class OpenAIResponsesProvider extends BaseLLMProvider {
|
|
|
66
53
|
instructions, input, toolSchemas = [], toolChoice = 'auto', followUpToolChoice = 'auto',
|
|
67
54
|
modelConfig, metadata = {}, assistant, phiProcessor = null,
|
|
68
55
|
promptVariables = null, promptVersion = null, resolvedPromptId = null, resolvedPresetId = null, trace = null,
|
|
69
|
-
shadow = false, shouldContinue = null
|
|
56
|
+
shadow = false, shouldContinue = null
|
|
70
57
|
}, options = {}) {
|
|
71
58
|
const executeTools = options.toolExecutor || handleFunctionCalls;
|
|
72
59
|
const shadowSuperseded = () => shadow && !shouldContinue();
|
|
@@ -116,23 +103,6 @@ class OpenAIResponsesProvider extends BaseLLMProvider {
|
|
|
116
103
|
let producingIteration = trace?.addIteration({ response, durationMs, apiCallConfig }) || null;
|
|
117
104
|
let finalResponse = response;
|
|
118
105
|
|
|
119
|
-
let bridgeNote = null;
|
|
120
|
-
if (onFirstIteration) {
|
|
121
|
-
const narrationText = (response.output || [])
|
|
122
|
-
.filter(item => item.type === 'message')
|
|
123
|
-
.map(item => this._messageItemToText(item))
|
|
124
|
-
.filter(Boolean)
|
|
125
|
-
.join(' ')
|
|
126
|
-
.trim();
|
|
127
|
-
const hasToolCalls = Boolean(assistant) && (response.output || []).some(item => item.type === 'function_call');
|
|
128
|
-
try {
|
|
129
|
-
const decision = onFirstIteration({ narrationText, hasToolCalls });
|
|
130
|
-
if (decision?.sent && decision.dedupNote) bridgeNote = decision.dedupNote;
|
|
131
|
-
} catch (err) {
|
|
132
|
-
logger.warn('[OpenAIResponsesProvider] bridge callback failed', { error: err.message });
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
106
|
if (assistant && response.output) {
|
|
137
107
|
let currentInput = [...input];
|
|
138
108
|
|
|
@@ -150,10 +120,6 @@ class OpenAIResponsesProvider extends BaseLLMProvider {
|
|
|
150
120
|
const { outputs, toolsExecuted } = await executeTools(functionCalls, assistant, phiProcessor, producingIteration);
|
|
151
121
|
|
|
152
122
|
currentInput.push(...finalResponse.output, ...outputs);
|
|
153
|
-
if (bridgeNote) {
|
|
154
|
-
currentInput.push({ role: 'developer', content: bridgeNote });
|
|
155
|
-
bridgeNote = null;
|
|
156
|
-
}
|
|
157
123
|
allToolsExecuted.push(...toolsExecuted);
|
|
158
124
|
|
|
159
125
|
const { result: { result: followUp, retries: followUpRetries }, durationMs: followUpDurationMs } = await withTiming(() => makeAPICall(currentInput));
|
|
@@ -162,14 +128,6 @@ class OpenAIResponsesProvider extends BaseLLMProvider {
|
|
|
162
128
|
producingIteration = trace?.addIteration({ response: followUp, durationMs: followUpDurationMs, apiCallConfig }) || null;
|
|
163
129
|
finalResponse = followUp;
|
|
164
130
|
}
|
|
165
|
-
|
|
166
|
-
// Budget exhausted mid-work: the loop stopped while the model still wanted tools, so the reply may be empty. Make it
|
|
167
|
-
// visible — but not on a shadow-supersede break (:142), which also exits with pending calls and is not exhaustion.
|
|
168
|
-
const pendingCalls = (finalResponse.output || []).filter(item => item.type === 'function_call');
|
|
169
|
-
if (pendingCalls.length && !shadowSuperseded()) {
|
|
170
|
-
logger.warn('[OpenAIResponsesProvider] Tool-iteration budget exhausted with pending tool calls', { maxFunctionRounds: this.maxFunctionRounds, pending: pendingCalls.map(c => c.name) });
|
|
171
|
-
trace?.setSignals?.({ toolBudgetExhausted: true });
|
|
172
|
-
}
|
|
173
131
|
}
|
|
174
132
|
|
|
175
133
|
return { response: finalResponse, toolCalls: allToolCalls, toolsExecuted: allToolsExecuted, retries: totalRetries, usage: accumulatedUsage };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const divergenceConfig = require('../config/divergenceConfig');
|
|
2
2
|
const { getDivergenceModel } = require('../models/divergenceModel');
|
|
3
|
-
const { classify
|
|
3
|
+
const { classify } = require('../helpers/divergenceHelper');
|
|
4
4
|
const runtimeConfig = require('../../config/runtimeConfig');
|
|
5
5
|
const { logger } = require('../../utils/logger');
|
|
6
6
|
|
|
@@ -8,19 +8,15 @@ const { runClinicalAgent } = require('./clinicalAgentService');
|
|
|
8
8
|
|
|
9
9
|
async function runDivergence({ request, connection, trace, actualMessage }) {
|
|
10
10
|
let post;
|
|
11
|
-
const startedAt = Date.now();
|
|
12
11
|
try {
|
|
13
12
|
post = await runClinicalAgent(request, connection);
|
|
14
|
-
logger.debug('[divergence] oracle responded', { runId: request.runId, status: post.status, durationMs: Date.now() - startedAt });
|
|
15
13
|
if (post.status !== 200) {
|
|
16
14
|
return { ok: false, status: post.status, response: post.body };
|
|
17
15
|
}
|
|
18
16
|
} catch (error) {
|
|
19
|
-
logger.debug('[divergence] oracle errored', { runId: request.runId, error: error.message, durationMs: Date.now() - startedAt });
|
|
20
17
|
return { ok: false, status: 503, response: { error: error.message } };
|
|
21
18
|
}
|
|
22
|
-
const
|
|
23
|
-
const expected = { ...expectedTurn, tools_invoked: oracleToolsInvoked(expectedTurn, request.enabledTools) };
|
|
19
|
+
const expected = post.body?.run?.sessions?.[0]?.turns?.[0] || {};
|
|
24
20
|
const expectedMessage = post.body?.response || '';
|
|
25
21
|
|
|
26
22
|
const actual = trace.toDecision();
|
|
@@ -34,22 +30,12 @@ async function runDivergence({ request, connection, trace, actualMessage }) {
|
|
|
34
30
|
}
|
|
35
31
|
|
|
36
32
|
async function diverge({ shadow, runId }) {
|
|
37
|
-
const code = shadow?.context?.patientCode;
|
|
38
33
|
const inputs = shadow?.context?.divergenceInputs;
|
|
39
|
-
if (!inputs)
|
|
40
|
-
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
if (!shadow.result?.completed) {
|
|
44
|
-
logger.debug('[divergence] skip: shadow incomplete', { runId, code });
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
34
|
+
if (!inputs) return null;
|
|
35
|
+
if (!shadow.result?.completed) return null;
|
|
47
36
|
|
|
48
37
|
const { enabled, baseUrl, timeoutMs } = await divergenceConfig.getOracleConnection();
|
|
49
|
-
if (!enabled || !baseUrl)
|
|
50
|
-
logger.debug('[divergence] skip: oracle off', { runId, code });
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
38
|
+
if (!enabled || !baseUrl) return null;
|
|
53
39
|
|
|
54
40
|
const request = {
|
|
55
41
|
runId,
|
|
@@ -59,7 +45,6 @@ async function diverge({ shadow, runId }) {
|
|
|
59
45
|
enabledTools: inputs.toolIds || [],
|
|
60
46
|
additionalInstructions: inputs.additionalInstructions || null,
|
|
61
47
|
};
|
|
62
|
-
logger.debug('[divergence] oracle request', { runId, code, model: request.model, inputLen: request.input.length, tools: request.enabledTools.length });
|
|
63
48
|
|
|
64
49
|
const connection = {
|
|
65
50
|
baseUrl,
|
|
@@ -72,10 +57,8 @@ async function diverge({ shadow, runId }) {
|
|
|
72
57
|
});
|
|
73
58
|
|
|
74
59
|
if (report.ok) {
|
|
75
|
-
logger.debug('[divergence] classified', { runId, code, result: report.result, emergency: !!report.isEmergencyReview });
|
|
76
60
|
try {
|
|
77
61
|
await getDivergenceModel().record({ shadow, request, report });
|
|
78
|
-
logger.debug('[divergence] persisted', { runId, code });
|
|
79
62
|
} catch (error) {
|
|
80
63
|
logger.warn('[divergence] persist failed', { runId, error: error.message });
|
|
81
64
|
}
|
|
@@ -15,15 +15,6 @@ 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
17
|
|
|
18
|
-
async function fetchConfigRecords(table, filter) {
|
|
19
|
-
const records = await getRecordByFilter(Config_ID, table, filter);
|
|
20
|
-
if (!records) {
|
|
21
|
-
logger.error('[promptComposer] Airtable read failed — refusing to widen the toolset', { table, filter });
|
|
22
|
-
throw new Error(`[promptComposer] Airtable read failed for ${table}`);
|
|
23
|
-
}
|
|
24
|
-
return records;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
18
|
async function fetchPreset(presetId, version = null) {
|
|
28
19
|
const cacheKey = version ? `preset:${presetId}:v${version}` : `preset:${presetId}:active`;
|
|
29
20
|
const cached = presetCache.get(cacheKey);
|
|
@@ -33,13 +24,8 @@ async function fetchPreset(presetId, version = null) {
|
|
|
33
24
|
? `AND({preset_id} = "${presetId}", {version} = ${version})`
|
|
34
25
|
: `AND({preset_id} = "${presetId}", {is_active} = 1)`;
|
|
35
26
|
|
|
36
|
-
const records = await
|
|
37
|
-
|
|
38
|
-
logger.error('[promptComposer] Ambiguous preset — several records match; using the first', {
|
|
39
|
-
presetId, version, matched: records.length,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
const record = records[0] || null;
|
|
27
|
+
const records = await getRecordByFilter(Config_ID, 'presets', filter);
|
|
28
|
+
const record = records?.[0] || null;
|
|
43
29
|
if (record) presetCache.set(cacheKey, record);
|
|
44
30
|
return record;
|
|
45
31
|
}
|
|
@@ -68,10 +54,6 @@ function applyVariables(text, variables) {
|
|
|
68
54
|
return text.replace(/\{\{(\w+)\}\}/g, (_, key) => variables[key] ?? '');
|
|
69
55
|
}
|
|
70
56
|
|
|
71
|
-
function variablesReferencedIn(text) {
|
|
72
|
-
return [...new Set([...(text || '').matchAll(/\{\{(\w+)\}\}/g)].map(m => m[1]))];
|
|
73
|
-
}
|
|
74
|
-
|
|
75
57
|
async function fetchSnippetsByRecordIds(recordIds) {
|
|
76
58
|
if (!recordIds?.length) return [];
|
|
77
59
|
|
|
@@ -95,7 +77,6 @@ async function composePrompt({ presetId = null, presetVersion = null, promptId =
|
|
|
95
77
|
let resolvedPromptId = promptId;
|
|
96
78
|
let presetSnippetIds = null;
|
|
97
79
|
let presetToolIds = null;
|
|
98
|
-
let presetSkillIds = null;
|
|
99
80
|
let resolvedPresetVersion = null;
|
|
100
81
|
let modelConfig = null;
|
|
101
82
|
|
|
@@ -103,12 +84,10 @@ async function composePrompt({ presetId = null, presetVersion = null, promptId =
|
|
|
103
84
|
const preset = await fetchPreset(id, version);
|
|
104
85
|
if (!preset) {
|
|
105
86
|
logger.error('[promptComposer] Preset not found in Airtable', { presetId: id, presetVersion: version });
|
|
106
|
-
presetToolIds = [];
|
|
107
87
|
return;
|
|
108
88
|
}
|
|
109
89
|
presetSnippetIds = preset.snippets || [];
|
|
110
90
|
presetToolIds = preset.tools || [];
|
|
111
|
-
presetSkillIds = preset.skills || [];
|
|
112
91
|
resolvedPresetVersion = preset.version ? String(preset.version) : null;
|
|
113
92
|
|
|
114
93
|
if (preset.model_config) {
|
|
@@ -153,7 +132,6 @@ async function composePrompt({ presetId = null, presetVersion = null, promptId =
|
|
|
153
132
|
baseContent = [...sorted.map(s => s.content), baseContent].join('\n\n');
|
|
154
133
|
}
|
|
155
134
|
|
|
156
|
-
const interpolatedVariables = variablesReferencedIn(baseContent);
|
|
157
135
|
const resolvedPrompt = applyVariables(baseContent, variables);
|
|
158
136
|
|
|
159
137
|
logger.debug('[promptComposer] Prompt composed', {
|
|
@@ -167,11 +145,9 @@ async function composePrompt({ presetId = null, presetVersion = null, promptId =
|
|
|
167
145
|
|
|
168
146
|
return {
|
|
169
147
|
resolvedPrompt,
|
|
170
|
-
interpolatedVariables,
|
|
171
148
|
snippetIds,
|
|
172
149
|
promptId: resolvedPromptId,
|
|
173
150
|
presetToolIds,
|
|
174
|
-
presetSkillIds,
|
|
175
151
|
presetId: presetId || null,
|
|
176
152
|
presetVersion: resolvedPresetVersion,
|
|
177
153
|
modelConfig,
|
|
@@ -185,10 +161,16 @@ async function fetchToolsByRecordIds(recordIds) {
|
|
|
185
161
|
const cached = toolCache.get(cacheKey);
|
|
186
162
|
if (cached) return cached;
|
|
187
163
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
164
|
+
try {
|
|
165
|
+
const formula = `OR(${recordIds.map(id => `RECORD_ID()="${id}"`).join(',')})`;
|
|
166
|
+
const records = await getRecordByFilter(Config_ID, 'tools', formula);
|
|
167
|
+
const tools = records || [];
|
|
168
|
+
toolCache.set(cacheKey, tools);
|
|
169
|
+
return tools;
|
|
170
|
+
} catch (error) {
|
|
171
|
+
logger.warn('[promptComposer] Failed to fetch preset tools, returning empty list', { error: error.message });
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
192
174
|
}
|
|
193
175
|
|
|
194
176
|
async function resolveTools({ promptId, assistant, presetToolIds = null, status = runtimeConfig.getServerMode() }) {
|
|
@@ -200,12 +182,7 @@ async function resolveTools({ promptId, assistant, presetToolIds = null, status
|
|
|
200
182
|
if (presetToolIds.length === 0) return { toolIds: [], filtered: true, descriptions: {} };
|
|
201
183
|
|
|
202
184
|
const mappedTools = await fetchToolsByRecordIds(presetToolIds);
|
|
203
|
-
if (!mappedTools.length) {
|
|
204
|
-
logger.error('[promptComposer] Preset tool ids resolved to no records — running with no tools', {
|
|
205
|
-
promptId, linked: presetToolIds.length,
|
|
206
|
-
});
|
|
207
|
-
return { toolIds: [], filtered: true, descriptions: {} };
|
|
208
|
-
}
|
|
185
|
+
if (!mappedTools.length) return { toolIds: [], filtered: false, descriptions: {} };
|
|
209
186
|
|
|
210
187
|
const isProd = status === 'prod';
|
|
211
188
|
const activeTools = mappedTools.filter(t => !t.status || t.status === 'prod' || (!isProd && t.status === 'dev'));
|
|
@@ -217,20 +194,12 @@ async function resolveTools({ promptId, assistant, presetToolIds = null, status
|
|
|
217
194
|
const id = tool.tool_id;
|
|
218
195
|
if (hasTool(id) || assistant?.tools?.has(id)) {
|
|
219
196
|
validToolIds.push(id);
|
|
220
|
-
|
|
221
|
-
if (typeof tool.description === 'string' && tool.description) descriptions[id] = tool.description;
|
|
197
|
+
if (tool.description) descriptions[id] = tool.description;
|
|
222
198
|
} else {
|
|
223
|
-
logger.
|
|
199
|
+
logger.warn('[promptComposer] Tool mapped in Airtable but not registered in code', { toolId: id, promptId });
|
|
224
200
|
}
|
|
225
201
|
}
|
|
226
202
|
|
|
227
|
-
if (validToolIds.length === 0) {
|
|
228
|
-
logger.error('[promptComposer] Preset links tools but none resolved — the agent will run with NO tools', {
|
|
229
|
-
promptId, status, linked: presetToolIds.length,
|
|
230
|
-
dropped: mappedTools.map(t => `${t.tool_id}:${t.status || 'prod'}`),
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
|
|
234
203
|
logger.debug('[promptComposer] Tools resolved', { promptId, mapped: activeTools.length, valid: validToolIds.length, filtered: true });
|
|
235
204
|
|
|
236
205
|
return { toolIds: validToolIds, filtered: true, descriptions };
|
|
@@ -14,26 +14,20 @@ async function attachShadowReplies(messages, code) {
|
|
|
14
14
|
const assistantMessages = messages.filter((msg) => msg.origin === 'assistant' && msg.response_id);
|
|
15
15
|
if (!assistantMessages.length) return messages;
|
|
16
16
|
|
|
17
|
-
const keyed = assistantMessages.filter((msg) => msg.turnId);
|
|
18
|
-
const legacy = assistantMessages.filter((msg) => !msg.turnId);
|
|
19
|
-
|
|
20
17
|
let traces;
|
|
21
18
|
try {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
});
|
|
32
|
-
or.push({ createdAt: { $gte: new Date(min - PAIRING_WINDOW_MS), $lte: new Date(max + PAIRING_WINDOW_MS) } });
|
|
33
|
-
}
|
|
19
|
+
let min = Infinity;
|
|
20
|
+
let max = -Infinity;
|
|
21
|
+
messages.forEach((msg) => {
|
|
22
|
+
const at = new Date(msg.createdAt).getTime();
|
|
23
|
+
if (at < min) min = at;
|
|
24
|
+
if (at > max) max = at;
|
|
25
|
+
});
|
|
26
|
+
const gte = new Date(min - PAIRING_WINDOW_MS);
|
|
27
|
+
const lte = new Date(max + PAIRING_WINDOW_MS);
|
|
34
28
|
traces = await getTurnTraceModel()
|
|
35
|
-
.find({ patientCode: code, outbound: { $ne: null }, $
|
|
36
|
-
.select({ outbound: 1, createdAt: 1
|
|
29
|
+
.find({ patientCode: code, outbound: { $ne: null }, createdAt: { $gte: gte, $lte: lte } })
|
|
30
|
+
.select({ outbound: 1, createdAt: 1 })
|
|
37
31
|
.sort({ createdAt: 1 })
|
|
38
32
|
.lean();
|
|
39
33
|
} catch (error) {
|
|
@@ -43,17 +37,7 @@ async function attachShadowReplies(messages, code) {
|
|
|
43
37
|
if (!traces.length) return messages;
|
|
44
38
|
|
|
45
39
|
const used = new Set();
|
|
46
|
-
|
|
47
|
-
const traceIndexByTurnId = new Map();
|
|
48
|
-
traces.forEach((trace, index) => { if (trace.turnId) traceIndexByTurnId.set(trace.turnId, index); });
|
|
49
|
-
keyed.forEach((msg) => {
|
|
50
|
-
const index = traceIndexByTurnId.get(msg.turnId);
|
|
51
|
-
if (index === undefined || used.has(index)) return;
|
|
52
|
-
used.add(index);
|
|
53
|
-
msg.shadowReply = traces[index].outbound;
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
legacy.forEach((msg) => {
|
|
40
|
+
assistantMessages.forEach((msg) => {
|
|
57
41
|
const at = new Date(msg.createdAt).getTime();
|
|
58
42
|
let bestIndex = -1;
|
|
59
43
|
let bestDelta = PAIRING_WINDOW_MS;
|