АДРЕСНЫЙ РЕЖИМ -ADDRESS:Шаг 2 - Универсализация value-вопросов общего домена (TOP-20, без словарей клиентов/поставщиков

This commit is contained in:
2026-04-03 00:05:58 +03:00
parent 88094c09f8
commit 58b293a3e4
18 changed files with 2126 additions and 20 deletions
@@ -90,6 +90,31 @@ describe("address query shape classifier", () => {
expect(result.mode).toBe("address_query");
});
it("keeps customer value ranking question in address lane", () => {
const result = detectAddressQuestionMode("какие клиенты самые доходные, выдай топ-20");
expect(result.mode).toBe("address_query");
});
it("keeps highest inflow slang question in address lane", () => {
const result = detectAddressQuestionMode("какие приходы самые высокие за все время");
expect(result.mode).toBe("address_query");
});
it("keeps typo customer highest-check question in address lane", () => {
const result = detectAddressQuestionMode("с каких кликентов самый высокий чек");
expect(result.mode).toBe("address_query");
});
it("keeps supplier payout ranking question in address lane", () => {
const result = detectAddressQuestionMode("кому мы больше всего сгрузили денег, топ-20 поставщиков");
expect(result.mode).toBe("address_query");
});
it("keeps contract turnover ranking question in address lane", () => {
const result = detectAddressQuestionMode("договоры по обороту ранкни и дай топ-20");
expect(result.mode).toBe("address_query");
});
});
describe("address compose stage utf8 headers", () => {
@@ -910,6 +935,184 @@ describe("address compose stage utf8 headers", () => {
expect(reply.text).toContain("Использованных договоров (есть factual связь с операциями): 148.");
expect(reply.text).toContain("Неиспользуемых договоров: 372.");
});
it("renders customer value top list with explicit top-2 limit", () => {
const reply = composeFactualReply(
"customer_revenue_and_payments",
[
{
period: "2020-03-01T00:00:00Z",
registrator: "Поступление 1",
account_dt: "",
account_kt: "",
amount: 500,
analytics: ["Клиент А", "Договор А-1"]
},
{
period: "2020-03-02T00:00:00Z",
registrator: "Поступление 2",
account_dt: "",
account_kt: "",
amount: 700,
analytics: ["Клиент Б", "Договор Б-1"]
},
{
period: "2020-03-03T00:00:00Z",
registrator: "Поступление 3",
account_dt: "",
account_kt: "",
amount: 300,
analytics: ["Клиент А", "Договор А-1"]
}
],
{ userMessage: "покажи топ-2 заказчиков по сумме поступлений" }
);
expect(reply.responseType).toBe("FACTUAL_LIST");
expect(reply.text).toContain("Топ-2 заказчиков по сумме поступлений:");
expect(reply.text).toContain("1. Клиент А | сумма: 800");
expect(reply.text).toContain("2. Клиент Б | сумма: 700");
});
it("renders top incoming deals for highest inflow wording", () => {
const reply = composeFactualReply(
"customer_revenue_and_payments",
[
{
period: "2020-03-01T00:00:00Z",
registrator: "Поступление 1",
account_dt: "",
account_kt: "",
amount: 500,
analytics: ["Клиент А", "Договор А-1"]
},
{
period: "2020-03-02T00:00:00Z",
registrator: "Поступление 2",
account_dt: "",
account_kt: "",
amount: 700,
analytics: ["Клиент Б", "Договор Б-1"]
}
],
{ userMessage: "какие приходы самые высокие за все время" }
);
expect(reply.responseType).toBe("FACTUAL_LIST");
expect(reply.text).toContain("самых крупных разовых сделок по поступлениям");
expect(reply.text).toContain("Поступление 2");
});
it("renders max-single ranking for highest-check typo wording", () => {
const reply = composeFactualReply(
"customer_revenue_and_payments",
[
{
period: "2020-03-01T00:00:00Z",
registrator: "Поступление 1",
account_dt: "",
account_kt: "",
amount: 500,
analytics: ["Клиент А", "Договор А-1"]
},
{
period: "2020-03-02T00:00:00Z",
registrator: "Поступление 2",
account_dt: "",
account_kt: "",
amount: 1200,
analytics: ["Клиент Б", "Договор Б-1"]
},
{
period: "2020-03-03T00:00:00Z",
registrator: "Поступление 3",
account_dt: "",
account_kt: "",
amount: 300,
analytics: ["Клиент А", "Договор А-1"]
}
],
{ userMessage: "с каких кликентов самый высокий чек" }
);
expect(reply.responseType).toBe("FACTUAL_LIST");
expect(reply.text).toContain("по максимальной сумме одной входящей операции");
expect(reply.text).toContain("1. Клиент Б | max single: 1200");
});
it("renders supplier payout list by operations count", () => {
const reply = composeFactualReply(
"supplier_payouts_profile",
[
{
period: "2020-03-01T00:00:00Z",
registrator: "Списание 1",
account_dt: "",
account_kt: "",
amount: 100,
analytics: ["Поставщик А", "Договор А-1"]
},
{
period: "2020-03-02T00:00:00Z",
registrator: "Списание 2",
account_dt: "",
account_kt: "",
amount: 120,
analytics: ["Поставщик А", "Договор А-2"]
},
{
period: "2020-03-03T00:00:00Z",
registrator: "Списание 3",
account_dt: "",
account_kt: "",
amount: 500,
analytics: ["Поставщик Б", "Договор Б-1"]
}
],
{ userMessage: "топ-20 поставщиков по количеству исходящих платежных операций" }
);
expect(reply.responseType).toBe("FACTUAL_LIST");
expect(reply.text).toContain("Топ-2 поставщиков по количеству исходящих платежных операций:");
expect(reply.text).toContain("1. Поставщик А | операций: 2");
});
it("renders contract value list for minimal active budgets", () => {
const reply = composeFactualReply(
"contract_usage_and_value",
[
{
period: "2020-03-01T00:00:00Z",
registrator: "CT_VALUE_IN",
account_dt: "",
account_kt: "",
amount: 900,
analytics: ["Клиент А", "Договор 01/20"]
},
{
period: "2020-03-02T00:00:00Z",
registrator: "CT_VALUE_OUT",
account_dt: "",
account_kt: "",
amount: 100,
analytics: ["Поставщик Б", "Договор 02/20"]
},
{
period: "2020-03-03T00:00:00Z",
registrator: "CT_VALUE_IN",
account_dt: "",
account_kt: "",
amount: 150,
analytics: ["Клиент В", "Договор 03/20"]
}
],
{ userMessage: "покажи топ-20 договоров с минимальным бюджетом среди активных договоров" }
);
expect(reply.responseType).toBe("FACTUAL_LIST");
expect(reply.text).toContain("активных договоров с минимальным бюджетом");
expect(reply.text).toContain("1. Договор 02/20 | оборот: 100");
});
});
describe("address intent resolver expansion (M2.3a)", () => {
@@ -1150,6 +1353,31 @@ describe("address intent resolver expansion (M2.3a)", () => {
expect(result.intent).toBe("contract_usage_overview");
});
it("resolves customer revenue/payout ranking intent", () => {
const result = resolveAddressIntent("какие клиенты самые доходные, выдай топ-20");
expect(result.intent).toBe("customer_revenue_and_payments");
});
it("resolves customer revenue intent from highest inflow slang wording", () => {
const result = resolveAddressIntent("какие приходы самые высокие за все время");
expect(result.intent).toBe("customer_revenue_and_payments");
});
it("resolves customer revenue intent from typo highest-check wording", () => {
const result = resolveAddressIntent("с каких кликентов самый высокий чек");
expect(result.intent).toBe("customer_revenue_and_payments");
});
it("resolves supplier payouts profile intent from slang wording", () => {
const result = resolveAddressIntent("кому мы больше всего сгрузили денег, топ-20 поставщиков");
expect(result.intent).toBe("supplier_payouts_profile");
});
it("resolves contract usage and value intent", () => {
const result = resolveAddressIntent("договоры по обороту ранкни и дай топ-20");
expect(result.intent).toBe("contract_usage_and_value");
});
it("resolves contracts-by-counterparty intent from list wording", () => {
const result = resolveAddressIntent("покажи договора все по жуковке 51");
expect(result.intent).toBe("list_contracts_by_counterparty");
@@ -1175,21 +1403,42 @@ describe("address filter extraction for balance drilldown", () => {
"Сколько всего договоров заведено и сколько из них реально использовались?",
"contract_usage_overview"
);
const customerValue = extractAddressFilters(
"какие клиенты самые доходные, выдай топ-20",
"customer_revenue_and_payments"
);
const supplierValue = extractAddressFilters(
"кому мы больше всего сгрузили денег, топ-20 поставщиков",
"supplier_payouts_profile"
);
const contractValue = extractAddressFilters(
"договоры по обороту ранкни и дай топ-20",
"contract_usage_and_value"
);
expect(periodProfile.extracted_filters.limit).toBeUndefined();
expect(docSectionProfile.extracted_filters.limit).toBeUndefined();
expect(counterpartyProfile.extracted_filters.limit).toBeUndefined();
expect(counterpartyLifecycle.extracted_filters.limit).toBeUndefined();
expect(contractOverview.extracted_filters.limit).toBeUndefined();
expect(customerValue.extracted_filters.limit).toBe(20);
expect(supplierValue.extracted_filters.limit).toBe(20);
expect(contractValue.extracted_filters.limit).toBe(20);
expect(periodProfile.extracted_filters.period_to).toBeDefined();
expect(docSectionProfile.extracted_filters.period_to).toBeDefined();
expect(counterpartyProfile.extracted_filters.period_to).toBeDefined();
expect(counterpartyLifecycle.extracted_filters.period_to).toBeDefined();
expect(contractOverview.extracted_filters.period_to).toBeDefined();
expect(customerValue.extracted_filters.period_to).toBeDefined();
expect(supplierValue.extracted_filters.period_to).toBeDefined();
expect(contractValue.extracted_filters.period_to).toBeDefined();
expect(periodProfile.warnings).toContain("period_to_defaulted_today_for_management_profile");
expect(docSectionProfile.warnings).toContain("period_to_defaulted_today_for_management_profile");
expect(counterpartyProfile.warnings).toContain("period_to_defaulted_today_for_management_profile");
expect(counterpartyLifecycle.warnings).not.toContain("period_to_defaulted_today_for_management_profile");
expect(contractOverview.warnings).toContain("period_to_defaulted_today_for_management_profile");
expect(customerValue.warnings).toContain("period_to_defaulted_today_for_management_profile");
expect(supplierValue.warnings).toContain("period_to_defaulted_today_for_management_profile");
expect(contractValue.warnings).toContain("period_to_defaulted_today_for_management_profile");
});
it("extracts short-year period for lifecycle customer list question", () => {
@@ -1626,6 +1875,56 @@ describe("address query limited taxonomy and stage diagnostics", () => {
expect(["FACTUAL_SUMMARY", "LIMITED_WITH_REASON"]).toContain(result?.response_type);
});
it("routes customer value question into dedicated aggregate recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("какие клиенты самые доходные, выдай топ-20");
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 highest inflow slang wording into customer value aggregate recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("какие приходы самые высокие за все время");
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 highest-check wording into customer value aggregate recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("с каких кликентов самый высокий чек");
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 supplier payout question into dedicated aggregate recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("кому мы больше всего сгрузили денег, топ-20 поставщиков");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("supplier_payouts_profile");
expect(result?.debug.selected_recipe).toBe("address_supplier_payouts_profile_v1");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
});
it("routes contract value question into dedicated aggregate recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("договоры по обороту ранкни и дай топ-20");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("contract_usage_and_value");
expect(result?.debug.selected_recipe).toBe("address_contract_usage_and_value_v1");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
});
it("routes customer lifecycle question into dedicated aggregate recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("Какие заказчики работали с нами в 2020 году?");
@@ -1966,6 +2265,40 @@ describe("address recipe catalog counterparty filtering", () => {
expect(plan.query).toContain("ДоговорКонтрагента");
});
it("selects customer value recipe and keeps top-20 default", () => {
const selected = selectAddressRecipe("customer_revenue_and_payments", {});
expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, {});
expect(plan.recipe.recipe_id).toBe("address_customer_revenue_and_payments_v1");
expect(plan.limit).toBe(20);
expect(plan.query).toContain("ПоступлениеНаРасчетныйСчет");
expect(plan.query).toContain("БанкПоступление.ДоговорКонтрагента");
});
it("selects supplier payouts recipe and keeps top-20 default", () => {
const selected = selectAddressRecipe("supplier_payouts_profile", {});
expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, {});
expect(plan.recipe.recipe_id).toBe("address_supplier_payouts_profile_v1");
expect(plan.limit).toBe(20);
expect(plan.query).toContain("СписаниеСРасчетногоСчета");
expect(plan.query).toContain("БанкСписание.ДоговорКонтрагента");
});
it("selects contract value recipe and keeps top-20 default", () => {
const selected = selectAddressRecipe("contract_usage_and_value", {});
expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, {});
expect(plan.recipe.recipe_id).toBe("address_contract_usage_and_value_v1");
expect(plan.limit).toBe(20);
expect(plan.query).toContain("CT_VALUE_IN");
expect(plan.query).toContain("CT_VALUE_OUT");
expect(plan.query).toContain("ДоговорКонтрагента");
});
it("selects contracts-by-counterparty recipe from contract catalog", () => {
const selected = selectAddressRecipe("list_contracts_by_counterparty", {
counterparty: "Жуковка 51"