Укрепить exact value-flow аналитику адресного контура

This commit is contained in:
2026-05-01 22:37:57 +03:00
parent 472d982486
commit 924f6fb0ea
11 changed files with 234 additions and 20 deletions
@@ -2822,9 +2822,9 @@ describe("address filter extraction for balance drilldown", () => {
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(customerValue.extracted_filters.limit).toBe(1000);
expect(supplierValue.extracted_filters.limit).toBe(1000);
expect(contractValue.extracted_filters.limit).toBe(1000);
expect(vatForecast.extracted_filters.limit).toBeUndefined();
expect(periodProfile.extracted_filters.period_to).toBeDefined();
expect(docSectionProfile.extracted_filters.period_to).toBeDefined();
@@ -2844,6 +2844,9 @@ describe("address filter extraction for balance drilldown", () => {
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");
expect(customerValue.warnings).toContain("value_analytics_sample_limit_expanded");
expect(supplierValue.warnings).toContain("value_analytics_sample_limit_expanded");
expect(contractValue.warnings).toContain("value_analytics_sample_limit_expanded");
expect(vatForecast.warnings).toContain("period_derived_from_month_phrase");
expect(vatForecast.warnings).toContain("period_from_derived_from_quarter_for_vat_forecast");
expect(vatForecast.warnings).toContain("period_to_derived_from_as_of_date_for_vat_forecast");
@@ -5066,6 +5069,17 @@ describe("address recipe catalog counterparty filtering", () => {
expect(plan.query).toContain("БанкПоступление.ДоговорКонтрагента");
});
it("expands customer value analytics sample independently from visible ranking size", () => {
const filters = extractAddressFilters("какой у нас самый доходный год", "customer_revenue_and_payments");
const selected = selectAddressRecipe("customer_revenue_and_payments", filters.extracted_filters);
expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, filters.extracted_filters);
expect(filters.extracted_filters.limit).toBe(1000);
expect(filters.warnings).toContain("value_analytics_sample_limit_expanded");
expect(plan.limit).toBe(1000);
});
it("selects supplier payouts recipe and keeps top-20 default", () => {
const selected = selectAddressRecipe("supplier_payouts_profile", {});
expect(selected.selected_recipe).toBeTruthy();
@@ -9,9 +9,16 @@ describe("address reply builders regressions", () => {
"customer_revenue_and_payments",
[
{
counterparty: "Чапурнов",
amount: 250000,
period: "2020-03-31",
registrator: "Поступление 1"
} as any,
{
counterparty: "Малый клиент",
amount: 100000,
period: "2020-04-30",
registrator: "Поступление 2"
} as any
],
{
@@ -31,7 +38,7 @@ describe("address reply builders regressions", () => {
detectContractValueFocus: () => "top_by_turnover",
detectMinOpsForAvgCheck: () => 1,
extractRequestedYearFromQuestion: () => null,
extractCounterpartyName: () => "Чапурнов",
extractCounterpartyName: (row: any) => row.counterparty ?? "Чапурнов",
extractContractName: () => null,
counterpartyLookupMatches: () => false,
toUtcDayTimestamp: () => null,
@@ -44,6 +51,47 @@ describe("address reply builders regressions", () => {
expect(result?.text.split("\n")[0]).toContain("Чапурнов");
});
it("does not overclaim a comparative top customer ranking when only one candidate is present", () => {
const result = composeCounterpartyAnalyticsReply(
"customer_revenue_and_payments",
[
{
amount: 250000,
period: "2021-03-31",
registrator: "Поступление 1"
} as any
],
{
userMessage: "кто больше всего принес денег в 2021"
},
{
formatPercent: () => null,
formatDateRu: (value: string) => value,
formatMoneyRub: (value: number) => `${value}`,
extractYearFromIso: (value: string | null) => (value ? Number(value.slice(0, 4)) : null),
detectCounterpartyProfileFocus: () => "full_profile",
detectCounterpartyLifecycleFocus: () => "active_customers_all_time",
hasCounterpartyLifecycleLongevityQuestion: () => false,
hasCounterpartyActivityAgeQuestion: () => false,
detectRankingLimit: () => 5,
detectValueRankingFocus: () => "top_by_total",
detectContractValueFocus: () => "top_by_turnover",
detectMinOpsForAvgCheck: () => 1,
extractRequestedYearFromQuestion: () => null,
extractCounterpartyName: () => "Группа СВК",
extractContractName: () => null,
counterpartyLookupMatches: () => false,
toUtcDayTimestamp: () => null,
formatAgeYearsMonthsDays: () => "0 дней",
normalizeQuestionText: (value: string | null | undefined) => String(value ?? "")
}
);
expect(result?.text.split("\n")[0]).toContain("найден один клиент");
expect(result?.text.split("\n")[0]).toContain("не полноценный сравнительный рейтинг");
expect(result?.text).not.toContain("Самый доходный клиент");
});
it("starts top year aggregate reply with a direct business answer", () => {
const result = composeCounterpartyAnalyticsReply(
"customer_revenue_and_payments",
@@ -761,6 +761,46 @@ describe("assistant orchestration contract", () => {
expect(decision.orchestrationContract?.aggregate_analytics_signal_fallback_to_deep).toBe(false);
});
it("keeps all-time top customer ranking in address lane instead of stale deep problem answer", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "кто у нас самый доходный клиент за все время",
effectiveAddressUserMessage: "определить самого доходного клиента за весь период",
followupContext: {
previous_intent: "counterparty_activity_lifecycle",
previous_filters: {
organization: "ООО Альтернатива Плюс"
}
},
llmPreDecomposeMeta: {
applied: true,
llmCanonicalCandidateDetected: true,
predecomposeContract: {
mode: "address_query",
mode_confidence: "medium",
intent: "customer_revenue_and_payments",
intent_confidence: "medium"
},
semanticExtractionContract: {
valid: false,
apply_canonical_recommended: false,
extraction: {
query_shape: "AGGREGATE_LOOKUP",
aggregation_profile: "management_profile"
},
guard_hints: {},
reason_codes: ["ranking_semantic_guard_rejected"]
}
} 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.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: