838 lines
36 KiB
TypeScript
838 lines
36 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
||
import { createAssistantRoutePolicy } from "../src/services/assistantRoutePolicy";
|
||
|
||
function toNonEmptyString(value: unknown): string | null {
|
||
if (value === null || value === undefined) {
|
||
return null;
|
||
}
|
||
const text = String(value).trim();
|
||
return text.length > 0 ? text : null;
|
||
}
|
||
|
||
function normalizeOrganizationScopeValue(value: unknown): string | null {
|
||
const text = toNonEmptyString(value);
|
||
if (!text) {
|
||
return null;
|
||
}
|
||
return text.replace(/^"+|"+$/g, "").replace(/^'+|'+$/g, "");
|
||
}
|
||
|
||
function buildPolicy(overrides: Record<string, unknown> = {}) {
|
||
return createAssistantRoutePolicy({
|
||
repairAddressMojibake: (text: string) => text,
|
||
findLastGroundedAddressAnswerDebug: () => null,
|
||
findLastOrganizationClarificationAddressDebug: () => null,
|
||
mergeKnownOrganizations: (values: unknown[]) =>
|
||
Array.from(
|
||
new Set(
|
||
(Array.isArray(values) ? values : [])
|
||
.map((item) => normalizeOrganizationScopeValue(item))
|
||
.filter((item): item is string => Boolean(item))
|
||
)
|
||
),
|
||
normalizeOrganizationScopeValue,
|
||
resolveOrganizationSelectionFromMessage: () => null,
|
||
resolveMetaSignalSet: (input: {
|
||
rawUserMessage?: string;
|
||
repairedRawUserMessage?: string;
|
||
effectiveAddressUserMessage?: string;
|
||
repairedEffectiveAddressUserMessage?: string;
|
||
}) => {
|
||
const samples = [
|
||
input.rawUserMessage,
|
||
input.repairedRawUserMessage,
|
||
input.effectiveAddressUserMessage,
|
||
input.repairedEffectiveAddressUserMessage
|
||
].join(" ");
|
||
return {
|
||
dataScopeMetaQuery: /по какой компании|какая база|по каким конторам/i.test(samples),
|
||
capabilityMetaQuery: /что ты можешь|что ты умеешь/i.test(samples),
|
||
metaAnswerFollowupSignal: /это норм|что думаешь/i.test(samples),
|
||
answerInspectionFollowupSignal: /это ошибка|у тебя написано кто контрагент/i.test(samples)
|
||
};
|
||
},
|
||
resolveHardMetaMode: (input: {
|
||
dataScopeMetaQuery?: boolean;
|
||
capabilityMetaQuery?: boolean;
|
||
dataRetrievalSignal?: boolean;
|
||
}) =>
|
||
input.dataScopeMetaQuery
|
||
? "data_scope"
|
||
: input.capabilityMetaQuery && !input.dataRetrievalSignal
|
||
? "capability"
|
||
: null,
|
||
isMetaFollowupOverGroundedAnswer: () => false,
|
||
isAnswerInspectionFollowupOverGroundedAnswer: () => false,
|
||
hasDataRetrievalRequestSignal: () => false,
|
||
hasOrganizationFactLookupSignal: () => false,
|
||
hasOrganizationFactFollowupSignal: () => false,
|
||
hasAggregateBusinessAnalyticsSignal: () => false,
|
||
hasStandaloneAddressTopicSignal: () => false,
|
||
hasOpenContractsAddressSignal: () => false,
|
||
detectAddressQuestionMode: () => ({ mode: "unsupported", confidence: "low" }),
|
||
resolveAddressIntent: () => ({ intent: "unknown", confidence: "low" }),
|
||
toNonEmptyString,
|
||
hasStrictDeepInvestigationCue: () => false,
|
||
hasStrongDataIntentSignal: () => false,
|
||
hasAccountingSignal: () => false,
|
||
hasDangerOrCoercionSignal: () => false,
|
||
hasAddressFollowupContextSignal: () => false,
|
||
hasShortDebtMirrorFollowupSignal: () => false,
|
||
isInventorySelectedObjectIntent: (intent: unknown) => /inventory/i.test(String(intent ?? "")),
|
||
hasShortInventoryObjectFollowupSignal: () => false,
|
||
resolveRouteMemorySignals: () => ({
|
||
contextualHistoricalCapabilityFollowupDetected: false,
|
||
contextualMemoryRecapFollowupDetected: false
|
||
}),
|
||
findLastAddressAssistantItem: () => null,
|
||
hasAddressLlmPreDecomposeCandidate: () => false,
|
||
resolveAddressToolGateDecision: () => ({
|
||
runAddressLane: false,
|
||
decision: "skip_address_lane",
|
||
reason: "no_address_signal_after_l0"
|
||
}),
|
||
hasSameDateAccountFollowupSignalForPredecompose: () => false,
|
||
hasLooseAllTimeAddressLookupSignal: () => false,
|
||
hasDeepAnalysisPreferenceSignal: () => false,
|
||
hasDirectDeepAnalysisSignal: () => false,
|
||
shouldEmitOrganizationSelectionReply: () => false,
|
||
compactWhitespace: (text: string) => String(text ?? "").replace(/\s+/g, " ").trim(),
|
||
hasDeepSessionContinuationSignal: () => false,
|
||
resolveLivingAssistantModeDecision: (input: { addressLaneTriggered?: boolean }) =>
|
||
input.addressLaneTriggered
|
||
? { mode: "address_data", reason: "address_lane_triggered" }
|
||
: { mode: "chat", reason: "living_chat_signal_detected" },
|
||
resolveProviderExecutionState: (input: { useMock?: unknown; llmPreDecomposeReason?: unknown }) => {
|
||
const reason = String(input?.llmPreDecomposeReason ?? "");
|
||
return {
|
||
provider_mode: Boolean(input?.useMock) ? "mock" : "unknown",
|
||
normalized_provider: null,
|
||
use_mock: Boolean(input?.useMock),
|
||
llm_runtime_unavailable_detected: /missing api key|authentication|api key is missing/i.test(reason),
|
||
living_mode_forced_deep: Boolean(input?.useMock),
|
||
living_mode_forced_reason: Boolean(input?.useMock) ? "mock_mode_keeps_deep_pipeline" : null
|
||
};
|
||
},
|
||
...overrides
|
||
});
|
||
}
|
||
|
||
describe("assistantRoutePolicy", () => {
|
||
it("routes data-scope meta question to chat contract", () => {
|
||
const policy = buildPolicy();
|
||
|
||
const decision = policy.resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: "по какой компании мы можем работать?",
|
||
effectiveAddressUserMessage: "по какой компании мы можем работать?",
|
||
followupContext: null,
|
||
llmPreDecomposeMeta: null,
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(false);
|
||
expect(decision.toolGateReason).toBe("assistant_data_scope_query_detected");
|
||
expect(decision.livingMode).toBe("chat");
|
||
expect(decision.orchestrationContract?.hard_meta_mode).toBe("data_scope");
|
||
expect(decision.orchestrationContract?.provider_execution?.provider_mode).toBe("unknown");
|
||
});
|
||
|
||
it("keeps supported address intent in address lane", () => {
|
||
const policy = buildPolicy({
|
||
detectAddressQuestionMode: () => ({ mode: "address_query", confidence: "high" }),
|
||
resolveAddressIntent: () => ({ intent: "inventory_on_hand_as_of_date", confidence: "high" }),
|
||
resolveAddressToolGateDecision: () => ({
|
||
runAddressLane: true,
|
||
decision: "run_address_lane",
|
||
reason: "address_mode_classifier_detected"
|
||
})
|
||
});
|
||
|
||
const decision = policy.resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: "какие товары сейчас лежат на складе",
|
||
effectiveAddressUserMessage: "какие товары сейчас лежат на складе",
|
||
followupContext: null,
|
||
llmPreDecomposeMeta: null,
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(true);
|
||
expect(decision.toolGateReason).toBe("address_mode_classifier_detected");
|
||
expect(decision.livingMode).toBe("address_data");
|
||
expect(decision.orchestrationContract?.semantic_route_arbitration?.supported_address_intent_detected).toBe(true);
|
||
});
|
||
|
||
it("uses internal route-policy tool gate when no external override is provided", () => {
|
||
const policy = buildPolicy({
|
||
resolveAddressToolGateDecision: undefined,
|
||
detectAddressQuestionMode: () => ({ mode: "address_query", confidence: "high" }),
|
||
resolveAddressIntent: () => ({ intent: "inventory_on_hand_as_of_date", confidence: "high" }),
|
||
hasAddressLlmPreDecomposeCandidate: () => true
|
||
});
|
||
|
||
const decision = policy.resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: "какие остатки на складе",
|
||
effectiveAddressUserMessage: "какие остатки на складе",
|
||
followupContext: null,
|
||
llmPreDecomposeMeta: null,
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(true);
|
||
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||
expect(decision.toolGateReason).toBe("address_mode_classifier_detected");
|
||
expect(decision.livingMode).toBe("address_data");
|
||
});
|
||
|
||
it("does not let deep session continuation override an exact VAT period route", () => {
|
||
const policy = buildPolicy({
|
||
detectAddressQuestionMode: () => ({ mode: "address_query", confidence: "high" }),
|
||
resolveAddressIntent: () => ({ intent: "vat_liability_confirmed_for_tax_period", confidence: "high" }),
|
||
resolveAddressToolGateDecision: () => ({
|
||
runAddressLane: true,
|
||
decision: "run_address_lane",
|
||
reason: "address_mode_classifier_detected"
|
||
}),
|
||
hasDeepSessionContinuationSignal: () => true
|
||
});
|
||
|
||
const decision = policy.resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: "а какой НДС мы должны сгрузить на март 2020?",
|
||
effectiveAddressUserMessage: "а какой НДС мы должны сгрузить на март 2020?",
|
||
followupContext: null,
|
||
llmPreDecomposeMeta: {
|
||
applied: true,
|
||
llmCanonicalCandidateDetected: true,
|
||
predecomposeContract: {
|
||
mode: "address_query",
|
||
mode_confidence: "high",
|
||
intent: "vat_liability_confirmed_for_tax_period",
|
||
intent_confidence: "high"
|
||
},
|
||
semanticExtractionContract: {
|
||
valid: true,
|
||
apply_canonical_recommended: true,
|
||
extraction: {
|
||
query_shape: "UNKNOWN",
|
||
aggregation_profile: "unknown"
|
||
},
|
||
guard_hints: {
|
||
deep_investigation_signal_detected: false
|
||
}
|
||
}
|
||
} as any,
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(true);
|
||
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||
expect(decision.toolGateReason).toBe("address_mode_classifier_detected");
|
||
expect(decision.livingMode).toBe("address_data");
|
||
expect(decision.orchestrationContract?.deep_session_continuation_fallback_to_deep).toBe(false);
|
||
});
|
||
|
||
it("keeps an exact address route out of deep fallback when only semantic deep hint is present", () => {
|
||
const policy = buildPolicy({
|
||
detectAddressQuestionMode: () => ({ mode: "address_query", confidence: "high" }),
|
||
resolveAddressIntent: () => ({ intent: "vat_liability_confirmed_for_tax_period", confidence: "high" }),
|
||
resolveAddressToolGateDecision: () => ({
|
||
runAddressLane: true,
|
||
decision: "run_address_lane",
|
||
reason: "address_mode_classifier_detected"
|
||
})
|
||
});
|
||
|
||
const decision = policy.resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: "какой НДС мы должны заплатить на март 2020",
|
||
effectiveAddressUserMessage: "какой НДС мы должны заплатить на март 2020",
|
||
followupContext: null,
|
||
llmPreDecomposeMeta: {
|
||
applied: true,
|
||
llmCanonicalCandidateDetected: true,
|
||
predecomposeContract: {
|
||
mode: "address_query",
|
||
mode_confidence: "high",
|
||
intent: "vat_liability_confirmed_for_tax_period",
|
||
intent_confidence: "high"
|
||
},
|
||
semanticExtractionContract: {
|
||
valid: true,
|
||
apply_canonical_recommended: true,
|
||
extraction: {
|
||
query_shape: "UNKNOWN",
|
||
aggregation_profile: "unknown"
|
||
},
|
||
guard_hints: {
|
||
deep_investigation_signal_detected: true
|
||
}
|
||
}
|
||
} as any,
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(true);
|
||
expect(decision.livingMode).toBe("address_data");
|
||
expect(decision.orchestrationContract?.semantic_route_arbitration?.supported_address_intent_detected).toBe(true);
|
||
expect(decision.orchestrationContract?.deep_analysis_signal_fallback_to_deep).toBe(false);
|
||
});
|
||
|
||
it("routes memory recap follow-up over grounded answer to chat", () => {
|
||
const policy = buildPolicy({
|
||
resolveRouteMemorySignals: () => ({
|
||
contextualHistoricalCapabilityFollowupDetected: false,
|
||
contextualMemoryRecapFollowupDetected: true
|
||
}),
|
||
findLastGroundedAddressAnswerDebug: () => ({ execution_lane: "address_query" })
|
||
});
|
||
|
||
const decision = policy.resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: "а ты помнишь что мы обсуждали?",
|
||
effectiveAddressUserMessage: "а ты помнишь что мы обсуждали?",
|
||
followupContext: null,
|
||
llmPreDecomposeMeta: {
|
||
applied: false,
|
||
reason: "normalized_fragment_rejected_semantic_guard",
|
||
predecomposeContract: {
|
||
mode: "unsupported",
|
||
mode_confidence: "low",
|
||
intent: "unknown",
|
||
intent_confidence: "low"
|
||
}
|
||
},
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(false);
|
||
expect(decision.toolGateReason).toBe("memory_recap_followup_detected");
|
||
expect(decision.livingMode).toBe("chat");
|
||
expect(decision.livingReason).toBe("memory_recap_followup_detected");
|
||
});
|
||
|
||
it("routes answer inspection follow-up over grounded selected-object answer to chat", () => {
|
||
const policy = buildPolicy({
|
||
findLastGroundedAddressAnswerDebug: () => ({ execution_lane: "address_query" }),
|
||
resolveAddressToolGateDecision: () => ({
|
||
runAddressLane: true,
|
||
decision: "run_address_lane",
|
||
reason: "address_mode_classifier_detected"
|
||
}),
|
||
detectAddressQuestionMode: () => ({ mode: "address_query", confidence: "high" }),
|
||
isAnswerInspectionFollowupOverGroundedAnswer: () => true
|
||
});
|
||
|
||
const decision = policy.resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: "у тебя написано кто контрагент: рабочая станция - это ошибка?",
|
||
effectiveAddressUserMessage: "у тебя написано кто контрагент: рабочая станция - это ошибка?",
|
||
followupContext: { previous_intent: "inventory_sale_trace_for_item", previous_anchor_type: "item" },
|
||
llmPreDecomposeMeta: null,
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(false);
|
||
expect(decision.toolGateReason).toBe("answer_inspection_followup_over_grounded_answer");
|
||
expect(decision.livingMode).toBe("chat");
|
||
expect(decision.livingReason).toBe("answer_inspection_followup_detected");
|
||
});
|
||
|
||
it("routes organization fact lookup away from address lane even with follow-up context", () => {
|
||
const policy = buildPolicy({
|
||
hasDataRetrievalRequestSignal: () => true,
|
||
hasStrongDataIntentSignal: () => true,
|
||
detectAddressQuestionMode: () => ({ mode: "address_query", confidence: "high" }),
|
||
resolveAddressIntent: () => ({ intent: "inventory_on_hand_as_of_date", confidence: "low" }),
|
||
hasOrganizationFactLookupSignal: (text: unknown) =>
|
||
/возраст.*альтернатива плюс/i.test(String(text ?? "")),
|
||
resolveAddressToolGateDecision: () => ({
|
||
runAddressLane: true,
|
||
decision: "run_address_lane",
|
||
reason: "address_mode_classifier_detected"
|
||
})
|
||
});
|
||
|
||
const decision = policy.resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: "а какой возраст у Альтернативы Плюс?",
|
||
effectiveAddressUserMessage: "Какой возраст у контрагента Альтернатива Плюс?",
|
||
followupContext: {
|
||
previous_intent: "inventory_on_hand_as_of_date",
|
||
previous_filters: {
|
||
organization: "ООО \"Альтернатива Плюс\"",
|
||
as_of_date: "2021-03-31"
|
||
},
|
||
previous_anchor_type: "item",
|
||
previous_anchor_value: "Модуль прямоугольный 1400*110*750"
|
||
},
|
||
llmPreDecomposeMeta: null,
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(false);
|
||
expect(decision.toolGateReason).toBe("organization_fact_lookup_signal_detected");
|
||
expect(decision.livingMode).toBe("chat");
|
||
expect(decision.livingReason).toBe("organization_fact_lookup_signal_detected");
|
||
});
|
||
|
||
it("routes a late company switch to chat instead of reusing the old address contour", () => {
|
||
const policy = buildPolicy({
|
||
detectAddressQuestionMode: () => ({ mode: "address_query", confidence: "high" }),
|
||
resolveAddressIntent: () => ({ intent: "inventory_on_hand_as_of_date", confidence: "high" }),
|
||
resolveAddressToolGateDecision: () => ({
|
||
runAddressLane: true,
|
||
decision: "run_address_lane",
|
||
reason: "address_mode_classifier_detected"
|
||
}),
|
||
resolveOrganizationSelectionFromMessage: () => "РАЙМ",
|
||
shouldEmitOrganizationSelectionReply: () => true
|
||
});
|
||
|
||
const decision = policy.resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: "теперь давай по РАЙМ",
|
||
effectiveAddressUserMessage: "теперь давай по РАЙМ",
|
||
followupContext: {
|
||
previous_intent: "inventory_on_hand_as_of_date",
|
||
previous_filters: {
|
||
organization: "ООО Альтернатива Плюс",
|
||
as_of_date: "2026-04-19"
|
||
}
|
||
},
|
||
sessionItems: [
|
||
{
|
||
role: "assistant",
|
||
debug: {
|
||
execution_lane: "address_query",
|
||
answer_grounding_check: { status: "grounded" },
|
||
extracted_filters: {
|
||
organization: "ООО Альтернатива Плюс",
|
||
as_of_date: "2026-04-19"
|
||
}
|
||
}
|
||
}
|
||
],
|
||
sessionOrganizationScope: {
|
||
knownOrganizations: ["ООО Альтернатива Плюс", "РАЙМ"],
|
||
selectedOrganization: null,
|
||
activeOrganization: "ООО Альтернатива Плюс"
|
||
},
|
||
llmPreDecomposeMeta: null,
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(false);
|
||
expect(decision.toolGateReason).toBe("organization_scope_switch_detected");
|
||
expect(decision.livingMode).toBe("chat");
|
||
expect(decision.livingReason).toBe("organization_scope_switch_detected");
|
||
expect(decision.orchestrationContract?.organization_scope_switch_detected).toBe(true);
|
||
expect(decision.orchestrationContract?.organization_scope_selection).toBe("РАЙМ");
|
||
});
|
||
|
||
it("routes explicit recap wording with selected-object phrasing to chat even when address-like cues exist", () => {
|
||
const policy = buildPolicy({
|
||
hasStrongDataIntentSignal: () => true,
|
||
hasDataRetrievalRequestSignal: () => true,
|
||
resolveRouteMemorySignals: () => ({
|
||
contextualHistoricalCapabilityFollowupDetected: false,
|
||
contextualMemoryRecapFollowupDetected: true
|
||
}),
|
||
findLastGroundedAddressAnswerDebug: () => ({
|
||
execution_lane: "address_query",
|
||
detected_intent: "inventory_purchase_provenance_for_item"
|
||
})
|
||
});
|
||
|
||
const decision = policy.resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: "а ты помнишь, что мы по этой позиции уже выяснили?",
|
||
effectiveAddressUserMessage: "а ты помнишь, что мы по этой позиции уже выяснили?",
|
||
followupContext: null,
|
||
llmPreDecomposeMeta: {
|
||
applied: false,
|
||
reason: "normalized_fragment_rejected_semantic_guard",
|
||
predecomposeContract: {
|
||
mode: "unsupported",
|
||
mode_confidence: "low",
|
||
intent: "unknown",
|
||
intent_confidence: "low"
|
||
}
|
||
},
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(false);
|
||
expect(decision.toolGateReason).toBe("memory_recap_followup_detected");
|
||
expect(decision.livingMode).toBe("chat");
|
||
expect(decision.livingReason).toBe("memory_recap_followup_detected");
|
||
});
|
||
|
||
it("does not force unsupported-intent fallback when predecompose runtime is unavailable", () => {
|
||
const policy = buildPolicy({
|
||
hasStrongDataIntentSignal: () => true,
|
||
hasDataRetrievalRequestSignal: () => true,
|
||
resolveAddressToolGateDecision: () => ({
|
||
runAddressLane: true,
|
||
decision: "run_address_lane",
|
||
reason: "address_mode_classifier_detected"
|
||
})
|
||
});
|
||
|
||
const decision = policy.resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: "покажи документы по сверке",
|
||
effectiveAddressUserMessage: "покажи документы по сверке",
|
||
followupContext: { root_context_only: true },
|
||
llmPreDecomposeMeta: {
|
||
reason: "error:OpenAI API key is missing",
|
||
predecomposeContract: {
|
||
mode: "unsupported",
|
||
mode_confidence: "low",
|
||
intent: "unknown",
|
||
intent_confidence: "low"
|
||
}
|
||
},
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.toolGateReason).toBe("address_mode_classifier_detected");
|
||
expect(decision.orchestrationContract?.unsupported_address_intent_fallback_to_deep).toBe(false);
|
||
expect(decision.orchestrationContract?.provider_execution?.llm_runtime_unavailable_detected).toBe(true);
|
||
});
|
||
|
||
it("does not classify colloquial VAT root query as non-domain when L0 address gate is positive", () => {
|
||
const policy = buildPolicy({
|
||
hasStrongDataIntentSignal: () => true,
|
||
resolveAddressToolGateDecision: () => ({
|
||
runAddressLane: true,
|
||
decision: "run_address_lane",
|
||
reason: "address_signal_detected"
|
||
})
|
||
});
|
||
|
||
const decision = policy.resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: "скок ндс надо заплатить в налоговую на февраль 2017",
|
||
effectiveAddressUserMessage: "скок ндс надо заплатить в налоговую на февраль 2017",
|
||
followupContext: null,
|
||
llmPreDecomposeMeta: null,
|
||
useMock: true
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(true);
|
||
expect(decision.toolGateReason).toBe("address_signal_detected");
|
||
expect(decision.livingMode).toBe("address_data");
|
||
});
|
||
|
||
it("does not mark organization selection after grounded continuity as non-domain noise", () => {
|
||
const policy = buildPolicy({
|
||
findLastGroundedAddressAnswerDebug: () => null,
|
||
findLastOrganizationClarificationAddressDebug: () => ({
|
||
organization_candidates: ["Org A", "Org B"]
|
||
}),
|
||
resolveOrganizationSelectionFromMessage: (userMessage: unknown, knownOrganizations: unknown) => {
|
||
const normalized = String(userMessage ?? "").trim().toLowerCase();
|
||
const candidates = Array.isArray(knownOrganizations) ? knownOrganizations.map((item) => String(item)) : [];
|
||
return candidates.find((candidate) => candidate.toLowerCase() === normalized) ?? null;
|
||
}
|
||
});
|
||
|
||
const decision = policy.resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: "Org A",
|
||
effectiveAddressUserMessage: "Org A",
|
||
followupContext: null,
|
||
llmPreDecomposeMeta: null,
|
||
sessionItems: [
|
||
{
|
||
role: "assistant",
|
||
debug: {
|
||
execution_lane: "address_query",
|
||
answer_grounding_check: { status: "grounded" },
|
||
extracted_filters: {
|
||
organization: "Org A",
|
||
as_of_date: "2021-03-31"
|
||
},
|
||
detected_intent: "receivables_confirmed_as_of_date"
|
||
}
|
||
}
|
||
],
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(false);
|
||
expect(decision.toolGateReason).toBe("no_address_signal_after_l0");
|
||
expect(decision.livingMode).toBe("chat");
|
||
expect(decision.orchestrationContract?.hard_meta_mode).toBeNull();
|
||
expect(decision.orchestrationContract?.followup_context_detected).toBe(false);
|
||
});
|
||
|
||
it("keeps plain organization clarification in address lane for pending route-candidate scope", () => {
|
||
const orgName =
|
||
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
||
const policy = buildPolicy({
|
||
resolveAddressToolGateDecision: undefined
|
||
});
|
||
|
||
const decision = policy.resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: orgName,
|
||
effectiveAddressUserMessage: orgName,
|
||
followupContext: {
|
||
previous_intent: "customer_revenue_and_payments",
|
||
target_intent: "customer_revenue_and_payments",
|
||
previous_discovery_pilot_scope: "counterparty_value_flow_query_movements_v1",
|
||
previous_discovery_loop_status: "awaiting_clarification",
|
||
previous_discovery_loop_selected_chain_id: "value_flow_ranking",
|
||
previous_discovery_loop_pending_axes: ["organization"],
|
||
previous_discovery_loop_asked_domain_family: "counterparty_value",
|
||
previous_discovery_loop_asked_action_family: "turnover",
|
||
previous_discovery_loop_unsupported_family: "counterparty_value_or_turnover"
|
||
},
|
||
llmPreDecomposeMeta: {
|
||
predecomposeContract: {
|
||
mode: "unsupported",
|
||
intent: "unknown",
|
||
entities: { organization: orgName },
|
||
semantics: { anchor_kind: "organization", anchor_value: orgName }
|
||
}
|
||
},
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(true);
|
||
expect(decision.toolGateReason).toBe("followup_context_detected");
|
||
expect(decision.livingMode).toBe("address_data");
|
||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||
expect(decision.orchestrationContract?.hard_meta_mode).toBeNull();
|
||
});
|
||
|
||
it("does not turn short entity follow-up into organization switch just because scope already has an active company", () => {
|
||
const policy = buildPolicy({
|
||
resolveAddressToolGateDecision: undefined,
|
||
findLastOrganizationClarificationAddressDebug: () => ({
|
||
execution_lane: "address_query",
|
||
limited_reason_category: "missing_anchor",
|
||
organization_candidates: ["ООО Альтернатива Плюс", "ООО Лайсвуд", "РАЙМ"]
|
||
}),
|
||
shouldEmitOrganizationSelectionReply: () => true
|
||
});
|
||
|
||
const decision = policy.resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: "а по свк",
|
||
effectiveAddressUserMessage: "а по свк",
|
||
followupContext: {
|
||
previous_intent: "list_documents_by_counterparty",
|
||
previous_filters: {
|
||
counterparty: "Чепурнов П.Д."
|
||
},
|
||
previous_anchor_type: "counterparty",
|
||
previous_anchor_value: "Чепурнов П.Д."
|
||
},
|
||
sessionItems: [
|
||
{
|
||
role: "assistant",
|
||
debug: {
|
||
execution_lane: "address_query",
|
||
answer_grounding_check: { status: "grounded" },
|
||
detected_intent: "list_documents_by_counterparty",
|
||
extracted_filters: {
|
||
counterparty: "Чепурнов П.Д.",
|
||
organization: "ООО Альтернатива Плюс"
|
||
}
|
||
}
|
||
}
|
||
],
|
||
sessionOrganizationScope: {
|
||
knownOrganizations: ["ООО Альтернатива Плюс", "ООО Лайсвуд", "РАЙМ"],
|
||
selectedOrganization: "ООО Альтернатива Плюс",
|
||
activeOrganization: "ООО Альтернатива Плюс"
|
||
},
|
||
llmPreDecomposeMeta: null,
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.toolGateReason).not.toBe("organization_scope_switch_detected");
|
||
expect(decision.orchestrationContract?.organization_scope_switch_detected).not.toBe(true);
|
||
});
|
||
|
||
it("routes broad business evaluation follow-up to chat instead of replaying lifecycle address intent", () => {
|
||
const policy = buildPolicy({
|
||
resolveAddressIntent: () => ({ intent: "counterparty_activity_lifecycle", confidence: "high" }),
|
||
findLastGroundedAddressAnswerDebug: () => ({
|
||
execution_lane: "address_query",
|
||
answer_grounding_check: { status: "grounded" },
|
||
detected_intent: "counterparty_activity_lifecycle",
|
||
extracted_filters: {
|
||
organization: 'ООО "Альтернатива Плюс"',
|
||
period_to: "2026-04-18"
|
||
}
|
||
}),
|
||
resolveAssistantTurnMeaning: () => ({
|
||
schema_version: "assistant_turn_meaning_v1",
|
||
asked_domain_family: "business_summary",
|
||
asked_action_family: "broad_evaluation",
|
||
explicit_intent_candidate: null,
|
||
unsupported_but_understood_family: "broad_business_evaluation",
|
||
stale_replay_forbidden: true,
|
||
reason_codes: ["broad_business_evaluation_current_turn_signal"]
|
||
}),
|
||
resolveAddressToolGateDecision: () => ({
|
||
runAddressLane: false,
|
||
decision: "skip_address_lane",
|
||
reason: "no_address_signal_after_l0"
|
||
})
|
||
});
|
||
|
||
const decision = policy.resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: "Как ты оценишь деятельность компании?",
|
||
effectiveAddressUserMessage: "Как ты оценишь деятельность компании?",
|
||
followupContext: null,
|
||
llmPreDecomposeMeta: {
|
||
applied: true,
|
||
predecomposeContract: {
|
||
mode: "unsupported",
|
||
mode_confidence: "low",
|
||
intent: "unknown",
|
||
intent_confidence: "low"
|
||
},
|
||
semanticExtractionContract: {
|
||
valid: true,
|
||
apply_canonical_recommended: true,
|
||
extraction: {
|
||
query_shape: "UNKNOWN",
|
||
aggregation_profile: "unknown"
|
||
},
|
||
guard_hints: {
|
||
deep_investigation_signal_detected: false
|
||
}
|
||
}
|
||
},
|
||
sessionItems: [
|
||
{
|
||
role: "assistant",
|
||
debug: {
|
||
execution_lane: "address_query",
|
||
answer_grounding_check: { status: "grounded" },
|
||
detected_intent: "counterparty_activity_lifecycle",
|
||
extracted_filters: {
|
||
organization: 'ООО "Альтернатива Плюс"',
|
||
period_to: "2026-04-18"
|
||
}
|
||
}
|
||
}
|
||
],
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(false);
|
||
expect(decision.toolGateDecision).toBe("skip_address_lane");
|
||
expect(decision.toolGateReason).toBe("unsupported_current_turn_meaning_boundary");
|
||
expect(decision.livingMode).toBe("chat");
|
||
expect(decision.livingReason).toBe("unsupported_current_turn_meaning_boundary");
|
||
expect(decision.orchestrationContract?.unsupported_current_turn_family).toBe("broad_business_evaluation");
|
||
});
|
||
|
||
it("lets broad business-audit meaning override noisy capability wording", () => {
|
||
const policy = buildPolicy({
|
||
resolveMetaSignalSet: () => ({
|
||
dataScopeMetaQuery: false,
|
||
capabilityMetaQuery: true,
|
||
metaAnswerFollowupSignal: false,
|
||
answerInspectionFollowupSignal: false
|
||
}),
|
||
resolveAssistantTurnMeaning: () => ({
|
||
schema_version: "assistant_turn_meaning_v1",
|
||
asked_domain_family: "business_summary",
|
||
asked_action_family: "broad_evaluation",
|
||
explicit_intent_candidate: null,
|
||
unsupported_but_understood_family: "broad_business_evaluation",
|
||
stale_replay_forbidden: true,
|
||
reason_codes: ["broad_business_evaluation_current_turn_signal"]
|
||
})
|
||
});
|
||
|
||
const decision = policy.resolveAssistantOrchestrationDecision({
|
||
rawUserMessage:
|
||
"Собери это как нормальный бизнес-аудит: что уже можно сказать уверенно, что proxy и что директору проверить руками.",
|
||
effectiveAddressUserMessage:
|
||
"Собери это как нормальный бизнес-аудит: что уже можно сказать уверенно, что proxy и что директору проверить руками.",
|
||
followupContext: null,
|
||
llmPreDecomposeMeta: null,
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(false);
|
||
expect(decision.toolGateReason).toBe("unsupported_current_turn_meaning_boundary");
|
||
expect(decision.livingReason).toBe("unsupported_current_turn_meaning_boundary");
|
||
expect(decision.orchestrationContract?.unsupported_current_turn_family).toBe("broad_business_evaluation");
|
||
expect(decision.orchestrationContract?.hard_meta_mode).toBeNull();
|
||
expect((decision.orchestrationContract as Record<string, unknown>)?.reason_codes).toContain(
|
||
"business_overview_meaning_overrides_capability_meta_noise"
|
||
);
|
||
});
|
||
|
||
it("recovers an address route from current-turn meaning when L0 resolver is noisy", () => {
|
||
const policy = buildPolicy({
|
||
resolveAddressToolGateDecision: undefined,
|
||
resolveAssistantTurnMeaning: () => ({
|
||
schema_version: "assistant_turn_meaning_v1",
|
||
explicit_intent_candidate: "receivables_confirmed_as_of_date",
|
||
meaning_confidence: "high",
|
||
reason_codes: ["receivables_current_turn_meaning_signal"],
|
||
stale_replay_forbidden: false
|
||
})
|
||
});
|
||
|
||
const decision = policy.resolveAssistantOrchestrationDecision({
|
||
rawUserMessage:
|
||
"\u043a\u0442\u043e \u043d\u0430\u043c\u0441 \u0434\u043e\u043b\u0436\u0435\u043d \u0434\u0435\u043d\u0435\u0433 \u043d\u0430 \u0441\u0435\u0433\u043e\u0434\u043d\u044f",
|
||
effectiveAddressUserMessage:
|
||
"\u043a\u0442\u043e \u043d\u0430\u043c\u0441 \u0434\u043e\u043b\u0436\u0435\u043d \u0434\u0435\u043d\u0435\u0433 \u043d\u0430 \u0441\u0435\u0433\u043e\u0434\u043d\u044f",
|
||
followupContext: null,
|
||
llmPreDecomposeMeta: null,
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(true);
|
||
expect(decision.livingMode).toBe("address_data");
|
||
expect(decision.orchestrationContract?.address_intent).toBe("receivables_confirmed_as_of_date");
|
||
expect(decision.orchestrationContract?.assistant_turn_meaning?.schema_version).toBe(
|
||
"assistant_turn_meaning_v1"
|
||
);
|
||
});
|
||
|
||
it("routes unsupported-but-understood current meaning to boundary instead of stale address carryover", () => {
|
||
const policy = buildPolicy({
|
||
hasDataRetrievalRequestSignal: () => true,
|
||
hasStrongDataIntentSignal: () => true,
|
||
resolveAddressToolGateDecision: () => ({
|
||
runAddressLane: true,
|
||
decision: "run_address_lane",
|
||
reason: "followup_context_detected"
|
||
}),
|
||
resolveAssistantTurnMeaning: () => ({
|
||
schema_version: "assistant_turn_meaning_v1",
|
||
asked_domain_family: "counterparty",
|
||
asked_action_family: "counterparty_value_or_turnover",
|
||
explicit_intent_candidate: null,
|
||
unsupported_but_understood_family: "counterparty_value_or_turnover",
|
||
stale_replay_forbidden: true,
|
||
reason_codes: ["counterparty_turnover_current_turn_signal"]
|
||
})
|
||
});
|
||
|
||
const decision = policy.resolveAssistantOrchestrationDecision({
|
||
rawUserMessage:
|
||
"\u043a\u0430\u043a\u043e\u0439 \u043e\u0431\u043e\u0440\u043e\u0442 \u0431\u044b\u043b \u0441\u0432\u043a",
|
||
effectiveAddressUserMessage:
|
||
"\u043a\u0430\u043a\u043e\u0439 \u043e\u0431\u043e\u0440\u043e\u0442 \u0431\u044b\u043b \u0441\u0432\u043a",
|
||
followupContext: {
|
||
previous_intent: "list_documents_by_counterparty",
|
||
previous_filters: {
|
||
counterparty: "Previous Counterparty"
|
||
}
|
||
},
|
||
llmPreDecomposeMeta: null,
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(false);
|
||
expect(decision.toolGateReason).toBe("unsupported_current_turn_meaning_boundary");
|
||
expect(decision.livingMode).toBe("chat");
|
||
expect(decision.orchestrationContract?.unsupported_current_turn_meaning_boundary).toBe(true);
|
||
expect(decision.orchestrationContract?.unsupported_current_turn_family).toBe(
|
||
"counterparty_value_or_turnover"
|
||
);
|
||
});
|
||
});
|