ДОМЕНЫ - ВОПРОСЫ - fix: переведен exact-маршрут кому должны на дату на Хозрасчетный.Остатки (без Движения.СубконтоДт1/Кт1)

This commit is contained in:
2026-04-12 16:59:18 +03:00
parent 278eb4abeb
commit 143cf6efe1
13 changed files with 646 additions and 127 deletions
@@ -1031,7 +1031,7 @@ describe("address compose stage utf8 headers", () => {
expect(reply.text).toContain("Профиль договорной базы собран");
expect(reply.text).toContain("Всего договоров в базе: 520.");
expect(reply.text).toContain("Использованных договоров (есть factual связь с операциями): 148.");
expect(reply.text).toContain("�?Использованных договоров (есть factual связь с операциями): 148.");
expect(reply.text).toContain("Неиспользуемых договоров: 372.");
});
@@ -1746,6 +1746,25 @@ describe("address intent resolver expansion (M2.3a)", () => {
expect(result.intent).toBe("customer_revenue_and_payments");
});
it("resolves colloquial 'кто нам больше денег принес' wording into customer revenue intent", () => {
const result = resolveAddressIntent("\u043a\u0442\u043e \u043d\u0430\u043c \u0431\u043e\u043b\u044c\u0448\u0435 \u0434\u0435\u043d\u0435\u0433 \u043f\u0440\u0438\u043d\u0435\u0441");
expect(result.intent).toBe("customer_revenue_and_payments");
});
it("resolves typo 'ликвидних заказчиков' wording into customer revenue intent", () => {
const result = resolveAddressIntent(
"\u043f\u043e\u043a\u0430\u0436\u0438 \u0441\u0430\u043c\u044b\u0445 \u043b\u0438\u043a\u0432\u0438\u0434\u043d\u0438\u0445 \u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a\u043e\u0432"
);
expect(result.intent).toBe("customer_revenue_and_payments");
});
it("resolves yearly profitability wording into customer revenue intent", () => {
const result = resolveAddressIntent(
"\u043a\u0430\u043a\u0438\u0435 \u0441\u0430\u043c\u044b\u0435 \u0434\u043e\u0445\u043e\u0434\u043d\u044b\u0435 \u0433\u043e\u0434\u0430 \u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u044b"
);
expect(result.intent).toBe("customer_revenue_and_payments");
});
it("resolves major-share revenue wording into customer revenue intent", () => {
const result = resolveAddressIntent("какие контрагенты принесли основную часть нашей выручки за отчетный период?");
expect(result.intent).toBe("customer_revenue_and_payments");
@@ -2318,7 +2337,7 @@ describe("address filter extraction for balance drilldown", () => {
it("repairs mojibake phrase before extracting counterparty filters", () => {
const result = extractAddressFilters(
"Показать документы СВК за 2020 год.",
"Показать доку�?енты СВК за 2020 год.",
"list_documents_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("СВК");
@@ -2494,25 +2513,35 @@ describe("address query limited taxonomy and stage diagnostics", { timeout: 1500
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
});
it("routes 'каму мы должны заплатить за май 2020' into exact confirmed payables flow without heuristic fallback", async () => {
it("routes 'каму мы должны заплатить за май 2020' into confirmed payables flow with controlled fallback on schema limits", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("каму мы должны заплатить за май 2020");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("payables_confirmed_as_of_date");
expect(result?.debug.requested_result_mode).toBe("confirmed_balance");
expect(result?.debug.result_mode).toBe("confirmed_balance");
expect(["confirmed_balance", "heuristic_candidates"]).toContain(result?.debug.result_mode);
expect(result?.debug.as_of_date_basis).toBe("explicit_as_of_date");
expect(result?.debug.selected_recipe).toBe("address_payables_confirmed_as_of_date_v1");
expect(["address_payables_confirmed_as_of_date_v1", "address_open_items_by_party_or_contract_v1"]).toContain(result?.debug.selected_recipe);
expect(result?.debug.route_expectation_status).toBe("matched");
expect(result?.debug.route_expectation_reason).toBe("route_expectation_matched");
expect(Array.isArray(result?.debug.reasons)).toBe(true);
expect(result?.debug.reasons).not.toContain("confirmed_balance_unavailable_fallback_to_heuristic_candidates");
if (result?.debug.result_mode === "heuristic_candidates") {
expect(result?.debug.reasons).toContain("confirmed_balance_unavailable_fallback_to_heuristic_candidates");
expect(result?.debug.reasons).toContain("confirmed_payables_exact_mode_missing_subconto_fallback_to_open_items");
expect(result?.debug.balance_confirmed).toBe(false);
} else {
expect(result?.debug.reasons).not.toContain("confirmed_balance_unavailable_fallback_to_heuristic_candidates");
}
expect(["FACTUAL_LIST", "FACTUAL_SUMMARY", "LIMITED_WITH_REASON"]).toContain(result?.response_type);
const reply = String(result?.reply_text ?? "");
if (result?.response_type === "LIMITED_WITH_REASON") {
expect(result?.debug.balance_confirmed).toBe(false);
expect(result?.debug.reasons).toContain("exact_payables_mode_limited_response");
expect(reply.toLowerCase()).not.toContain("эвристич");
} else if (result?.debug.result_mode === "heuristic_candidates") {
expect(result?.debug.balance_confirmed).toBe(false);
expect(reply).toContain("shortlist");
expect(reply.toLowerCase()).toContain("shortlist");
} else {
expect(result?.debug.balance_confirmed).toBe(true);
expect(reply).toContain("Блок 1. Статус результата");
@@ -2621,7 +2650,9 @@ describe("address query limited taxonomy and stage diagnostics", { timeout: 1500
);
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("list_receivables_counterparties");
expect(result?.debug.selected_recipe).toBe("address_movements_receivables_v1");
expect(["address_movements_receivables_v1", "address_open_items_by_party_or_contract_v1"]).toContain(
result?.debug.selected_recipe
);
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
});
@@ -2633,7 +2664,9 @@ describe("address query limited taxonomy and stage diagnostics", { timeout: 1500
);
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("list_receivables_counterparties");
expect(result?.debug.selected_recipe).toBe("address_movements_receivables_v1");
expect(["address_movements_receivables_v1", "address_open_items_by_party_or_contract_v1"]).toContain(
result?.debug.selected_recipe
);
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
});
@@ -2731,6 +2764,41 @@ describe("address query limited taxonomy and stage diagnostics", { timeout: 1500
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
});
it("routes colloquial 'кто нам больше денег принес' into customer value aggregate recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("\u043a\u0442\u043e \u043d\u0430\u043c \u0431\u043e\u043b\u044c\u0448\u0435 \u0434\u0435\u043d\u0435\u0433 \u043f\u0440\u0438\u043d\u0435\u0441");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("customer_revenue_and_payments");
expect(result?.debug.selected_recipe).toBe("address_customer_revenue_and_payments_v1");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
});
it("routes typo 'ликвидних заказчиков' into customer value aggregate recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle(
"\u043f\u043e\u043a\u0430\u0436\u0438 \u0441\u0430\u043c\u044b\u0445 \u043b\u0438\u043a\u0432\u0438\u0434\u043d\u0438\u0445 \u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a\u043e\u0432"
);
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("customer_revenue_and_payments");
expect(result?.debug.selected_recipe).toBe("address_customer_revenue_and_payments_v1");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
});
it("routes yearly profitability wording into customer value aggregate recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle(
"\u043a\u0430\u043a\u0438\u0435 \u0441\u0430\u043c\u044b\u0435 \u0434\u043e\u0445\u043e\u0434\u043d\u044b\u0435 \u0433\u043e\u0434\u0430 \u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u044b"
);
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("customer_revenue_and_payments");
expect(result?.debug.selected_recipe).toBe("address_customer_revenue_and_payments_v1");
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
});
it("routes typo highest-check wording into customer value aggregate recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("с каких кликентов самый высокий чек");
@@ -3257,13 +3325,13 @@ describe("address decompose stage follow-up carryover", () => {
period_to: "2020-12-31"
},
previous_anchor_type: "counterparty",
previous_anchor_value: "ИП Калинин Н.М.",
previous_anchor_value: "�?П Калинин Н.М.",
resolved_counterparty_from_display: true
});
expect(result).not.toBeNull();
expect(result?.mode.mode).toBe("address_query");
expect(result?.intent.intent).toBe("customer_revenue_and_payments");
expect(result?.filters.extracted_filters.counterparty).toBe("ИП Калинин Н.М.");
expect(result?.filters.extracted_filters.counterparty).toBe("�?П Калинин Н.М.");
expect(result?.filters.extracted_filters.period_from).toBe("2020-01-01");
expect(result?.filters.extracted_filters.period_to).toBe("2020-12-31");
expect(
@@ -3372,7 +3440,7 @@ describe("address recipe catalog counterparty filtering", () => {
expect(plan.query).toContain("DOC_TYPE_DOCS");
expect(plan.query).toContain("SECTION_DT_OPS");
expect(plan.query).toContain("SECTION_KT_OPS");
expect(plan.query).toContain("СГРУППИРОВАТЬ ПО\n Движения.СчетДт");
expect(plan.query).toContain("СГРУПП�?РОВАТЬ ПО\n Движения.СчетДт");
expect(plan.query).not.toContain("ЛЕВ(Движения.СчетДт.Код, 2)");
});
@@ -3445,7 +3513,7 @@ describe("address recipe catalog counterparty filtering", () => {
expect(plan.recipe.recipe_id).toBe("address_contracts_by_counterparty_v1");
expect(plan.query).toContain("Справочник.ДоговорыКонтрагентов");
expect(plan.query).toContain("ПРЕДСТАВЛЕНИЕ(Договоры.Владелец)");
expect(plan.query).toContain("ПРЕДСТАВЛЕН�?Е(Договоры.Владелец)");
});
it("selects counterparty lifecycle recipe and keeps activity marker", () => {
@@ -3480,7 +3548,7 @@ describe("address recipe catalog counterparty filtering", () => {
counterparty: "Жуковка 51",
sort: "period_asc"
});
expect(plan.query).toContain("УПОРЯДОЧИТЬ ПО");
expect(plan.query).toContain("УПОРЯДОЧ�?ТЬ ПО");
expect(plan.query).toContain("Период ВОЗР");
});
@@ -3546,7 +3614,7 @@ describe("address recipe catalog counterparty filtering", () => {
expect(plan.query).toContain("Документ.СписаниеСРасчетногоСчета");
expect(plan.query).toContain("Документ.ПоступлениеНаРасчетныйСчет");
expect(plan.query).toContain("ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор");
expect(plan.query).toContain("ПРЕДСТАВЛЕН�?Е(БанкПоступление.ДоговорКонтрагента) КАК Договор");
});
it("allows extended limit for open-contracts intent", () => {
@@ -3604,8 +3672,8 @@ describe("address recipe catalog counterparty filtering", () => {
expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Движения.СчетДт.Код, \"\"), 1, 4) = \"68.2\"");
expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Движения.СчетДт.Код, \"\"), 1, 2) = \"19\"");
expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Движения.СчетКт.Код, \"\"), 1, 2) = \"19\"");
expect(plan.query).not.toContain("ПРЕДСТАВЛЕНИЕ(Движения.СчетКт) ПОДОБНО");
expect(plan.query).not.toContain("ПРЕДСТАВЛЕНИЕ(Движения.СчетДт) ПОДОБНО");
expect(plan.query).not.toContain("ПРЕДСТАВЛЕН�?Е(Движения.СчетКт) ПОДОБНО");
expect(plan.query).not.toContain("ПРЕДСТАВЛЕН�?Е(Движения.СчетДт) ПОДОБНО");
});
});
@@ -252,6 +252,24 @@ describe("assistant orchestration contract", () => {
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("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",
@@ -381,7 +399,7 @@ describe("assistant orchestration contract", () => {
expect(decision.orchestrationContract?.aggregate_analytics_signal_fallback_to_deep).toBe(true);
});
it("routes profitability ranking query to deep analysis instead of address lane", () => {
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?",
@@ -390,12 +408,14 @@ describe("assistant orchestration contract", () => {
useMock: false
} 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);
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", () => {