974 lines
43 KiB
TypeScript
974 lines
43 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
||
import { resolveAssistantOrchestrationDecision, resolveLivingAssistantModeDecision } from "../src/services/assistantService";
|
||
import {
|
||
buildAddressLlmPredecomposeContractV1,
|
||
buildAddressSemanticExtractionContractV1
|
||
} from "../src/services/address_runtime/predecomposeContract";
|
||
|
||
describe("assistant living router mode decision", () => {
|
||
it("returns address_data when address lane already triggered", () => {
|
||
const decision = resolveLivingAssistantModeDecision({
|
||
userMessage: "давай",
|
||
addressLaneTriggered: true,
|
||
useMock: false,
|
||
predecomposeMode: "address_query",
|
||
predecomposeModeConfidence: "high"
|
||
});
|
||
expect(decision.mode).toBe("address_data");
|
||
expect(decision.reason).toBe("address_lane_triggered");
|
||
});
|
||
|
||
it("keeps deep pipeline in mock mode to avoid test-env network calls", () => {
|
||
const decision = resolveLivingAssistantModeDecision({
|
||
userMessage: "привет",
|
||
addressLaneTriggered: false,
|
||
useMock: true,
|
||
predecomposeMode: "unsupported",
|
||
predecomposeModeConfidence: "low"
|
||
});
|
||
expect(decision.mode).toBe("deep_analysis");
|
||
expect(decision.reason).toBe("mock_mode_keeps_deep_pipeline");
|
||
});
|
||
|
||
it("routes casual non-data phrase to chat mode", () => {
|
||
const decision = resolveLivingAssistantModeDecision({
|
||
userMessage: "привет, как дела?",
|
||
addressLaneTriggered: false,
|
||
useMock: false,
|
||
predecomposeMode: "unsupported",
|
||
predecomposeModeConfidence: "low"
|
||
});
|
||
expect(decision.mode).toBe("chat");
|
||
});
|
||
|
||
it("keeps deep mode for strong data signal", () => {
|
||
const decision = resolveLivingAssistantModeDecision({
|
||
userMessage: "покажи документы по свк за 2020",
|
||
addressLaneTriggered: false,
|
||
useMock: false,
|
||
predecomposeMode: "unsupported",
|
||
predecomposeModeConfidence: "low"
|
||
});
|
||
expect(decision.mode).toBe("deep_analysis");
|
||
expect(decision.reason).toBe("strong_data_signal_detected");
|
||
});
|
||
it("keeps deep mode for accumulated advances query even when predecompose mode is unsupported", () => {
|
||
const decision = resolveLivingAssistantModeDecision({
|
||
userMessage: "Где у нас накопились авансы к отгрузкам, которые уже давно пора закрыть?",
|
||
addressLaneTriggered: false,
|
||
useMock: false,
|
||
predecomposeMode: "unsupported",
|
||
predecomposeModeConfidence: "low"
|
||
});
|
||
expect(decision.mode).toBe("deep_analysis");
|
||
expect(decision.reason).toBe("strong_data_signal_detected");
|
||
});
|
||
it("routes short unsupported predecompose prompts to deep fallback instead of chat", () => {
|
||
const decision = resolveLivingAssistantModeDecision({
|
||
userMessage: "без воды?",
|
||
addressLaneTriggered: false,
|
||
useMock: false,
|
||
predecomposeMode: "unsupported",
|
||
predecomposeModeConfidence: "low"
|
||
});
|
||
expect(decision.mode).toBe("deep_analysis");
|
||
expect(decision.reason).toBe("predecompose_unsupported_mode_fallback_to_deep");
|
||
});
|
||
|
||
it("routes ultra-short deictic follow-up ('тут?') to chat mode", () => {
|
||
const decision = resolveLivingAssistantModeDecision({
|
||
userMessage: "тут?",
|
||
addressLaneTriggered: false,
|
||
useMock: false,
|
||
predecomposeMode: "unsupported",
|
||
predecomposeModeConfidence: "low"
|
||
});
|
||
expect(decision.mode).toBe("chat");
|
||
expect(decision.reason).toBe("living_chat_signal_detected");
|
||
});
|
||
it("routes capability question to chat even when phrase contains 1С", () => {
|
||
const decision = resolveLivingAssistantModeDecision({
|
||
userMessage: "и 1с можешь настроить?",
|
||
addressLaneTriggered: false,
|
||
useMock: false,
|
||
predecomposeMode: "unsupported",
|
||
predecomposeModeConfidence: "low"
|
||
});
|
||
expect(decision.mode).toBe("chat");
|
||
expect(decision.reason).toBe("assistant_capability_query_detected");
|
||
});
|
||
it("routes capability question 'ok - what can you do in 1c' to chat", () => {
|
||
const decision = resolveLivingAssistantModeDecision({
|
||
userMessage: "\u043e\u043a - \u0447\u0442\u043e \u043c\u043e\u0436\u0435\u0448\u044c \u043f\u043e 1\u0441",
|
||
addressLaneTriggered: false,
|
||
useMock: false,
|
||
predecomposeMode: "unsupported",
|
||
predecomposeModeConfidence: "low"
|
||
});
|
||
expect(decision.mode).toBe("chat");
|
||
expect(decision.reason).toBe("assistant_capability_query_detected");
|
||
});
|
||
it("routes feature-capability wording to chat", () => {
|
||
const decision = resolveLivingAssistantModeDecision({
|
||
userMessage: "а какие фичи по работе с 1с у тебя отработаны максимально?",
|
||
addressLaneTriggered: false,
|
||
useMock: false,
|
||
predecomposeMode: "unsupported",
|
||
predecomposeModeConfidence: "low"
|
||
});
|
||
expect(decision.mode).toBe("chat");
|
||
expect(decision.reason).toBe("assistant_capability_query_detected");
|
||
});
|
||
it("routes data-scope question to chat instead of address lane", () => {
|
||
const decision = resolveLivingAssistantModeDecision({
|
||
userMessage: "по какой компании мы можем работать?",
|
||
addressLaneTriggered: false,
|
||
useMock: false,
|
||
predecomposeMode: "unsupported",
|
||
predecomposeModeConfidence: "low"
|
||
});
|
||
expect(decision.mode).toBe("chat");
|
||
expect(decision.reason).toBe("assistant_data_scope_query_detected");
|
||
});
|
||
it("routes 'whose base is this' style question to chat", () => {
|
||
const decision = resolveLivingAssistantModeDecision({
|
||
userMessage: "ну база в тебе чья? как называется контора?",
|
||
addressLaneTriggered: false,
|
||
useMock: false,
|
||
predecomposeMode: "unsupported",
|
||
predecomposeModeConfidence: "low"
|
||
});
|
||
expect(decision.mode).toBe("chat");
|
||
expect(decision.reason).toBe("assistant_data_scope_query_detected");
|
||
});
|
||
it("routes 'какая база подрублена?' to data-scope chat mode", () => {
|
||
const decision = resolveLivingAssistantModeDecision({
|
||
userMessage: "какая база подрублена?",
|
||
addressLaneTriggered: false,
|
||
useMock: false,
|
||
predecomposeMode: "unsupported",
|
||
predecomposeModeConfidence: "low"
|
||
});
|
||
expect(decision.mode).toBe("chat");
|
||
expect(decision.reason).toBe("assistant_data_scope_query_detected");
|
||
});
|
||
it("routes typo data-scope wording with misspelled company token to chat", () => {
|
||
const decision = resolveLivingAssistantModeDecision({
|
||
userMessage: "подскажи плиз с какой компинией можем поработать?",
|
||
addressLaneTriggered: false,
|
||
useMock: false,
|
||
predecomposeMode: "unsupported",
|
||
predecomposeModeConfidence: "low"
|
||
});
|
||
expect(decision.mode).toBe("chat");
|
||
expect(decision.reason).toBe("assistant_data_scope_query_detected");
|
||
});
|
||
|
||
it("routes slang data-scope wording 'по каким конторам можем общаться' to chat", () => {
|
||
const decision = resolveLivingAssistantModeDecision({
|
||
userMessage: "\u043f\u043e \u043a\u0430\u043a\u0438\u043c \u043a\u043e\u043d\u0442\u043e\u0440\u0430\u043c \u043c\u043e\u0436\u0435\u043c \u043e\u0431\u0449\u0430\u0442\u044c\u0441\u044f?",
|
||
addressLaneTriggered: false,
|
||
useMock: false,
|
||
predecomposeMode: "unsupported",
|
||
predecomposeModeConfidence: "low"
|
||
});
|
||
expect(decision.mode).toBe("chat");
|
||
expect(decision.reason).toBe("assistant_data_scope_query_detected");
|
||
});
|
||
|
||
it("routes data-scope wording without question mark when interrogative token is present", () => {
|
||
const decision = resolveLivingAssistantModeDecision({
|
||
userMessage: "каза какой компании подключена к 1с",
|
||
addressLaneTriggered: false,
|
||
useMock: false,
|
||
predecomposeMode: "unsupported",
|
||
predecomposeModeConfidence: "low"
|
||
});
|
||
expect(decision.mode).toBe("chat");
|
||
expect(decision.reason).toBe("assistant_data_scope_query_detected");
|
||
});
|
||
|
||
it("does not treat contract ranking data query as data-scope meta question", () => {
|
||
const decision = resolveLivingAssistantModeDecision({
|
||
userMessage: "по альтернативе вопрос. какой самый жирный контракт за всю историю у нее?",
|
||
addressLaneTriggered: false,
|
||
useMock: false,
|
||
predecomposeMode: "unsupported",
|
||
predecomposeModeConfidence: "low"
|
||
});
|
||
expect(decision.mode).toBe("deep_analysis");
|
||
expect(decision.reason).toBe("strong_data_signal_detected");
|
||
});
|
||
});
|
||
|
||
describe("assistant orchestration contract", () => {
|
||
it("routes non-domain emotional follow-up to indexed chat path instead of address lane", () => {
|
||
const decision = resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: "бля хочу выпилиться от этого ебаного 1с",
|
||
effectiveAddressUserMessage: "бля хочу выпилиться от этого ебаного 1с",
|
||
followupContext: {
|
||
previous_intent: "payables_confirmed_as_of_date",
|
||
previous_filters: {
|
||
period_from: "2021-05-01",
|
||
period_to: "2021-05-31",
|
||
as_of_date: "2021-05-31"
|
||
}
|
||
},
|
||
llmPreDecomposeMeta: {
|
||
applied: false,
|
||
reason: "no_usable_fragment",
|
||
predecomposeContract: {
|
||
mode: "unsupported",
|
||
mode_confidence: "low",
|
||
intent: "unknown",
|
||
intent_confidence: "low"
|
||
}
|
||
} as any,
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(false);
|
||
expect(decision.toolGateDecision).toBe("skip_address_lane");
|
||
expect(decision.toolGateReason).toBe("non_domain_query_indexed");
|
||
expect(decision.livingMode).toBe("chat");
|
||
expect(decision.livingReason).toBe("non_domain_query_indexed");
|
||
expect(decision.orchestrationContract?.hard_meta_mode).toBe("non_domain");
|
||
});
|
||
|
||
it("keeps VAT payable forecast query in address lane", () => {
|
||
const decision = resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: "какой прогноз оплаты ндс за 12 мая 2020",
|
||
effectiveAddressUserMessage: "какой прогноз оплаты ндс за 12 мая 2020",
|
||
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");
|
||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||
expect(decision.orchestrationContract?.unsupported_address_intent_fallback_to_deep).toBe(false);
|
||
});
|
||
|
||
it("keeps supported contract analytics query in address lane", () => {
|
||
const decision = resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: "по альтернативе вопрос. какой самый жирный контракт за всю историю у нее?",
|
||
effectiveAddressUserMessage: "по альтернативе вопрос. какой самый жирный контракт за всю историю у нее?",
|
||
followupContext: null,
|
||
llmPreDecomposeMeta: null,
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(true);
|
||
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||
expect(["address_mode_classifier_detected", "address_intent_resolver_detected"]).toContain(String(decision.toolGateReason));
|
||
expect(decision.livingMode).toBe("address_data");
|
||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||
});
|
||
|
||
it("keeps inventory provenance and sale-trace queries in address lane", () => {
|
||
const decision = resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: "От какого поставщика куплен товар Диван трехместный из текущего остатка на складе Основной склад",
|
||
effectiveAddressUserMessage: "От какого поставщика куплен товар Диван трехместный из текущего остатка на складе Основной склад",
|
||
followupContext: {
|
||
previous_intent: "inventory_purchase_provenance_for_item",
|
||
previous_filters: {
|
||
as_of_date: "2020-03-31"
|
||
}
|
||
},
|
||
llmPreDecomposeMeta: {
|
||
applied: true,
|
||
llmCanonicalCandidateDetected: true,
|
||
predecomposeContract: {
|
||
mode: "address_query",
|
||
mode_confidence: "high",
|
||
intent: "inventory_purchase_provenance_for_item",
|
||
intent_confidence: "high"
|
||
}
|
||
} as any,
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(true);
|
||
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||
expect(["address_mode_classifier_detected", "address_intent_resolver_detected"]).toContain(String(decision.toolGateReason));
|
||
expect(decision.livingMode).toBe("address_data");
|
||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||
});
|
||
|
||
it("keeps short inventory follow-up 'когда' in address lane when a selected-item provenance context exists", () => {
|
||
const decision = resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: "когда",
|
||
effectiveAddressUserMessage: "когда",
|
||
followupContext: {
|
||
previous_intent: "inventory_purchase_provenance_for_item",
|
||
previous_filters: {
|
||
item: "Столешница 600*3050*26 дуб ниагара",
|
||
warehouse: "Основной склад",
|
||
as_of_date: "2019-03-31"
|
||
}
|
||
},
|
||
llmPreDecomposeMeta: {
|
||
applied: false,
|
||
reason: "normalized_fragment_rejected_semantic_guard",
|
||
llmCanonicalCandidateDetected: true,
|
||
predecomposeContract: {
|
||
mode: "unsupported",
|
||
mode_confidence: "low",
|
||
intent: "unknown",
|
||
intent_confidence: "low"
|
||
},
|
||
semanticExtractionContract: {
|
||
valid: false,
|
||
apply_canonical_recommended: false,
|
||
reason_codes: ["unsupported_low_confidence_contract"]
|
||
}
|
||
} as any,
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(true);
|
||
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||
expect(decision.livingMode).toBe("address_data");
|
||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||
});
|
||
|
||
it("keeps customer-value ranking question in address lane even when LLM semantic guard rejects canonical rewrite", () => {
|
||
const decision = resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: "кто больше всего принес денег в 2020",
|
||
effectiveAddressUserMessage: "кто больше всего принес денег в 2020",
|
||
followupContext: null,
|
||
llmPreDecomposeMeta: {
|
||
applied: false,
|
||
reason: "normalized_fragment_rejected_semantic_guard",
|
||
llmCanonicalCandidateDetected: true,
|
||
predecomposeContract: {
|
||
mode: "unsupported",
|
||
mode_confidence: "low",
|
||
intent: "unknown",
|
||
intent_confidence: "low"
|
||
},
|
||
semanticExtractionContract: {
|
||
valid: false,
|
||
apply_canonical_recommended: false,
|
||
reason_codes: ["unsupported_low_confidence_contract"]
|
||
}
|
||
} as any,
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(true);
|
||
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||
expect(["address_intent_resolver_detected", "address_mode_classifier_detected", "address_signal_detected"]).toContain(
|
||
String(decision.toolGateReason)
|
||
);
|
||
expect(decision.livingMode).toBe("address_data");
|
||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||
});
|
||
|
||
it("keeps colloquial 'кто нам больше денег принес' in address lane", () => {
|
||
const decision = resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: "\u043a\u0442\u043e \u043d\u0430\u043c \u0431\u043e\u043b\u044c\u0448\u0435 \u0434\u0435\u043d\u0435\u0433 \u043f\u0440\u0438\u043d\u0435\u0441",
|
||
effectiveAddressUserMessage: "\u043a\u0442\u043e \u043d\u0430\u043c \u0431\u043e\u043b\u044c\u0448\u0435 \u0434\u0435\u043d\u0435\u0433 \u043f\u0440\u0438\u043d\u0435\u0441",
|
||
followupContext: null,
|
||
llmPreDecomposeMeta: null,
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(true);
|
||
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||
expect(["address_intent_resolver_detected", "address_mode_classifier_detected", "address_signal_detected"]).toContain(
|
||
String(decision.toolGateReason)
|
||
);
|
||
expect(decision.livingMode).toBe("address_data");
|
||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||
});
|
||
|
||
it("keeps short mirror follow-up 'a мы кому' in address lane instead of non-domain chat", () => {
|
||
const decision = resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: "a мы кому",
|
||
effectiveAddressUserMessage: "a мы кому",
|
||
followupContext: null,
|
||
llmPreDecomposeMeta: {
|
||
applied: false,
|
||
reason: "normalized_fragment_rejected_semantic_guard",
|
||
llmCanonicalCandidateDetected: true,
|
||
predecomposeContract: {
|
||
mode: "unsupported",
|
||
mode_confidence: "low",
|
||
intent: "unknown",
|
||
intent_confidence: "low"
|
||
},
|
||
semanticExtractionContract: {
|
||
valid: false,
|
||
apply_canonical_recommended: false,
|
||
reason_codes: ["unsupported_low_confidence_contract"]
|
||
}
|
||
} as any,
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(true);
|
||
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||
expect(decision.livingMode).toBe("address_data");
|
||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||
});
|
||
|
||
it("routes unsupported turnover-by-organization query to deep analysis", () => {
|
||
const decision = resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: "\u043a\u0430\u043a\u0438\u0435 \u043e\u0431\u043e\u0440\u043e\u0442\u044b \u043f\u043e \u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0435 \u0437\u0430 2020 \u0433\u043e\u0434",
|
||
effectiveAddressUserMessage: "\u041e\u0431\u043e\u0440\u043e\u0442\u044b \u043f\u043e \u0441\u0447\u0435\u0442\u0443 '\u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430' \u0437\u0430 2020 \u0433\u043e\u0434.",
|
||
followupContext: null,
|
||
llmPreDecomposeMeta: {
|
||
applied: true,
|
||
llmCanonicalCandidateDetected: true,
|
||
predecomposeContract: {
|
||
mode: "unsupported",
|
||
mode_confidence: "low",
|
||
intent: "account_balance_snapshot",
|
||
intent_confidence: "high"
|
||
}
|
||
} as any,
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(false);
|
||
expect(decision.toolGateDecision).toBe("skip_address_lane");
|
||
expect(decision.livingMode).toBe("deep_analysis");
|
||
expect([
|
||
"address_signal_unsupported_intent_fallback_to_deep",
|
||
"aggregate_analytics_signal_fallback_to_deep"
|
||
]).toContain(String(decision.toolGateReason));
|
||
expect([
|
||
"unsupported_address_intent_fallback_to_deep",
|
||
"aggregate_analytics_signal_fallback_to_deep"
|
||
]).toContain(String(decision.livingReason));
|
||
});
|
||
|
||
it("does not route advances-to-shipment risk query to chat when semantic guard rejects canonical rewrite", () => {
|
||
const decision = resolveAssistantOrchestrationDecision({
|
||
rawUserMessage:
|
||
"Где у нас накопились авансы к отгрузкам, которые уже давно пора закрыть или хотя бы перепроверить?",
|
||
effectiveAddressUserMessage:
|
||
"Где у нас накопились авансы к отгрузкам, которые уже давно пора закрыть или хотя бы перепроверить?",
|
||
followupContext: null,
|
||
llmPreDecomposeMeta: {
|
||
applied: false,
|
||
llmCanonicalCandidateDetected: false,
|
||
reason: "normalized_fragment_rejected_semantic_guard",
|
||
predecomposeContract: {
|
||
mode: "unsupported",
|
||
mode_confidence: "low",
|
||
intent: "unknown",
|
||
intent_confidence: "low"
|
||
},
|
||
semanticExtractionContract: {
|
||
valid: false,
|
||
apply_canonical_recommended: false,
|
||
reason_codes: ["unsupported_low_confidence_contract"]
|
||
}
|
||
} as any,
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.livingMode).toBe("address_data");
|
||
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||
expect(["address_signal_detected", "address_intent_resolver_detected", "address_mode_classifier_detected"]).toContain(
|
||
String(decision.toolGateReason)
|
||
);
|
||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||
});
|
||
|
||
it("routes unsupported turnover query to deep even with followup context carryover", () => {
|
||
const decision = resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: "\u043a\u0430\u043a\u0438\u0435 \u043e\u0431\u043e\u0440\u043e\u0442\u044b \u043f\u043e \u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0435 \u0437\u0430 2020 \u0433\u043e\u0434",
|
||
effectiveAddressUserMessage: "\u041e\u0431\u043e\u0440\u043e\u0442\u044b \u043f\u043e \u0441\u0447\u0435\u0442\u0443 '\u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430' \u0437\u0430 2020 \u0433\u043e\u0434.",
|
||
followupContext: {
|
||
previous_intent: "list_documents_by_contract",
|
||
previous_filters: {
|
||
organization: "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"
|
||
}
|
||
},
|
||
llmPreDecomposeMeta: {
|
||
applied: true,
|
||
llmCanonicalCandidateDetected: true,
|
||
predecomposeContract: {
|
||
mode: "unsupported",
|
||
mode_confidence: "low",
|
||
intent: "account_balance_snapshot",
|
||
intent_confidence: "high"
|
||
},
|
||
semanticExtractionContract: {
|
||
extraction: {
|
||
query_shape: "AGGREGATE_LOOKUP",
|
||
aggregation_profile: "management_profile"
|
||
},
|
||
guard_hints: {
|
||
deep_investigation_signal_detected: false
|
||
}
|
||
}
|
||
} as any,
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(false);
|
||
expect(decision.toolGateDecision).toBe("skip_address_lane");
|
||
expect(decision.livingMode).toBe("deep_analysis");
|
||
expect([
|
||
"address_signal_unsupported_intent_fallback_to_deep",
|
||
"aggregate_analytics_signal_fallback_to_deep"
|
||
]).toContain(String(decision.toolGateReason));
|
||
expect(
|
||
decision.orchestrationContract?.semantic_route_arbitration?.followup_semantic_override_to_deep_allowed
|
||
).toBe(true);
|
||
});
|
||
|
||
it("routes standalone aggregate query to deep even when stale followup context exists and LLM predecompose is unavailable", () => {
|
||
const decision = resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: "какие обороты по альтернативе за 2020 год",
|
||
effectiveAddressUserMessage: "какие обороты по альтернативе за 2020 год",
|
||
followupContext: {
|
||
previous_intent: "list_documents_by_contract",
|
||
previous_filters: {
|
||
organization: "ООО Альтернатива Плюс"
|
||
}
|
||
},
|
||
llmPreDecomposeMeta: null as any,
|
||
useMock: true
|
||
} as any);
|
||
|
||
expect(decision.runAddressLane).toBe(false);
|
||
expect(decision.toolGateDecision).toBe("skip_address_lane");
|
||
expect(decision.toolGateReason).toBe("aggregate_analytics_signal_fallback_to_deep");
|
||
expect(decision.livingMode).toBe("deep_analysis");
|
||
expect(decision.livingReason).toBe("aggregate_analytics_signal_fallback_to_deep");
|
||
expect(decision.orchestrationContract?.aggregate_analytics_signal_fallback_to_deep).toBe(true);
|
||
});
|
||
|
||
it("keeps profitability ranking query in address lane", () => {
|
||
const decision = resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: "\u043a\u0430\u043a\u043e\u0439 \u0441\u0430\u043c\u044b\u0439 \u0434\u043e\u0445\u043e\u0434\u043d\u044b\u0439 \u0433\u043e\u0434?",
|
||
effectiveAddressUserMessage: "\u043a\u0430\u043a\u043e\u0439 \u0441\u0430\u043c\u044b\u0439 \u0434\u043e\u0445\u043e\u0434\u043d\u044b\u0439 \u0433\u043e\u0434?",
|
||
followupContext: null,
|
||
llmPreDecomposeMeta: null as any,
|
||
useMock: false
|
||
} as any);
|
||
|
||
expect(decision.runAddressLane).toBe(true);
|
||
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||
expect(["address_intent_resolver_detected", "address_mode_classifier_detected", "address_signal_detected"]).toContain(
|
||
String(decision.toolGateReason)
|
||
);
|
||
expect(decision.livingMode).toBe("address_data");
|
||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||
expect(decision.orchestrationContract?.aggregate_analytics_signal_fallback_to_deep).toBe(false);
|
||
});
|
||
|
||
it("keeps unsupported retrieval query in address lane when LLM runtime is unavailable", () => {
|
||
const decision = resolveAssistantOrchestrationDecision({
|
||
rawUserMessage:
|
||
"\u0413\u0434\u0435 \u0443 \u043d\u0430\u0441 \u043d\u0430\u043a\u043e\u043f\u0438\u043b\u0438\u0441\u044c \u0430\u0432\u0430\u043d\u0441\u044b \u043a \u043e\u0442\u0433\u0440\u0443\u0437\u043a\u0430\u043c, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0434\u0430\u0432\u043d\u043e \u043f\u043e\u0440\u0430 \u0437\u0430\u043a\u0440\u044b\u0442\u044c?",
|
||
effectiveAddressUserMessage:
|
||
"\u0413\u0434\u0435 \u0443 \u043d\u0430\u0441 \u043d\u0430\u043a\u043e\u043f\u0438\u043b\u0438\u0441\u044c \u0430\u0432\u0430\u043d\u0441\u044b \u043a \u043e\u0442\u0433\u0440\u0443\u0437\u043a\u0430\u043c, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0434\u0430\u0432\u043d\u043e \u043f\u043e\u0440\u0430 \u0437\u0430\u043a\u0440\u044b\u0442\u044c?",
|
||
followupContext: null,
|
||
llmPreDecomposeMeta: {
|
||
applied: false,
|
||
reason: "error:OpenAI API key is missing.",
|
||
llmCanonicalCandidateDetected: false,
|
||
predecomposeContract: {
|
||
mode: "unsupported",
|
||
mode_confidence: "low",
|
||
intent: "unknown",
|
||
intent_confidence: "low"
|
||
},
|
||
semanticExtractionContract: {
|
||
valid: false,
|
||
apply_canonical_recommended: false,
|
||
reason_codes: ["unsupported_low_confidence_contract", "deep_investigation_signal_detected"],
|
||
guard_hints: {
|
||
deep_investigation_signal_detected: true
|
||
},
|
||
extraction: {
|
||
query_shape: "VERIFY_FACTUAL",
|
||
aggregation_profile: "unknown"
|
||
}
|
||
}
|
||
} as any,
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(true);
|
||
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||
expect(decision.livingMode).toBe("address_data");
|
||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||
expect(decision.orchestrationContract?.unsupported_address_intent_fallback_to_deep).toBe(false);
|
||
expect(decision.orchestrationContract?.deep_analysis_signal_fallback_to_deep).toBe(false);
|
||
});
|
||
|
||
it("keeps VAT explain follow-up in address lane when followup context is present", () => {
|
||
const decision = resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: "почему прогноз к уплате 0?",
|
||
effectiveAddressUserMessage: "почему прогноз к уплате 0?",
|
||
followupContext: {
|
||
previous_intent: "vat_payable_forecast",
|
||
previous_filters: {
|
||
period_from: "2020-03-01",
|
||
period_to: "2020-03-31"
|
||
},
|
||
previous_anchor_type: "unknown",
|
||
previous_anchor_value: null
|
||
},
|
||
llmPreDecomposeMeta: null,
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(true);
|
||
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||
expect(decision.toolGateReason).toBe("followup_context_detected");
|
||
expect(decision.livingMode).toBe("address_data");
|
||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||
});
|
||
|
||
it("keeps documentary inventory chain verification in address lane for supported exact intent", () => {
|
||
const question =
|
||
"Есть ли документально подтвержденная цепочка: поставщик Гамма-мебель, ООО -> товар Шкаф картотечный 1000*400*2100 -> покупатель Департамент капитального ремонта города Москвы";
|
||
const decision = resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: question,
|
||
effectiveAddressUserMessage: question,
|
||
followupContext: null,
|
||
llmPreDecomposeMeta: {
|
||
applied: true,
|
||
llmCanonicalCandidateDetected: true,
|
||
predecomposeContract: {
|
||
mode: "deep_analysis",
|
||
mode_confidence: "high",
|
||
intent: "inventory_purchase_to_sale_chain",
|
||
intent_confidence: "medium"
|
||
},
|
||
semanticExtractionContract: {
|
||
valid: true,
|
||
apply_canonical_recommended: true,
|
||
reason_codes: ["deep_investigation_signal_detected"],
|
||
guard_hints: {
|
||
deep_investigation_signal_detected: true
|
||
},
|
||
extraction: {
|
||
query_shape: "VERIFY_FACTUAL",
|
||
aggregation_profile: "unknown"
|
||
}
|
||
}
|
||
} as any,
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(true);
|
||
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||
expect(decision.livingMode).toBe("address_data");
|
||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||
expect(decision.orchestrationContract?.deep_analysis_signal_fallback_to_deep).toBe(false);
|
||
expect(decision.orchestrationContract?.semantic_route_arbitration?.supported_address_intent_detected).toBe(true);
|
||
expect(decision.orchestrationContract?.semantic_route_arbitration?.strict_deep_investigation_bypass_allowed).toBe(true);
|
||
});
|
||
|
||
it("keeps 'a na tekushuyu datu' follow-up in address lane when previous VAT context exists", () => {
|
||
const decision = resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: "а на текущую дату",
|
||
effectiveAddressUserMessage: "а на текущую дату",
|
||
followupContext: {
|
||
previous_intent: "vat_payable_confirmed_as_of_date",
|
||
previous_filters: {
|
||
period_from: "2016-03-01",
|
||
period_to: "2016-03-31",
|
||
as_of_date: "2016-03-31"
|
||
},
|
||
previous_anchor_type: "unknown",
|
||
previous_anchor_value: null
|
||
},
|
||
llmPreDecomposeMeta: {
|
||
applied: false,
|
||
reason: "normalized_fragment_rejected_semantic_guard",
|
||
llmCanonicalCandidateDetected: true,
|
||
predecomposeContract: {
|
||
mode: "unsupported",
|
||
mode_confidence: "low",
|
||
intent: "unknown",
|
||
intent_confidence: "low"
|
||
}
|
||
} as any,
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(true);
|
||
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||
expect(decision.livingMode).toBe("address_data");
|
||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||
});
|
||
|
||
it("keeps explicit address-mode unknown-intent data query in address lane", () => {
|
||
const decision = resolveAssistantOrchestrationDecision({
|
||
rawUserMessage:
|
||
"Покажи контрагентов, по которым сальдо скорее всего не совпадет с их актом сверки. Может, стоит поторопиться и запросить сверку?",
|
||
effectiveAddressUserMessage:
|
||
"Показать контрагентов с вероятным несогласием между сальдо и актом сверки. Рекомендовать запросить сверку.",
|
||
followupContext: null,
|
||
llmPreDecomposeMeta: {
|
||
applied: true,
|
||
llmCanonicalCandidateDetected: true,
|
||
predecomposeContract: {
|
||
mode: "address_query",
|
||
mode_confidence: "high",
|
||
intent: "unknown",
|
||
intent_confidence: "low"
|
||
}
|
||
} as any,
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(true);
|
||
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||
expect(decision.livingMode).toBe("address_data");
|
||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||
expect(decision.orchestrationContract?.unsupported_address_intent_fallback_to_deep).toBe(false);
|
||
});
|
||
|
||
it("keeps confirmed open-contracts query in address lane despite 'unclosed' wording", () => {
|
||
const decision = resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: "\u041f\u043e\u043a\u0430\u0436\u0438 \u043d\u0435\u0437\u0430\u043a\u0440\u044b\u0442\u044b\u0435 \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u044b \u043d\u0430 2020-12-31",
|
||
effectiveAddressUserMessage: "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043d\u0435\u0437\u0430\u043a\u0440\u044b\u0442\u044b\u0435 \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u044b \u043f\u043e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044e \u043d\u0430 \u043a\u043e\u043d\u0435\u0446 \u0434\u0435\u043a\u0430\u0431\u0440\u044f 2020 \u0433\u043e\u0434\u0430.",
|
||
followupContext: {
|
||
previous_intent: "month_close_costs_20_44"
|
||
},
|
||
llmPreDecomposeMeta: {
|
||
applied: true,
|
||
llmCanonicalCandidateDetected: true,
|
||
predecomposeContract: {
|
||
mode: "address_query",
|
||
mode_confidence: "high",
|
||
intent: "open_contracts_confirmed_as_of_date",
|
||
intent_confidence: "medium"
|
||
}
|
||
} as any,
|
||
useMock: false
|
||
} as any);
|
||
|
||
expect(decision.runAddressLane).toBe(true);
|
||
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||
expect(decision.livingMode).toBe("address_data");
|
||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||
expect(decision.toolGateReason).toBe("address_mode_classifier_detected");
|
||
});
|
||
|
||
it("keeps inventory-on-hand query in address lane", () => {
|
||
const decision = resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: "Какие товары сейчас лежат на складе",
|
||
effectiveAddressUserMessage: "Какие товары сейчас лежат на складе",
|
||
followupContext: null,
|
||
llmPreDecomposeMeta: null as any,
|
||
useMock: true
|
||
} as any);
|
||
|
||
expect(decision.runAddressLane).toBe(true);
|
||
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||
expect(["address_intent_resolver_detected", "address_mode_classifier_detected", "address_signal_detected"]).toContain(
|
||
String(decision.toolGateReason)
|
||
);
|
||
expect(decision.livingMode).toBe("address_data");
|
||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||
});
|
||
|
||
it("keeps slang stock-state query with organization scope in address lane instead of deep fallback", () => {
|
||
const rawUserMessage = "чекни плиз чо там на складе альтернативы происходит";
|
||
const effectiveAddressUserMessage = "проверь, что происходит на складе у компании 'альтернатива'";
|
||
const predecomposeContract = buildAddressLlmPredecomposeContractV1({
|
||
sourceMessage: rawUserMessage,
|
||
canonicalMessage: effectiveAddressUserMessage,
|
||
semanticHints: {
|
||
scope_target_kind: "organization",
|
||
scope_target_text: "альтернатива",
|
||
date_scope_kind: "implicit_current",
|
||
self_scope_detected: false,
|
||
selected_object_scope_detected: false
|
||
}
|
||
});
|
||
const semanticExtractionContract = buildAddressSemanticExtractionContractV1({
|
||
sourceMessage: rawUserMessage,
|
||
canonicalMessage: effectiveAddressUserMessage,
|
||
predecomposeContract
|
||
});
|
||
|
||
const decision = resolveAssistantOrchestrationDecision({
|
||
rawUserMessage,
|
||
effectiveAddressUserMessage,
|
||
followupContext: null,
|
||
llmPreDecomposeMeta: {
|
||
applied: true,
|
||
llmCanonicalCandidateDetected: true,
|
||
predecomposeContract,
|
||
semanticExtractionContract
|
||
} as any,
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(true);
|
||
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||
expect(decision.livingMode).toBe("address_data");
|
||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||
expect(decision.orchestrationContract?.unsupported_address_intent_fallback_to_deep).toBe(false);
|
||
expect(decision.orchestrationContract?.deep_analysis_signal_fallback_to_deep).toBe(false);
|
||
expect(decision.orchestrationContract?.semantic_route_arbitration?.supported_address_intent_detected).toBe(true);
|
||
});
|
||
|
||
it("keeps short colloquial stock query with organization scope in address lane instead of chat fallback", () => {
|
||
const rawUserMessage = "че на складах альтернативы";
|
||
const effectiveAddressUserMessage = "что находится на складах у компании 'альтернатива'";
|
||
const predecomposeContract = buildAddressLlmPredecomposeContractV1({
|
||
sourceMessage: rawUserMessage,
|
||
canonicalMessage: effectiveAddressUserMessage,
|
||
semanticHints: {
|
||
scope_target_kind: "organization",
|
||
scope_target_text: "альтернатива",
|
||
date_scope_kind: "implicit_current",
|
||
self_scope_detected: false,
|
||
selected_object_scope_detected: false
|
||
}
|
||
});
|
||
const semanticExtractionContract = buildAddressSemanticExtractionContractV1({
|
||
sourceMessage: rawUserMessage,
|
||
canonicalMessage: effectiveAddressUserMessage,
|
||
predecomposeContract
|
||
});
|
||
|
||
const decision = resolveAssistantOrchestrationDecision({
|
||
rawUserMessage,
|
||
effectiveAddressUserMessage,
|
||
followupContext: null,
|
||
llmPreDecomposeMeta: {
|
||
applied: true,
|
||
llmCanonicalCandidateDetected: true,
|
||
predecomposeContract,
|
||
semanticExtractionContract
|
||
} as any,
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(true);
|
||
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||
expect([
|
||
"address_intent_resolver_detected",
|
||
"address_mode_classifier_detected",
|
||
"llm_canonical_data_signal_detected",
|
||
"address_signal_detected"
|
||
]).toContain(
|
||
String(decision.toolGateReason)
|
||
);
|
||
expect(decision.livingMode).toBe("address_data");
|
||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||
});
|
||
|
||
it("keeps open-contracts request in address lane even with stale deep followup context when LLM contract is absent", () => {
|
||
const decision = resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: "Покажи незакрытые договоры на 2020-12-31",
|
||
effectiveAddressUserMessage: "Покажи незакрытые договоры на 2020-12-31",
|
||
followupContext: {
|
||
previous_intent: "month_close_costs_20_44"
|
||
},
|
||
llmPreDecomposeMeta: null as any,
|
||
useMock: true
|
||
} as any);
|
||
|
||
expect(decision.runAddressLane).toBe(true);
|
||
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||
expect(decision.livingMode).toBe("address_data");
|
||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||
});
|
||
|
||
it("routes 'по каким конторам можем общаться?' to chat/data-scope in orchestration contract", () => {
|
||
const decision = resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: "по каким конторам можем общаться?",
|
||
effectiveAddressUserMessage: "по каким контрагентам у нас есть активность или договоры?",
|
||
followupContext: null,
|
||
llmPreDecomposeMeta: {
|
||
applied: true,
|
||
llmCanonicalCandidateDetected: true,
|
||
predecomposeContract: {
|
||
mode: "address_query",
|
||
mode_confidence: "medium",
|
||
intent: "list_documents_by_contract",
|
||
intent_confidence: "medium"
|
||
}
|
||
} as any,
|
||
useMock: false
|
||
} as any);
|
||
|
||
expect(decision.runAddressLane).toBe(false);
|
||
expect(decision.toolGateDecision).toBe("skip_address_lane");
|
||
expect(decision.toolGateReason).toBe("assistant_data_scope_query_detected");
|
||
expect(decision.livingMode).toBe("chat");
|
||
expect(decision.livingReason).toBe("assistant_data_scope_query_detected");
|
||
});
|
||
|
||
it("does not force address lane for deep-analysis unknown intent query with date-like token", () => {
|
||
const decision = resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: "найди какие либо ошибки на 21 мая 2022 года",
|
||
effectiveAddressUserMessage: "Найти ошибки в бухгалтерии за 21 мая 2022 года.",
|
||
followupContext: null,
|
||
llmPreDecomposeMeta: {
|
||
applied: true,
|
||
llmCanonicalCandidateDetected: true,
|
||
predecomposeContract: {
|
||
mode: "deep_analysis",
|
||
mode_confidence: "high",
|
||
intent: "unknown",
|
||
intent_confidence: "low"
|
||
}
|
||
} as any,
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(false);
|
||
expect(decision.toolGateDecision).toBe("skip_address_lane");
|
||
expect(decision.livingMode).toBe("deep_analysis");
|
||
expect(["address_signal_unsupported_intent_fallback_to_deep", "no_address_signal_after_l0"]).toContain(
|
||
decision.toolGateReason
|
||
);
|
||
});
|
||
|
||
it("routes risk/anomaly analytics wording to deep pipeline", () => {
|
||
const decision = resolveAssistantOrchestrationDecision({
|
||
rawUserMessage: "Проверь НДС по счету 19 за 2020-06 и рискованные записи по документам.",
|
||
effectiveAddressUserMessage: "Проверь НДС по счету 19 за 2020-06 и рискованные записи по документам.",
|
||
followupContext: null,
|
||
llmPreDecomposeMeta: null,
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(false);
|
||
expect(decision.toolGateDecision).toBe("skip_address_lane");
|
||
expect(decision.toolGateReason).toBe("deep_analysis_signal_fallback_to_deep");
|
||
expect(decision.livingMode).toBe("deep_analysis");
|
||
expect(decision.livingReason).toBe("deep_analysis_signal_fallback_to_deep");
|
||
expect(decision.orchestrationContract?.deep_analysis_signal_fallback_to_deep).toBe(true);
|
||
});
|
||
|
||
it("routes settlement closure verification wording to deep pipeline", () => {
|
||
const decision = resolveAssistantOrchestrationDecision({
|
||
rawUserMessage:
|
||
"По оплате поставщику на счете 60 в июле 2020 остался хвост. Проверь закрытие по договору и объекту расчетов.",
|
||
effectiveAddressUserMessage:
|
||
"По оплате поставщику на счете 60 в июле 2020 остался хвост. Проверь закрытие по договору и объекту расчетов.",
|
||
followupContext: null,
|
||
llmPreDecomposeMeta: null,
|
||
useMock: false
|
||
});
|
||
|
||
expect(decision.runAddressLane).toBe(false);
|
||
expect(decision.toolGateDecision).toBe("skip_address_lane");
|
||
expect(decision.toolGateReason).toBe("deep_analysis_signal_fallback_to_deep");
|
||
expect(decision.livingMode).toBe("deep_analysis");
|
||
expect(decision.livingReason).toBe("deep_analysis_signal_fallback_to_deep");
|
||
expect(decision.orchestrationContract?.deep_analysis_signal_fallback_to_deep).toBe(true);
|
||
});
|
||
});
|