АДРЕСНЫЙ РЕЖИМ -ADDRESS:Шаг 2 - Универсализация value-вопросов общего домена (TOP-20, без словарей клиентов/поставщиков
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user