ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов 2.28 - вынос retry-orchestration адресного лейна (контекстный прогон, primary, retry сырого вопроса, fallback limited) в отдельный адаптер.
This commit is contained in:
parent
9b1d1bff91
commit
3531f7ddfe
|
|
@ -946,7 +946,57 @@ Validation:
|
||||||
- `assistantAddressFollowupContext.test.ts`
|
- `assistantAddressFollowupContext.test.ts`
|
||||||
- `assistantWave10SettlementCorrectiveRegression.test.ts`
|
- `assistantWave10SettlementCorrectiveRegression.test.ts`
|
||||||
|
|
||||||
Status: **In progress (Phase 2.1 + 2.2 + 2.3 + 2.4 + 2.5 + 2.6 + 2.7 + 2.8 + 2.9 + 2.10 + 2.11 + 2.12 + 2.13 + 2.14 + 2.15 + 2.16 + 2.17 + 2.18 + 2.19 + 2.20 + 2.21 + 2.22 + 2.23 + 2.24 + 2.25 + 2.26 + 2.27 completed)**
|
Implemented in current pass (Phase 2.28):
|
||||||
|
1. Extracted address-lane retry orchestration branch from `assistantService` into dedicated runtime adapter:
|
||||||
|
- `assistantAddressLaneRuntimeAdapter.ts`
|
||||||
|
- introduced:
|
||||||
|
- `runAssistantAddressLaneRuntime(...)`
|
||||||
|
2. Centralized address retry/runtime branch sequence (behavior-preserving):
|
||||||
|
- contextual-first execution when followup context is preferred;
|
||||||
|
- primary execution without followup context;
|
||||||
|
- optional contextual fallback when context exists but is not preferred;
|
||||||
|
- retry with raw user message for retryable limited results;
|
||||||
|
- deterministic fallback to pending limited result when retry does not improve outcome.
|
||||||
|
3. Rewired `assistantService` address lane execution path to consume retry adapter output and preserve existing address finalization contract.
|
||||||
|
4. Added focused unit tests:
|
||||||
|
- `assistantAddressLaneRuntimeAdapter.test.ts`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
1. `npm run build` passed.
|
||||||
|
2. Targeted living/address followup pack passed:
|
||||||
|
- `assistantAddressLaneRuntimeAdapter.test.ts`
|
||||||
|
- `assistantAddressFollowupContext.test.ts`
|
||||||
|
- `assistantLivingChatMode.test.ts`
|
||||||
|
- `assistantLivingRouter.test.ts`
|
||||||
|
- `assistantWave10SettlementCorrectiveRegression.test.ts`
|
||||||
|
|
||||||
|
Implemented in current pass (Phase 2.29):
|
||||||
|
1. Extracted address orchestration bootstrap block from `assistantService` into dedicated runtime adapter:
|
||||||
|
- `assistantAddressOrchestrationRuntimeAdapter.ts`
|
||||||
|
- introduced:
|
||||||
|
- `buildAssistantAddressOrchestrationRuntime(...)`
|
||||||
|
2. Centralized address orchestration bootstrap sequence (behavior-preserving):
|
||||||
|
- LLM predecompose stage or deterministic fallback contract when feature is disabled;
|
||||||
|
- effective address input message resolution;
|
||||||
|
- followup carryover context resolution;
|
||||||
|
- orchestration/tool-gate decision resolution;
|
||||||
|
- dialog continuation contract projection into runtime meta;
|
||||||
|
- living mode decision projection for chat fallback.
|
||||||
|
3. Rewired `assistantService` address pre-lane bootstrap path to consume orchestration runtime adapter output.
|
||||||
|
4. Added focused unit tests:
|
||||||
|
- `assistantAddressOrchestrationRuntimeAdapter.test.ts`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
1. `npm run build` passed.
|
||||||
|
2. Targeted living/address followup pack passed:
|
||||||
|
- `assistantAddressOrchestrationRuntimeAdapter.test.ts`
|
||||||
|
- `assistantAddressLaneRuntimeAdapter.test.ts`
|
||||||
|
- `assistantAddressFollowupContext.test.ts`
|
||||||
|
- `assistantLivingChatMode.test.ts`
|
||||||
|
- `assistantLivingRouter.test.ts`
|
||||||
|
- `assistantWave10SettlementCorrectiveRegression.test.ts`
|
||||||
|
|
||||||
|
Status: **In progress (Phase 2.1 + 2.2 + 2.3 + 2.4 + 2.5 + 2.6 + 2.7 + 2.8 + 2.9 + 2.10 + 2.11 + 2.12 + 2.13 + 2.14 + 2.15 + 2.16 + 2.17 + 2.18 + 2.19 + 2.20 + 2.21 + 2.22 + 2.23 + 2.24 + 2.25 + 2.26 + 2.27 + 2.28 + 2.29 completed)**
|
||||||
|
|
||||||
## Stage 3 (P2): Hybrid Semantic Layer (LLM + Deterministic Guards)
|
## Stage 3 (P2): Hybrid Semantic Layer (LLM + Deterministic Guards)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.runAssistantAddressLaneRuntime = runAssistantAddressLaneRuntime;
|
||||||
|
function limitedCategory(addressLane) {
|
||||||
|
return typeof addressLane?.debug?.limited_reason_category === "string"
|
||||||
|
? addressLane.debug.limited_reason_category
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
async function runAssistantAddressLaneRuntime(input) {
|
||||||
|
const retryAudit = {
|
||||||
|
attempted: false,
|
||||||
|
reason: null,
|
||||||
|
initial_limited_category: null,
|
||||||
|
retry_message: null,
|
||||||
|
retry_used_followup_context: false,
|
||||||
|
retry_result_category: null
|
||||||
|
};
|
||||||
|
let pendingLimited = null;
|
||||||
|
const evaluateAddressLane = (addressLane, messageUsed, carryMeta) => {
|
||||||
|
if (!addressLane?.handled) {
|
||||||
|
return { action: "continue" };
|
||||||
|
}
|
||||||
|
if (!input.isRetryableAddressLimitedResult(addressLane)) {
|
||||||
|
return {
|
||||||
|
action: "return",
|
||||||
|
selection: {
|
||||||
|
addressLane,
|
||||||
|
messageUsed,
|
||||||
|
carryMeta
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!pendingLimited) {
|
||||||
|
pendingLimited = {
|
||||||
|
addressLane,
|
||||||
|
messageUsed,
|
||||||
|
carryMeta
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { action: "continue" };
|
||||||
|
};
|
||||||
|
if (input.shouldPreferContextualLane) {
|
||||||
|
const contextualAddressLane = await input.runAddressLaneAttempt(input.addressInputMessage, input.carryover);
|
||||||
|
const decision = evaluateAddressLane(contextualAddressLane, input.addressInputMessage, input.carryover);
|
||||||
|
if (decision.action === "return") {
|
||||||
|
return {
|
||||||
|
handled: true,
|
||||||
|
selection: decision.selection,
|
||||||
|
retryAudit
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const primaryAddressLane = await input.runAddressLaneAttempt(input.addressInputMessage, null);
|
||||||
|
const primaryDecision = evaluateAddressLane(primaryAddressLane, input.addressInputMessage, null);
|
||||||
|
if (primaryDecision.action === "return") {
|
||||||
|
return {
|
||||||
|
handled: true,
|
||||||
|
selection: primaryDecision.selection,
|
||||||
|
retryAudit
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!input.shouldPreferContextualLane && input.carryover?.followupContext) {
|
||||||
|
const contextualAddressLane = await input.runAddressLaneAttempt(input.addressInputMessage, input.carryover);
|
||||||
|
const contextualDecision = evaluateAddressLane(contextualAddressLane, input.addressInputMessage, input.carryover);
|
||||||
|
if (contextualDecision.action === "return") {
|
||||||
|
return {
|
||||||
|
handled: true,
|
||||||
|
selection: contextualDecision.selection,
|
||||||
|
retryAudit
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const pendingLimitedSelection = pendingLimited;
|
||||||
|
if (pendingLimitedSelection && input.canRetryWithRawUserMessage) {
|
||||||
|
retryAudit.attempted = true;
|
||||||
|
retryAudit.reason = "limited_result_retry_with_raw_message";
|
||||||
|
retryAudit.initial_limited_category = limitedCategory(pendingLimitedSelection?.addressLane ?? null);
|
||||||
|
retryAudit.retry_message = input.userMessage;
|
||||||
|
if (input.carryover?.followupContext) {
|
||||||
|
retryAudit.retry_used_followup_context = true;
|
||||||
|
const rawContextualLane = await input.runAddressLaneAttempt(input.userMessage, input.carryover);
|
||||||
|
const rawContextualDecision = evaluateAddressLane(rawContextualLane, input.userMessage, input.carryover);
|
||||||
|
if (rawContextualDecision.action === "return") {
|
||||||
|
retryAudit.retry_result_category = limitedCategory(rawContextualDecision.selection.addressLane);
|
||||||
|
return {
|
||||||
|
handled: true,
|
||||||
|
selection: rawContextualDecision.selection,
|
||||||
|
retryAudit
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const rawPrimaryLane = await input.runAddressLaneAttempt(input.userMessage, null);
|
||||||
|
retryAudit.retry_result_category = limitedCategory(rawPrimaryLane);
|
||||||
|
const rawPrimaryDecision = evaluateAddressLane(rawPrimaryLane, input.userMessage, null);
|
||||||
|
if (rawPrimaryDecision.action === "return") {
|
||||||
|
return {
|
||||||
|
handled: true,
|
||||||
|
selection: rawPrimaryDecision.selection,
|
||||||
|
retryAudit
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pendingLimited) {
|
||||||
|
return {
|
||||||
|
handled: true,
|
||||||
|
selection: pendingLimited,
|
||||||
|
retryAudit
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
handled: false,
|
||||||
|
selection: null,
|
||||||
|
retryAudit
|
||||||
|
};
|
||||||
|
}
|
||||||
58
llm_normalizer/backend/dist/services/assistantAddressOrchestrationRuntimeAdapter.js
vendored
Normal file
58
llm_normalizer/backend/dist/services/assistantAddressOrchestrationRuntimeAdapter.js
vendored
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.buildAssistantAddressOrchestrationRuntime = buildAssistantAddressOrchestrationRuntime;
|
||||||
|
function fallbackAddressPreDecompose(userMessage, llmProvider, buildAddressLlmPredecomposeContractV1, sanitizeAddressMessageForFallback) {
|
||||||
|
const provider = llmProvider === "local" ? "local" : llmProvider === "openai" ? "openai" : null;
|
||||||
|
return {
|
||||||
|
attempted: false,
|
||||||
|
applied: false,
|
||||||
|
provider,
|
||||||
|
traceId: null,
|
||||||
|
effectiveMessage: userMessage,
|
||||||
|
reason: "disabled_by_feature_flag",
|
||||||
|
llmCanonicalCandidateDetected: false,
|
||||||
|
predecomposeContract: buildAddressLlmPredecomposeContractV1({
|
||||||
|
sourceMessage: userMessage,
|
||||||
|
canonicalMessage: userMessage
|
||||||
|
}),
|
||||||
|
fallbackRuleHit: null,
|
||||||
|
sanitizedUserMessage: sanitizeAddressMessageForFallback(userMessage),
|
||||||
|
toolGateDecision: null,
|
||||||
|
toolGateReason: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async function buildAssistantAddressOrchestrationRuntime(input) {
|
||||||
|
const addressPreDecompose = input.featureAddressLlmPredecomposeV1
|
||||||
|
? await input.runAddressLlmPreDecompose()
|
||||||
|
: fallbackAddressPreDecompose(input.userMessage, input.llmProvider, input.buildAddressLlmPredecomposeContractV1, input.sanitizeAddressMessageForFallback);
|
||||||
|
const addressInputMessage = input.toNonEmptyString(addressPreDecompose?.effectiveMessage) ?? input.userMessage;
|
||||||
|
const carryover = input.resolveAddressFollowupCarryoverContext(input.userMessage, input.sessionItems, addressInputMessage, addressPreDecompose);
|
||||||
|
const followupContext = carryover?.followupContext ?? null;
|
||||||
|
const orchestrationDecision = input.resolveAssistantOrchestrationDecision({
|
||||||
|
rawUserMessage: input.userMessage,
|
||||||
|
effectiveAddressUserMessage: addressInputMessage,
|
||||||
|
followupContext,
|
||||||
|
llmPreDecomposeMeta: addressPreDecompose,
|
||||||
|
useMock: input.useMock
|
||||||
|
});
|
||||||
|
const dialogContinuationContract = input.buildAddressDialogContinuationContractV2(input.userMessage, addressInputMessage, carryover, addressPreDecompose);
|
||||||
|
const addressRuntimeMeta = {
|
||||||
|
...addressPreDecompose,
|
||||||
|
toolGateDecision: orchestrationDecision.toolGateDecision ?? null,
|
||||||
|
toolGateReason: orchestrationDecision.toolGateReason ?? null,
|
||||||
|
dialogContinuationContract,
|
||||||
|
orchestrationContract: orchestrationDecision.orchestrationContract ?? null
|
||||||
|
};
|
||||||
|
const livingModeDecision = {
|
||||||
|
mode: orchestrationDecision.livingMode,
|
||||||
|
reason: orchestrationDecision.livingReason
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
addressPreDecompose,
|
||||||
|
addressInputMessage,
|
||||||
|
carryover,
|
||||||
|
orchestrationDecision,
|
||||||
|
addressRuntimeMeta,
|
||||||
|
livingModeDecision
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -75,6 +75,8 @@ const assistantDeepTurnGroundingRuntimeAdapter_1 = __importStar(require("./assis
|
||||||
const assistantDeepTurnPackagingRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnPackagingRuntimeAdapter"));
|
const assistantDeepTurnPackagingRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnPackagingRuntimeAdapter"));
|
||||||
const assistantDeepTurnPlanRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnPlanRuntimeAdapter"));
|
const assistantDeepTurnPlanRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnPlanRuntimeAdapter"));
|
||||||
const assistantDeepTurnRetrievalRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnRetrievalRuntimeAdapter"));
|
const assistantDeepTurnRetrievalRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnRetrievalRuntimeAdapter"));
|
||||||
|
const assistantAddressLaneRuntimeAdapter_1 = __importStar(require("./assistantAddressLaneRuntimeAdapter"));
|
||||||
|
const assistantAddressOrchestrationRuntimeAdapter_1 = __importStar(require("./assistantAddressOrchestrationRuntimeAdapter"));
|
||||||
const assistantLivingChatTurnFinalizeRuntimeAdapter_1 = __importStar(require("./assistantLivingChatTurnFinalizeRuntimeAdapter"));
|
const assistantLivingChatTurnFinalizeRuntimeAdapter_1 = __importStar(require("./assistantLivingChatTurnFinalizeRuntimeAdapter"));
|
||||||
const assistantLivingChatRuntimeAdapter_1 = __importStar(require("./assistantLivingChatRuntimeAdapter"));
|
const assistantLivingChatRuntimeAdapter_1 = __importStar(require("./assistantLivingChatRuntimeAdapter"));
|
||||||
const assistantQueryPlanning_1 = __importStar(require("./assistantQueryPlanning"));
|
const assistantQueryPlanning_1 = __importStar(require("./assistantQueryPlanning"));
|
||||||
|
|
@ -4560,47 +4562,27 @@ class AssistantService {
|
||||||
};
|
};
|
||||||
let addressRuntimeMetaForDeep = null;
|
let addressRuntimeMetaForDeep = null;
|
||||||
if (config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_V1) {
|
if (config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_V1) {
|
||||||
const addressPreDecompose = config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1
|
const addressOrchestrationRuntime = await (0, assistantAddressOrchestrationRuntimeAdapter_1.buildAssistantAddressOrchestrationRuntime)({
|
||||||
? await runAddressLlmPreDecompose(this.normalizerService, payload, userMessage)
|
userMessage,
|
||||||
: {
|
sessionItems: session.items,
|
||||||
attempted: false,
|
llmProvider: payload?.llmProvider,
|
||||||
applied: false,
|
useMock: Boolean(payload.useMock),
|
||||||
provider: payload?.llmProvider === "local" ? "local" : payload?.llmProvider === "openai" ? "openai" : null,
|
featureAddressLlmPredecomposeV1: config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1,
|
||||||
traceId: null,
|
runAddressLlmPreDecompose: async () => runAddressLlmPreDecompose(this.normalizerService, payload, userMessage),
|
||||||
effectiveMessage: userMessage,
|
buildAddressLlmPredecomposeContractV1: predecomposeContract_1.buildAddressLlmPredecomposeContractV1,
|
||||||
reason: "disabled_by_feature_flag",
|
sanitizeAddressMessageForFallback,
|
||||||
llmCanonicalCandidateDetected: false,
|
toNonEmptyString,
|
||||||
predecomposeContract: (0, predecomposeContract_1.buildAddressLlmPredecomposeContractV1)({
|
resolveAddressFollowupCarryoverContext,
|
||||||
sourceMessage: userMessage,
|
resolveAssistantOrchestrationDecision,
|
||||||
canonicalMessage: userMessage
|
buildAddressDialogContinuationContractV2
|
||||||
}),
|
|
||||||
fallbackRuleHit: null,
|
|
||||||
sanitizedUserMessage: sanitizeAddressMessageForFallback(userMessage),
|
|
||||||
toolGateDecision: null,
|
|
||||||
toolGateReason: null
|
|
||||||
};
|
|
||||||
const addressInputMessage = toNonEmptyString(addressPreDecompose?.effectiveMessage) ?? userMessage;
|
|
||||||
const carryover = resolveAddressFollowupCarryoverContext(userMessage, session.items, addressInputMessage, addressPreDecompose);
|
|
||||||
const orchestrationDecision = resolveAssistantOrchestrationDecision({
|
|
||||||
rawUserMessage: userMessage,
|
|
||||||
effectiveAddressUserMessage: addressInputMessage,
|
|
||||||
followupContext: carryover?.followupContext ?? null,
|
|
||||||
llmPreDecomposeMeta: addressPreDecompose,
|
|
||||||
useMock: Boolean(payload.useMock)
|
|
||||||
});
|
});
|
||||||
const dialogContinuationContract = buildAddressDialogContinuationContractV2(userMessage, addressInputMessage, carryover, addressPreDecompose);
|
const addressPreDecompose = addressOrchestrationRuntime.addressPreDecompose;
|
||||||
const addressRuntimeMeta = {
|
const addressInputMessage = addressOrchestrationRuntime.addressInputMessage;
|
||||||
...addressPreDecompose,
|
const carryover = addressOrchestrationRuntime.carryover;
|
||||||
toolGateDecision: orchestrationDecision.toolGateDecision,
|
const orchestrationDecision = addressOrchestrationRuntime.orchestrationDecision;
|
||||||
toolGateReason: orchestrationDecision.toolGateReason,
|
const addressRuntimeMeta = addressOrchestrationRuntime.addressRuntimeMeta;
|
||||||
dialogContinuationContract,
|
|
||||||
orchestrationContract: orchestrationDecision.orchestrationContract
|
|
||||||
};
|
|
||||||
addressRuntimeMetaForDeep = addressRuntimeMeta;
|
addressRuntimeMetaForDeep = addressRuntimeMeta;
|
||||||
const livingModeDecision = {
|
const livingModeDecision = addressOrchestrationRuntime.livingModeDecision;
|
||||||
mode: orchestrationDecision.livingMode,
|
|
||||||
reason: orchestrationDecision.livingReason
|
|
||||||
};
|
|
||||||
if (!orchestrationDecision.runAddressLane) {
|
if (!orchestrationDecision.runAddressLane) {
|
||||||
(0, log_1.logJson)({
|
(0, log_1.logJson)({
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
|
|
@ -4637,42 +4619,6 @@ class AssistantService {
|
||||||
const analysisDateHint = runtimeAnalysisContext.as_of_date ?? toNonEmptyString(payload?.context?.period_hint);
|
const analysisDateHint = runtimeAnalysisContext.as_of_date ?? toNonEmptyString(payload?.context?.period_hint);
|
||||||
const canRetryWithRawUserMessage = compactWhitespace(String(addressInputMessage ?? "").toLowerCase()) !==
|
const canRetryWithRawUserMessage = compactWhitespace(String(addressInputMessage ?? "").toLowerCase()) !==
|
||||||
compactWhitespace(String(userMessage ?? "").toLowerCase());
|
compactWhitespace(String(userMessage ?? "").toLowerCase());
|
||||||
const retryAudit = {
|
|
||||||
attempted: false,
|
|
||||||
reason: null,
|
|
||||||
initial_limited_category: null,
|
|
||||||
retry_message: null,
|
|
||||||
retry_used_followup_context: false,
|
|
||||||
retry_result_category: null
|
|
||||||
};
|
|
||||||
const withRetryMeta = () => ({
|
|
||||||
...addressRuntimeMeta,
|
|
||||||
addressRetryAudit: { ...retryAudit }
|
|
||||||
});
|
|
||||||
let pendingLimited = null;
|
|
||||||
const evaluateAddressLane = (addressLane, messageUsed, carryMeta) => {
|
|
||||||
if (!addressLane?.handled) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!isRetryableAddressLimitedResult(addressLane)) {
|
|
||||||
return {
|
|
||||||
action: "return",
|
|
||||||
addressLane,
|
|
||||||
messageUsed,
|
|
||||||
carryMeta
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (!pendingLimited) {
|
|
||||||
pendingLimited = {
|
|
||||||
addressLane,
|
|
||||||
messageUsed,
|
|
||||||
carryMeta
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
action: "continue"
|
|
||||||
};
|
|
||||||
};
|
|
||||||
const runAddressLaneAttempt = async (messageUsed, carryMeta) => {
|
const runAddressLaneAttempt = async (messageUsed, carryMeta) => {
|
||||||
const scopedFollowupContext = mergeFollowupContextWithOrganizationScope(carryMeta?.followupContext ?? null, sessionOrganizationScope.activeOrganization);
|
const scopedFollowupContext = mergeFollowupContextWithOrganizationScope(carryMeta?.followupContext ?? null, sessionOrganizationScope.activeOrganization);
|
||||||
if (scopedFollowupContext) {
|
if (scopedFollowupContext) {
|
||||||
|
|
@ -4685,48 +4631,20 @@ class AssistantService {
|
||||||
analysisDateHint
|
analysisDateHint
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
if (shouldPreferContextualLane) {
|
const addressLaneRuntime = await (0, assistantAddressLaneRuntimeAdapter_1.runAssistantAddressLaneRuntime)({
|
||||||
const contextualAddressLane = await runAddressLaneAttempt(addressInputMessage, carryover);
|
userMessage,
|
||||||
const decision = evaluateAddressLane(contextualAddressLane, addressInputMessage, carryover);
|
addressInputMessage,
|
||||||
if (decision?.action === "return") {
|
carryover,
|
||||||
return finalizeAddressLaneResponse(decision.addressLane, decision.messageUsed, decision.carryMeta, withRetryMeta());
|
shouldPreferContextualLane,
|
||||||
}
|
canRetryWithRawUserMessage,
|
||||||
}
|
runAddressLaneAttempt,
|
||||||
const primaryAddressLane = await runAddressLaneAttempt(addressInputMessage, null);
|
isRetryableAddressLimitedResult
|
||||||
const primaryDecision = evaluateAddressLane(primaryAddressLane, addressInputMessage, null);
|
});
|
||||||
if (primaryDecision?.action === "return") {
|
if (addressLaneRuntime.handled && addressLaneRuntime.selection) {
|
||||||
return finalizeAddressLaneResponse(primaryDecision.addressLane, primaryDecision.messageUsed, primaryDecision.carryMeta, withRetryMeta());
|
return finalizeAddressLaneResponse(addressLaneRuntime.selection.addressLane, addressLaneRuntime.selection.messageUsed, addressLaneRuntime.selection.carryMeta, {
|
||||||
}
|
...addressRuntimeMeta,
|
||||||
if (!shouldPreferContextualLane && carryover?.followupContext) {
|
addressRetryAudit: { ...addressLaneRuntime.retryAudit }
|
||||||
const contextualAddressLane = await runAddressLaneAttempt(addressInputMessage, carryover);
|
});
|
||||||
const contextualDecision = evaluateAddressLane(contextualAddressLane, addressInputMessage, carryover);
|
|
||||||
if (contextualDecision?.action === "return") {
|
|
||||||
return finalizeAddressLaneResponse(contextualDecision.addressLane, contextualDecision.messageUsed, contextualDecision.carryMeta, withRetryMeta());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (pendingLimited && canRetryWithRawUserMessage) {
|
|
||||||
retryAudit.attempted = true;
|
|
||||||
retryAudit.reason = "limited_result_retry_with_raw_message";
|
|
||||||
retryAudit.initial_limited_category = pendingLimited.addressLane?.debug?.limited_reason_category ?? null;
|
|
||||||
retryAudit.retry_message = userMessage;
|
|
||||||
if (carryover?.followupContext) {
|
|
||||||
retryAudit.retry_used_followup_context = true;
|
|
||||||
const rawContextualLane = await runAddressLaneAttempt(userMessage, carryover);
|
|
||||||
const rawContextualDecision = evaluateAddressLane(rawContextualLane, userMessage, carryover);
|
|
||||||
if (rawContextualDecision?.action === "return") {
|
|
||||||
retryAudit.retry_result_category = rawContextualDecision.addressLane?.debug?.limited_reason_category ?? null;
|
|
||||||
return finalizeAddressLaneResponse(rawContextualDecision.addressLane, rawContextualDecision.messageUsed, rawContextualDecision.carryMeta, withRetryMeta());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const rawPrimaryLane = await runAddressLaneAttempt(userMessage, null);
|
|
||||||
retryAudit.retry_result_category = rawPrimaryLane?.debug?.limited_reason_category ?? null;
|
|
||||||
const rawPrimaryDecision = evaluateAddressLane(rawPrimaryLane, userMessage, null);
|
|
||||||
if (rawPrimaryDecision?.action === "return") {
|
|
||||||
return finalizeAddressLaneResponse(rawPrimaryDecision.addressLane, rawPrimaryDecision.messageUsed, rawPrimaryDecision.carryMeta, withRetryMeta());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (pendingLimited) {
|
|
||||||
return finalizeAddressLaneResponse(pendingLimited.addressLane, pendingLimited.messageUsed, pendingLimited.carryMeta, withRetryMeta());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,179 @@
|
||||||
|
export interface AssistantAddressFollowupCarryoverLike {
|
||||||
|
followupContext?: unknown | null;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantAddressLaneLike {
|
||||||
|
handled?: boolean;
|
||||||
|
debug?: {
|
||||||
|
limited_reason_category?: string | null;
|
||||||
|
[key: string]: unknown;
|
||||||
|
} | null;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantAddressLaneSelection {
|
||||||
|
addressLane: AssistantAddressLaneLike;
|
||||||
|
messageUsed: string;
|
||||||
|
carryMeta: AssistantAddressFollowupCarryoverLike | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantAddressLaneRetryAudit {
|
||||||
|
attempted: boolean;
|
||||||
|
reason: string | null;
|
||||||
|
initial_limited_category: string | null;
|
||||||
|
retry_message: string | null;
|
||||||
|
retry_used_followup_context: boolean;
|
||||||
|
retry_result_category: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RunAssistantAddressLaneRuntimeInput {
|
||||||
|
userMessage: string;
|
||||||
|
addressInputMessage: string;
|
||||||
|
carryover: AssistantAddressFollowupCarryoverLike | null;
|
||||||
|
shouldPreferContextualLane: boolean;
|
||||||
|
canRetryWithRawUserMessage: boolean;
|
||||||
|
runAddressLaneAttempt: (
|
||||||
|
messageUsed: string,
|
||||||
|
carryMeta: AssistantAddressFollowupCarryoverLike | null
|
||||||
|
) => Promise<AssistantAddressLaneLike | null>;
|
||||||
|
isRetryableAddressLimitedResult: (addressLane: AssistantAddressLaneLike | null | undefined) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RunAssistantAddressLaneRuntimeOutput {
|
||||||
|
handled: boolean;
|
||||||
|
selection: AssistantAddressLaneSelection | null;
|
||||||
|
retryAudit: AssistantAddressLaneRetryAudit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function limitedCategory(addressLane: AssistantAddressLaneLike | null | undefined): string | null {
|
||||||
|
return typeof addressLane?.debug?.limited_reason_category === "string"
|
||||||
|
? addressLane.debug.limited_reason_category
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runAssistantAddressLaneRuntime(
|
||||||
|
input: RunAssistantAddressLaneRuntimeInput
|
||||||
|
): Promise<RunAssistantAddressLaneRuntimeOutput> {
|
||||||
|
const retryAudit: AssistantAddressLaneRetryAudit = {
|
||||||
|
attempted: false,
|
||||||
|
reason: null,
|
||||||
|
initial_limited_category: null,
|
||||||
|
retry_message: null,
|
||||||
|
retry_used_followup_context: false,
|
||||||
|
retry_result_category: null
|
||||||
|
};
|
||||||
|
|
||||||
|
let pendingLimited: AssistantAddressLaneSelection | null = null;
|
||||||
|
|
||||||
|
const evaluateAddressLane = (
|
||||||
|
addressLane: AssistantAddressLaneLike | null,
|
||||||
|
messageUsed: string,
|
||||||
|
carryMeta: AssistantAddressFollowupCarryoverLike | null
|
||||||
|
): { action: "return"; selection: AssistantAddressLaneSelection } | { action: "continue" } => {
|
||||||
|
if (!addressLane?.handled) {
|
||||||
|
return { action: "continue" };
|
||||||
|
}
|
||||||
|
if (!input.isRetryableAddressLimitedResult(addressLane)) {
|
||||||
|
return {
|
||||||
|
action: "return",
|
||||||
|
selection: {
|
||||||
|
addressLane,
|
||||||
|
messageUsed,
|
||||||
|
carryMeta
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!pendingLimited) {
|
||||||
|
pendingLimited = {
|
||||||
|
addressLane,
|
||||||
|
messageUsed,
|
||||||
|
carryMeta
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { action: "continue" };
|
||||||
|
};
|
||||||
|
|
||||||
|
if (input.shouldPreferContextualLane) {
|
||||||
|
const contextualAddressLane = await input.runAddressLaneAttempt(input.addressInputMessage, input.carryover);
|
||||||
|
const decision = evaluateAddressLane(contextualAddressLane, input.addressInputMessage, input.carryover);
|
||||||
|
if (decision.action === "return") {
|
||||||
|
return {
|
||||||
|
handled: true,
|
||||||
|
selection: decision.selection,
|
||||||
|
retryAudit
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const primaryAddressLane = await input.runAddressLaneAttempt(input.addressInputMessage, null);
|
||||||
|
const primaryDecision = evaluateAddressLane(primaryAddressLane, input.addressInputMessage, null);
|
||||||
|
if (primaryDecision.action === "return") {
|
||||||
|
return {
|
||||||
|
handled: true,
|
||||||
|
selection: primaryDecision.selection,
|
||||||
|
retryAudit
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!input.shouldPreferContextualLane && input.carryover?.followupContext) {
|
||||||
|
const contextualAddressLane = await input.runAddressLaneAttempt(input.addressInputMessage, input.carryover);
|
||||||
|
const contextualDecision = evaluateAddressLane(contextualAddressLane, input.addressInputMessage, input.carryover);
|
||||||
|
if (contextualDecision.action === "return") {
|
||||||
|
return {
|
||||||
|
handled: true,
|
||||||
|
selection: contextualDecision.selection,
|
||||||
|
retryAudit
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pendingLimitedSelection = pendingLimited;
|
||||||
|
if (pendingLimitedSelection && input.canRetryWithRawUserMessage) {
|
||||||
|
retryAudit.attempted = true;
|
||||||
|
retryAudit.reason = "limited_result_retry_with_raw_message";
|
||||||
|
retryAudit.initial_limited_category = limitedCategory(
|
||||||
|
(pendingLimitedSelection as AssistantAddressLaneSelection | null)?.addressLane ?? null
|
||||||
|
);
|
||||||
|
retryAudit.retry_message = input.userMessage;
|
||||||
|
|
||||||
|
if (input.carryover?.followupContext) {
|
||||||
|
retryAudit.retry_used_followup_context = true;
|
||||||
|
const rawContextualLane = await input.runAddressLaneAttempt(input.userMessage, input.carryover);
|
||||||
|
const rawContextualDecision = evaluateAddressLane(rawContextualLane, input.userMessage, input.carryover);
|
||||||
|
if (rawContextualDecision.action === "return") {
|
||||||
|
retryAudit.retry_result_category = limitedCategory(rawContextualDecision.selection.addressLane);
|
||||||
|
return {
|
||||||
|
handled: true,
|
||||||
|
selection: rawContextualDecision.selection,
|
||||||
|
retryAudit
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawPrimaryLane = await input.runAddressLaneAttempt(input.userMessage, null);
|
||||||
|
retryAudit.retry_result_category = limitedCategory(rawPrimaryLane);
|
||||||
|
const rawPrimaryDecision = evaluateAddressLane(rawPrimaryLane, input.userMessage, null);
|
||||||
|
if (rawPrimaryDecision.action === "return") {
|
||||||
|
return {
|
||||||
|
handled: true,
|
||||||
|
selection: rawPrimaryDecision.selection,
|
||||||
|
retryAudit
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingLimited) {
|
||||||
|
return {
|
||||||
|
handled: true,
|
||||||
|
selection: pendingLimited,
|
||||||
|
retryAudit
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
handled: false,
|
||||||
|
selection: null,
|
||||||
|
retryAudit
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
export interface BuildAssistantAddressOrchestrationRuntimeInput {
|
||||||
|
userMessage: string;
|
||||||
|
sessionItems: unknown[];
|
||||||
|
llmProvider: unknown;
|
||||||
|
useMock: boolean;
|
||||||
|
featureAddressLlmPredecomposeV1: boolean;
|
||||||
|
runAddressLlmPreDecompose: () => Promise<Record<string, unknown>>;
|
||||||
|
buildAddressLlmPredecomposeContractV1: (input: {
|
||||||
|
sourceMessage: string;
|
||||||
|
canonicalMessage: string;
|
||||||
|
}) => unknown;
|
||||||
|
sanitizeAddressMessageForFallback: (userMessage: string) => string;
|
||||||
|
toNonEmptyString: (value: unknown) => string | null;
|
||||||
|
resolveAddressFollowupCarryoverContext: (
|
||||||
|
userMessage: string,
|
||||||
|
sessionItems: unknown[],
|
||||||
|
addressInputMessage: string,
|
||||||
|
addressPreDecompose: Record<string, unknown>
|
||||||
|
) => AssistantAddressCarryoverLike | null;
|
||||||
|
resolveAssistantOrchestrationDecision: (input: {
|
||||||
|
rawUserMessage: string;
|
||||||
|
effectiveAddressUserMessage: string;
|
||||||
|
followupContext: unknown;
|
||||||
|
llmPreDecomposeMeta: Record<string, unknown>;
|
||||||
|
useMock: boolean;
|
||||||
|
}) => Record<string, unknown>;
|
||||||
|
buildAddressDialogContinuationContractV2: (
|
||||||
|
userMessage: string,
|
||||||
|
addressInputMessage: string,
|
||||||
|
carryover: AssistantAddressCarryoverLike | null,
|
||||||
|
addressPreDecompose: Record<string, unknown>
|
||||||
|
) => unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantAddressCarryoverLike {
|
||||||
|
followupContext?: unknown;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BuildAssistantAddressOrchestrationRuntimeOutput {
|
||||||
|
addressPreDecompose: Record<string, unknown>;
|
||||||
|
addressInputMessage: string;
|
||||||
|
carryover: AssistantAddressCarryoverLike | null;
|
||||||
|
orchestrationDecision: Record<string, unknown>;
|
||||||
|
addressRuntimeMeta: Record<string, unknown>;
|
||||||
|
livingModeDecision: {
|
||||||
|
mode: unknown;
|
||||||
|
reason: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function fallbackAddressPreDecompose(
|
||||||
|
userMessage: string,
|
||||||
|
llmProvider: unknown,
|
||||||
|
buildAddressLlmPredecomposeContractV1: BuildAssistantAddressOrchestrationRuntimeInput["buildAddressLlmPredecomposeContractV1"],
|
||||||
|
sanitizeAddressMessageForFallback: BuildAssistantAddressOrchestrationRuntimeInput["sanitizeAddressMessageForFallback"]
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const provider =
|
||||||
|
llmProvider === "local" ? "local" : llmProvider === "openai" ? "openai" : null;
|
||||||
|
return {
|
||||||
|
attempted: false,
|
||||||
|
applied: false,
|
||||||
|
provider,
|
||||||
|
traceId: null,
|
||||||
|
effectiveMessage: userMessage,
|
||||||
|
reason: "disabled_by_feature_flag",
|
||||||
|
llmCanonicalCandidateDetected: false,
|
||||||
|
predecomposeContract: buildAddressLlmPredecomposeContractV1({
|
||||||
|
sourceMessage: userMessage,
|
||||||
|
canonicalMessage: userMessage
|
||||||
|
}),
|
||||||
|
fallbackRuleHit: null,
|
||||||
|
sanitizedUserMessage: sanitizeAddressMessageForFallback(userMessage),
|
||||||
|
toolGateDecision: null,
|
||||||
|
toolGateReason: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function buildAssistantAddressOrchestrationRuntime(
|
||||||
|
input: BuildAssistantAddressOrchestrationRuntimeInput
|
||||||
|
): Promise<BuildAssistantAddressOrchestrationRuntimeOutput> {
|
||||||
|
const addressPreDecompose = input.featureAddressLlmPredecomposeV1
|
||||||
|
? await input.runAddressLlmPreDecompose()
|
||||||
|
: fallbackAddressPreDecompose(
|
||||||
|
input.userMessage,
|
||||||
|
input.llmProvider,
|
||||||
|
input.buildAddressLlmPredecomposeContractV1,
|
||||||
|
input.sanitizeAddressMessageForFallback
|
||||||
|
);
|
||||||
|
|
||||||
|
const addressInputMessage =
|
||||||
|
input.toNonEmptyString(addressPreDecompose?.effectiveMessage) ?? input.userMessage;
|
||||||
|
const carryover = input.resolveAddressFollowupCarryoverContext(
|
||||||
|
input.userMessage,
|
||||||
|
input.sessionItems,
|
||||||
|
addressInputMessage,
|
||||||
|
addressPreDecompose
|
||||||
|
);
|
||||||
|
const followupContext = carryover?.followupContext ?? null;
|
||||||
|
const orchestrationDecision = input.resolveAssistantOrchestrationDecision({
|
||||||
|
rawUserMessage: input.userMessage,
|
||||||
|
effectiveAddressUserMessage: addressInputMessage,
|
||||||
|
followupContext,
|
||||||
|
llmPreDecomposeMeta: addressPreDecompose,
|
||||||
|
useMock: input.useMock
|
||||||
|
});
|
||||||
|
const dialogContinuationContract = input.buildAddressDialogContinuationContractV2(
|
||||||
|
input.userMessage,
|
||||||
|
addressInputMessage,
|
||||||
|
carryover,
|
||||||
|
addressPreDecompose
|
||||||
|
);
|
||||||
|
const addressRuntimeMeta = {
|
||||||
|
...addressPreDecompose,
|
||||||
|
toolGateDecision: orchestrationDecision.toolGateDecision ?? null,
|
||||||
|
toolGateReason: orchestrationDecision.toolGateReason ?? null,
|
||||||
|
dialogContinuationContract,
|
||||||
|
orchestrationContract: orchestrationDecision.orchestrationContract ?? null
|
||||||
|
};
|
||||||
|
const livingModeDecision = {
|
||||||
|
mode: orchestrationDecision.livingMode,
|
||||||
|
reason: orchestrationDecision.livingReason
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
addressPreDecompose,
|
||||||
|
addressInputMessage,
|
||||||
|
carryover,
|
||||||
|
orchestrationDecision,
|
||||||
|
addressRuntimeMeta,
|
||||||
|
livingModeDecision
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -29,6 +29,8 @@ import * as assistantDeepTurnGroundingRuntimeAdapter_1 from "./assistantDeepTurn
|
||||||
import * as assistantDeepTurnPackagingRuntimeAdapter_1 from "./assistantDeepTurnPackagingRuntimeAdapter";
|
import * as assistantDeepTurnPackagingRuntimeAdapter_1 from "./assistantDeepTurnPackagingRuntimeAdapter";
|
||||||
import * as assistantDeepTurnPlanRuntimeAdapter_1 from "./assistantDeepTurnPlanRuntimeAdapter";
|
import * as assistantDeepTurnPlanRuntimeAdapter_1 from "./assistantDeepTurnPlanRuntimeAdapter";
|
||||||
import * as assistantDeepTurnRetrievalRuntimeAdapter_1 from "./assistantDeepTurnRetrievalRuntimeAdapter";
|
import * as assistantDeepTurnRetrievalRuntimeAdapter_1 from "./assistantDeepTurnRetrievalRuntimeAdapter";
|
||||||
|
import * as assistantAddressLaneRuntimeAdapter_1 from "./assistantAddressLaneRuntimeAdapter";
|
||||||
|
import * as assistantAddressOrchestrationRuntimeAdapter_1 from "./assistantAddressOrchestrationRuntimeAdapter";
|
||||||
import * as assistantLivingChatTurnFinalizeRuntimeAdapter_1 from "./assistantLivingChatTurnFinalizeRuntimeAdapter";
|
import * as assistantLivingChatTurnFinalizeRuntimeAdapter_1 from "./assistantLivingChatTurnFinalizeRuntimeAdapter";
|
||||||
import * as assistantLivingChatRuntimeAdapter_1 from "./assistantLivingChatRuntimeAdapter";
|
import * as assistantLivingChatRuntimeAdapter_1 from "./assistantLivingChatRuntimeAdapter";
|
||||||
import * as assistantQueryPlanning_1 from "./assistantQueryPlanning";
|
import * as assistantQueryPlanning_1 from "./assistantQueryPlanning";
|
||||||
|
|
@ -4515,47 +4517,27 @@ export class AssistantService {
|
||||||
};
|
};
|
||||||
let addressRuntimeMetaForDeep = null;
|
let addressRuntimeMetaForDeep = null;
|
||||||
if (config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_V1) {
|
if (config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_V1) {
|
||||||
const addressPreDecompose = config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1
|
const addressOrchestrationRuntime = await (0, assistantAddressOrchestrationRuntimeAdapter_1.buildAssistantAddressOrchestrationRuntime)({
|
||||||
? await runAddressLlmPreDecompose(this.normalizerService, payload, userMessage)
|
userMessage,
|
||||||
: {
|
sessionItems: session.items,
|
||||||
attempted: false,
|
llmProvider: payload?.llmProvider,
|
||||||
applied: false,
|
useMock: Boolean(payload.useMock),
|
||||||
provider: payload?.llmProvider === "local" ? "local" : payload?.llmProvider === "openai" ? "openai" : null,
|
featureAddressLlmPredecomposeV1: config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1,
|
||||||
traceId: null,
|
runAddressLlmPreDecompose: async () => runAddressLlmPreDecompose(this.normalizerService, payload, userMessage),
|
||||||
effectiveMessage: userMessage,
|
buildAddressLlmPredecomposeContractV1: predecomposeContract_1.buildAddressLlmPredecomposeContractV1,
|
||||||
reason: "disabled_by_feature_flag",
|
sanitizeAddressMessageForFallback,
|
||||||
llmCanonicalCandidateDetected: false,
|
toNonEmptyString,
|
||||||
predecomposeContract: (0, predecomposeContract_1.buildAddressLlmPredecomposeContractV1)({
|
resolveAddressFollowupCarryoverContext,
|
||||||
sourceMessage: userMessage,
|
resolveAssistantOrchestrationDecision,
|
||||||
canonicalMessage: userMessage
|
buildAddressDialogContinuationContractV2
|
||||||
}),
|
|
||||||
fallbackRuleHit: null,
|
|
||||||
sanitizedUserMessage: sanitizeAddressMessageForFallback(userMessage),
|
|
||||||
toolGateDecision: null,
|
|
||||||
toolGateReason: null
|
|
||||||
};
|
|
||||||
const addressInputMessage = toNonEmptyString(addressPreDecompose?.effectiveMessage) ?? userMessage;
|
|
||||||
const carryover = resolveAddressFollowupCarryoverContext(userMessage, session.items, addressInputMessage, addressPreDecompose);
|
|
||||||
const orchestrationDecision = resolveAssistantOrchestrationDecision({
|
|
||||||
rawUserMessage: userMessage,
|
|
||||||
effectiveAddressUserMessage: addressInputMessage,
|
|
||||||
followupContext: carryover?.followupContext ?? null,
|
|
||||||
llmPreDecomposeMeta: addressPreDecompose,
|
|
||||||
useMock: Boolean(payload.useMock)
|
|
||||||
});
|
});
|
||||||
const dialogContinuationContract = buildAddressDialogContinuationContractV2(userMessage, addressInputMessage, carryover, addressPreDecompose);
|
const addressPreDecompose = addressOrchestrationRuntime.addressPreDecompose;
|
||||||
const addressRuntimeMeta = {
|
const addressInputMessage = addressOrchestrationRuntime.addressInputMessage;
|
||||||
...addressPreDecompose,
|
const carryover = addressOrchestrationRuntime.carryover;
|
||||||
toolGateDecision: orchestrationDecision.toolGateDecision,
|
const orchestrationDecision = addressOrchestrationRuntime.orchestrationDecision;
|
||||||
toolGateReason: orchestrationDecision.toolGateReason,
|
const addressRuntimeMeta = addressOrchestrationRuntime.addressRuntimeMeta;
|
||||||
dialogContinuationContract,
|
|
||||||
orchestrationContract: orchestrationDecision.orchestrationContract
|
|
||||||
};
|
|
||||||
addressRuntimeMetaForDeep = addressRuntimeMeta;
|
addressRuntimeMetaForDeep = addressRuntimeMeta;
|
||||||
const livingModeDecision = {
|
const livingModeDecision = addressOrchestrationRuntime.livingModeDecision;
|
||||||
mode: orchestrationDecision.livingMode,
|
|
||||||
reason: orchestrationDecision.livingReason
|
|
||||||
};
|
|
||||||
if (!orchestrationDecision.runAddressLane) {
|
if (!orchestrationDecision.runAddressLane) {
|
||||||
(0, log_1.logJson)({
|
(0, log_1.logJson)({
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
|
|
@ -4592,42 +4574,6 @@ export class AssistantService {
|
||||||
const analysisDateHint = runtimeAnalysisContext.as_of_date ?? toNonEmptyString(payload?.context?.period_hint);
|
const analysisDateHint = runtimeAnalysisContext.as_of_date ?? toNonEmptyString(payload?.context?.period_hint);
|
||||||
const canRetryWithRawUserMessage = compactWhitespace(String(addressInputMessage ?? "").toLowerCase()) !==
|
const canRetryWithRawUserMessage = compactWhitespace(String(addressInputMessage ?? "").toLowerCase()) !==
|
||||||
compactWhitespace(String(userMessage ?? "").toLowerCase());
|
compactWhitespace(String(userMessage ?? "").toLowerCase());
|
||||||
const retryAudit = {
|
|
||||||
attempted: false,
|
|
||||||
reason: null,
|
|
||||||
initial_limited_category: null,
|
|
||||||
retry_message: null,
|
|
||||||
retry_used_followup_context: false,
|
|
||||||
retry_result_category: null
|
|
||||||
};
|
|
||||||
const withRetryMeta = () => ({
|
|
||||||
...addressRuntimeMeta,
|
|
||||||
addressRetryAudit: { ...retryAudit }
|
|
||||||
});
|
|
||||||
let pendingLimited = null;
|
|
||||||
const evaluateAddressLane = (addressLane, messageUsed, carryMeta) => {
|
|
||||||
if (!addressLane?.handled) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!isRetryableAddressLimitedResult(addressLane)) {
|
|
||||||
return {
|
|
||||||
action: "return",
|
|
||||||
addressLane,
|
|
||||||
messageUsed,
|
|
||||||
carryMeta
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (!pendingLimited) {
|
|
||||||
pendingLimited = {
|
|
||||||
addressLane,
|
|
||||||
messageUsed,
|
|
||||||
carryMeta
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
action: "continue"
|
|
||||||
};
|
|
||||||
};
|
|
||||||
const runAddressLaneAttempt = async (messageUsed, carryMeta) => {
|
const runAddressLaneAttempt = async (messageUsed, carryMeta) => {
|
||||||
const scopedFollowupContext = mergeFollowupContextWithOrganizationScope(carryMeta?.followupContext ?? null, sessionOrganizationScope.activeOrganization);
|
const scopedFollowupContext = mergeFollowupContextWithOrganizationScope(carryMeta?.followupContext ?? null, sessionOrganizationScope.activeOrganization);
|
||||||
if (scopedFollowupContext) {
|
if (scopedFollowupContext) {
|
||||||
|
|
@ -4640,48 +4586,20 @@ export class AssistantService {
|
||||||
analysisDateHint
|
analysisDateHint
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
if (shouldPreferContextualLane) {
|
const addressLaneRuntime = await (0, assistantAddressLaneRuntimeAdapter_1.runAssistantAddressLaneRuntime)({
|
||||||
const contextualAddressLane = await runAddressLaneAttempt(addressInputMessage, carryover);
|
userMessage,
|
||||||
const decision = evaluateAddressLane(contextualAddressLane, addressInputMessage, carryover);
|
addressInputMessage,
|
||||||
if (decision?.action === "return") {
|
carryover,
|
||||||
return finalizeAddressLaneResponse(decision.addressLane, decision.messageUsed, decision.carryMeta, withRetryMeta());
|
shouldPreferContextualLane,
|
||||||
}
|
canRetryWithRawUserMessage,
|
||||||
}
|
runAddressLaneAttempt,
|
||||||
const primaryAddressLane = await runAddressLaneAttempt(addressInputMessage, null);
|
isRetryableAddressLimitedResult
|
||||||
const primaryDecision = evaluateAddressLane(primaryAddressLane, addressInputMessage, null);
|
});
|
||||||
if (primaryDecision?.action === "return") {
|
if (addressLaneRuntime.handled && addressLaneRuntime.selection) {
|
||||||
return finalizeAddressLaneResponse(primaryDecision.addressLane, primaryDecision.messageUsed, primaryDecision.carryMeta, withRetryMeta());
|
return finalizeAddressLaneResponse(addressLaneRuntime.selection.addressLane, addressLaneRuntime.selection.messageUsed, addressLaneRuntime.selection.carryMeta, {
|
||||||
}
|
...addressRuntimeMeta,
|
||||||
if (!shouldPreferContextualLane && carryover?.followupContext) {
|
addressRetryAudit: { ...addressLaneRuntime.retryAudit }
|
||||||
const contextualAddressLane = await runAddressLaneAttempt(addressInputMessage, carryover);
|
});
|
||||||
const contextualDecision = evaluateAddressLane(contextualAddressLane, addressInputMessage, carryover);
|
|
||||||
if (contextualDecision?.action === "return") {
|
|
||||||
return finalizeAddressLaneResponse(contextualDecision.addressLane, contextualDecision.messageUsed, contextualDecision.carryMeta, withRetryMeta());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (pendingLimited && canRetryWithRawUserMessage) {
|
|
||||||
retryAudit.attempted = true;
|
|
||||||
retryAudit.reason = "limited_result_retry_with_raw_message";
|
|
||||||
retryAudit.initial_limited_category = pendingLimited.addressLane?.debug?.limited_reason_category ?? null;
|
|
||||||
retryAudit.retry_message = userMessage;
|
|
||||||
if (carryover?.followupContext) {
|
|
||||||
retryAudit.retry_used_followup_context = true;
|
|
||||||
const rawContextualLane = await runAddressLaneAttempt(userMessage, carryover);
|
|
||||||
const rawContextualDecision = evaluateAddressLane(rawContextualLane, userMessage, carryover);
|
|
||||||
if (rawContextualDecision?.action === "return") {
|
|
||||||
retryAudit.retry_result_category = rawContextualDecision.addressLane?.debug?.limited_reason_category ?? null;
|
|
||||||
return finalizeAddressLaneResponse(rawContextualDecision.addressLane, rawContextualDecision.messageUsed, rawContextualDecision.carryMeta, withRetryMeta());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const rawPrimaryLane = await runAddressLaneAttempt(userMessage, null);
|
|
||||||
retryAudit.retry_result_category = rawPrimaryLane?.debug?.limited_reason_category ?? null;
|
|
||||||
const rawPrimaryDecision = evaluateAddressLane(rawPrimaryLane, userMessage, null);
|
|
||||||
if (rawPrimaryDecision?.action === "return") {
|
|
||||||
return finalizeAddressLaneResponse(rawPrimaryDecision.addressLane, rawPrimaryDecision.messageUsed, rawPrimaryDecision.carryMeta, withRetryMeta());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (pendingLimited) {
|
|
||||||
return finalizeAddressLaneResponse(pendingLimited.addressLane, pendingLimited.messageUsed, pendingLimited.carryMeta, withRetryMeta());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import {
|
||||||
|
runAssistantAddressLaneRuntime,
|
||||||
|
type AssistantAddressFollowupCarryoverLike,
|
||||||
|
type AssistantAddressLaneLike
|
||||||
|
} from "../src/services/assistantAddressLaneRuntimeAdapter";
|
||||||
|
|
||||||
|
function limitedLane(category: string): AssistantAddressLaneLike {
|
||||||
|
return {
|
||||||
|
handled: true,
|
||||||
|
debug: {
|
||||||
|
limited_reason_category: category
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function factualLane(): AssistantAddressLaneLike {
|
||||||
|
return {
|
||||||
|
handled: true,
|
||||||
|
debug: {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function unhandledLane(): AssistantAddressLaneLike {
|
||||||
|
return {
|
||||||
|
handled: false,
|
||||||
|
debug: {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("assistant address lane runtime adapter", () => {
|
||||||
|
it("returns contextual lane immediately when preferred contextual attempt is factual", async () => {
|
||||||
|
const carryover: AssistantAddressFollowupCarryoverLike = { followupContext: { scope: "ctx" } };
|
||||||
|
const runAddressLaneAttempt = vi.fn(async () => factualLane());
|
||||||
|
|
||||||
|
const result = await runAssistantAddressLaneRuntime({
|
||||||
|
userMessage: "сырой вопрос",
|
||||||
|
addressInputMessage: "нормализованный вопрос",
|
||||||
|
carryover,
|
||||||
|
shouldPreferContextualLane: true,
|
||||||
|
canRetryWithRawUserMessage: true,
|
||||||
|
runAddressLaneAttempt,
|
||||||
|
isRetryableAddressLimitedResult: (lane) => Boolean(lane?.debug?.limited_reason_category)
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.handled).toBe(true);
|
||||||
|
expect(result.selection?.messageUsed).toBe("нормализованный вопрос");
|
||||||
|
expect(result.selection?.carryMeta).toBe(carryover);
|
||||||
|
expect(result.retryAudit.attempted).toBe(false);
|
||||||
|
expect(runAddressLaneAttempt).toHaveBeenCalledTimes(1);
|
||||||
|
expect(runAddressLaneAttempt).toHaveBeenCalledWith("нормализованный вопрос", carryover);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("retries with raw message after limited result and returns factual retry", async () => {
|
||||||
|
const carryover: AssistantAddressFollowupCarryoverLike = { followupContext: { scope: "ctx" } };
|
||||||
|
const runAddressLaneAttempt = vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce(limitedLane("empty_match")) // primary
|
||||||
|
.mockResolvedValueOnce(limitedLane("empty_match")) // contextual
|
||||||
|
.mockResolvedValueOnce(factualLane()); // raw contextual retry
|
||||||
|
|
||||||
|
const result = await runAssistantAddressLaneRuntime({
|
||||||
|
userMessage: "сырой вопрос",
|
||||||
|
addressInputMessage: "нормализованный вопрос",
|
||||||
|
carryover,
|
||||||
|
shouldPreferContextualLane: false,
|
||||||
|
canRetryWithRawUserMessage: true,
|
||||||
|
runAddressLaneAttempt,
|
||||||
|
isRetryableAddressLimitedResult: (lane) => Boolean(lane?.debug?.limited_reason_category)
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.handled).toBe(true);
|
||||||
|
expect(result.selection?.messageUsed).toBe("сырой вопрос");
|
||||||
|
expect(result.selection?.carryMeta).toBe(carryover);
|
||||||
|
expect(result.retryAudit.attempted).toBe(true);
|
||||||
|
expect(result.retryAudit.reason).toBe("limited_result_retry_with_raw_message");
|
||||||
|
expect(result.retryAudit.initial_limited_category).toBe("empty_match");
|
||||||
|
expect(result.retryAudit.retry_used_followup_context).toBe(true);
|
||||||
|
expect(result.retryAudit.retry_result_category).toBe(null);
|
||||||
|
expect(runAddressLaneAttempt).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns pending limited result when retry is disabled", async () => {
|
||||||
|
const runAddressLaneAttempt = vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce(limitedLane("missing_anchor")) // primary
|
||||||
|
.mockResolvedValueOnce(unhandledLane()); // contextual fallback
|
||||||
|
|
||||||
|
const result = await runAssistantAddressLaneRuntime({
|
||||||
|
userMessage: "сырой вопрос",
|
||||||
|
addressInputMessage: "нормализованный вопрос",
|
||||||
|
carryover: { followupContext: { scope: "ctx" } },
|
||||||
|
shouldPreferContextualLane: false,
|
||||||
|
canRetryWithRawUserMessage: false,
|
||||||
|
runAddressLaneAttempt,
|
||||||
|
isRetryableAddressLimitedResult: (lane) => Boolean(lane?.debug?.limited_reason_category)
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.handled).toBe(true);
|
||||||
|
expect(result.selection?.messageUsed).toBe("нормализованный вопрос");
|
||||||
|
expect(result.selection?.addressLane.debug?.limited_reason_category).toBe("missing_anchor");
|
||||||
|
expect(result.retryAudit.attempted).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { buildAssistantAddressOrchestrationRuntime } from "../src/services/assistantAddressOrchestrationRuntimeAdapter";
|
||||||
|
|
||||||
|
function buildInput(overrides: Record<string, unknown> = {}) {
|
||||||
|
const runAddressLlmPreDecompose = vi.fn(async () => ({
|
||||||
|
attempted: true,
|
||||||
|
applied: true,
|
||||||
|
effectiveMessage: "канон",
|
||||||
|
reason: "normalized_fragment_applied"
|
||||||
|
}));
|
||||||
|
const resolveAddressFollowupCarryoverContext = vi.fn(() => ({
|
||||||
|
followupContext: { id: "ctx" }
|
||||||
|
}));
|
||||||
|
const resolveAssistantOrchestrationDecision = vi.fn(() => ({
|
||||||
|
runAddressLane: true,
|
||||||
|
livingMode: "deep_analysis",
|
||||||
|
livingReason: "address_mode_classifier_detected",
|
||||||
|
toolGateDecision: "run_address_lane",
|
||||||
|
toolGateReason: "address_mode_classifier_detected",
|
||||||
|
orchestrationContract: { schema_version: "assistant_orchestration_contract_v1" }
|
||||||
|
}));
|
||||||
|
const buildAddressDialogContinuationContractV2 = vi.fn(() => ({
|
||||||
|
schema_version: "address_dialog_continuation_contract_v2"
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
userMessage: "сырой вопрос",
|
||||||
|
sessionItems: [],
|
||||||
|
llmProvider: "openai",
|
||||||
|
useMock: false,
|
||||||
|
featureAddressLlmPredecomposeV1: true,
|
||||||
|
runAddressLlmPreDecompose,
|
||||||
|
buildAddressLlmPredecomposeContractV1: () => ({ schema_version: "address_llm_predecompose_contract_v1" }),
|
||||||
|
sanitizeAddressMessageForFallback: () => "sanitized",
|
||||||
|
toNonEmptyString: (value: unknown) => {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const trimmed = value.trim();
|
||||||
|
return trimmed.length > 0 ? trimmed : null;
|
||||||
|
},
|
||||||
|
resolveAddressFollowupCarryoverContext,
|
||||||
|
resolveAssistantOrchestrationDecision,
|
||||||
|
buildAddressDialogContinuationContractV2,
|
||||||
|
__spies: {
|
||||||
|
runAddressLlmPreDecompose,
|
||||||
|
resolveAddressFollowupCarryoverContext,
|
||||||
|
resolveAssistantOrchestrationDecision,
|
||||||
|
buildAddressDialogContinuationContractV2
|
||||||
|
},
|
||||||
|
...overrides
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("assistant address orchestration runtime adapter", () => {
|
||||||
|
it("uses llm predecompose payload when feature is enabled", async () => {
|
||||||
|
const input = buildInput();
|
||||||
|
|
||||||
|
const output = await buildAssistantAddressOrchestrationRuntime(input);
|
||||||
|
|
||||||
|
expect(output.addressPreDecompose.reason).toBe("normalized_fragment_applied");
|
||||||
|
expect(output.addressInputMessage).toBe("канон");
|
||||||
|
expect(output.orchestrationDecision.runAddressLane).toBe(true);
|
||||||
|
expect(output.livingModeDecision.mode).toBe("deep_analysis");
|
||||||
|
expect(output.addressRuntimeMeta.toolGateDecision).toBe("run_address_lane");
|
||||||
|
expect(output.addressRuntimeMeta.dialogContinuationContract).toEqual({
|
||||||
|
schema_version: "address_dialog_continuation_contract_v2"
|
||||||
|
});
|
||||||
|
expect(input.__spies.runAddressLlmPreDecompose).toHaveBeenCalledTimes(1);
|
||||||
|
expect(input.__spies.resolveAddressFollowupCarryoverContext).toHaveBeenCalledTimes(1);
|
||||||
|
expect(input.__spies.resolveAssistantOrchestrationDecision).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds deterministic fallback predecompose payload when feature is disabled", async () => {
|
||||||
|
const input = buildInput({
|
||||||
|
featureAddressLlmPredecomposeV1: false,
|
||||||
|
llmProvider: "local",
|
||||||
|
runAddressLlmPreDecompose: vi.fn(async () => {
|
||||||
|
throw new Error("must not be called");
|
||||||
|
}),
|
||||||
|
resolveAssistantOrchestrationDecision: vi.fn(() => ({
|
||||||
|
runAddressLane: false,
|
||||||
|
livingMode: "chat",
|
||||||
|
livingReason: "predecompose_unsupported_mode",
|
||||||
|
toolGateDecision: "skip_address_lane",
|
||||||
|
toolGateReason: "llm_predecompose_unsupported_mode",
|
||||||
|
orchestrationContract: { schema_version: "assistant_orchestration_contract_v1" }
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
|
const output = await buildAssistantAddressOrchestrationRuntime(input);
|
||||||
|
|
||||||
|
expect(output.addressPreDecompose.attempted).toBe(false);
|
||||||
|
expect(output.addressPreDecompose.applied).toBe(false);
|
||||||
|
expect(output.addressPreDecompose.provider).toBe("local");
|
||||||
|
expect(output.addressPreDecompose.reason).toBe("disabled_by_feature_flag");
|
||||||
|
expect(output.addressPreDecompose.sanitizedUserMessage).toBe("sanitized");
|
||||||
|
expect(output.addressInputMessage).toBe("сырой вопрос");
|
||||||
|
expect(output.livingModeDecision.mode).toBe("chat");
|
||||||
|
expect(output.addressRuntimeMeta.toolGateDecision).toBe("skip_address_lane");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
Loading…
Reference in New Issue