Завершить phase99-105 schema primitive closure
This commit is contained in:
@@ -584,11 +584,39 @@ describe("address compose stage utf8 headers", () => {
|
||||
|
||||
expect(reply.text).toContain("по СБЕРБАНК");
|
||||
expect(reply.text).toContain("входящее поступление от банка в этом срезе не подтверждено");
|
||||
expect(reply.text).toContain("клиентскую выручку");
|
||||
expect(reply.text).toContain("не подтвержденная клиентская выручка");
|
||||
expect(reply.text).toContain("Сводка по направлению");
|
||||
expect(reply.text).toContain("Основание 1С");
|
||||
expect(reply.text).toContain("вид операции/назначение платежа/договор");
|
||||
});
|
||||
|
||||
it("keeps bank operation drilldown compact when many rows are available", () => {
|
||||
const rows = Array.from({ length: 8 }, (_, index) => ({
|
||||
period: `2020-01-${String(index + 1).padStart(2, "0")}T12:00:00Z`,
|
||||
registrator:
|
||||
index % 2 === 0
|
||||
? `Поступление на расчетный счет 0000000000${index}`
|
||||
: `Списание с расчетного счета 0000000000${index}`,
|
||||
account_dt: "0",
|
||||
account_kt: "0",
|
||||
amount: 100 + index,
|
||||
analytics: ["СБЕРБАНК, ПАО", "0"],
|
||||
counterparty: "СБЕРБАНК, ПАО",
|
||||
operation_kind: index % 2 === 0 ? "Прочее поступление" : "Прочее списание",
|
||||
payment_purpose: index % 2 === 0 ? "Депозит" : "Комиссия банка"
|
||||
}));
|
||||
|
||||
const reply = composeFactualReply("bank_operations_by_counterparty", rows, {
|
||||
counterpartyHint: "СБЕРБАНК",
|
||||
userMessage: "СБЕРБАНК это поставщик или финансовые списания?"
|
||||
});
|
||||
|
||||
expect(reply.text).toContain("Сводка по направлению");
|
||||
expect(reply.text).toContain("Это не обычный поставщик автоматически");
|
||||
expect(reply.text).toContain("Показаны первые 5 из 8");
|
||||
expect(reply.text).not.toContain("00000000007");
|
||||
});
|
||||
|
||||
it("renders readable russian header for contracts-by-counterparty list", () => {
|
||||
const reply = composeFactualReply("list_contracts_by_counterparty", [
|
||||
{
|
||||
@@ -2692,6 +2720,13 @@ describe("address intent resolver expansion (M2.3a)", () => {
|
||||
expect(result.intent).toBe("supplier_payouts_profile");
|
||||
});
|
||||
|
||||
it("resolves explicit supplier payment amount question into supplier payouts profile", () => {
|
||||
const result = resolveAddressIntent(
|
||||
"А теперь по поставщику Группа СВК за 2020: сколько мы ему заплатили и какой общий денежный смысл?"
|
||||
);
|
||||
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");
|
||||
@@ -2989,6 +3024,38 @@ describe("address filter extraction for balance drilldown", () => {
|
||||
expect(extracted.extracted_filters.counterparty).toBeUndefined();
|
||||
});
|
||||
|
||||
it("drops generic customer/supplier role tail from broad company overview wording", () => {
|
||||
const extracted = extractAddressFilters(
|
||||
"Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток, если по назначению он не обычный клиент или поставщик.",
|
||||
"customer_revenue_and_payments"
|
||||
);
|
||||
expect(extracted.extracted_filters.counterparty).toBeUndefined();
|
||||
expect(extracted.extracted_filters.period_from).toBe("2020-01-01");
|
||||
expect(extracted.extracted_filters.period_to).toBe("2020-12-31");
|
||||
expect(extracted.warnings).toContain("counterparty_anchor_dropped_low_quality");
|
||||
});
|
||||
|
||||
it("clears generic customer/supplier role tail from inherited follow-up anchor", () => {
|
||||
const result = runAddressDecomposeStage(
|
||||
"Покажи документы по нему за 2020",
|
||||
{
|
||||
previous_intent: "list_documents_by_counterparty",
|
||||
previous_anchor_type: "counterparty",
|
||||
previous_anchor_value: "или поставщик"
|
||||
}
|
||||
);
|
||||
expect(result?.filters.extracted_filters.counterparty).toBeUndefined();
|
||||
expect(result?.filters.extracted_filters.period_from).toBe("2020-01-01");
|
||||
expect(result?.filters.extracted_filters.period_to).toBe("2020-12-31");
|
||||
expect(result?.filters.warnings).toContain("counterparty_cleared_low_quality_followup_anchor");
|
||||
});
|
||||
|
||||
it("keeps real counterparty after explicit supplier role prefix", () => {
|
||||
const extracted = extractAddressFilters("Покажи платежи по поставщику Альфа за июль 2020", "supplier_payouts_profile");
|
||||
expect(extracted.extracted_filters.counterparty).toBe("Альфа");
|
||||
expect(extracted.warnings).not.toContain("counterparty_anchor_dropped_low_quality");
|
||||
});
|
||||
|
||||
it("derives VAT forecast quarter-to-date window when plain date phrase is present", () => {
|
||||
const extracted = extractAddressFilters(
|
||||
"мож прикинусь плиз скока ндс надо заплатить на 15 марта 2020 года",
|
||||
@@ -4751,6 +4818,65 @@ describe("address decompose stage follow-up carryover", () => {
|
||||
expect(result?.baseReasons).toContain("address_followup_context_applied");
|
||||
});
|
||||
|
||||
it("does not inherit stale counterparty for broad receivables mirror question", () => {
|
||||
const result = runAddressDecomposeStage("а нам кто должен на конец 2020?", {
|
||||
previous_intent: "payables_confirmed_as_of_date",
|
||||
previous_filters: {
|
||||
organization: "ООО Альтернатива Плюс",
|
||||
counterparty: "Группа СВК",
|
||||
period_from: "2020-01-01",
|
||||
period_to: "2020-12-31",
|
||||
as_of_date: "2020-12-31"
|
||||
},
|
||||
previous_anchor_type: "counterparty",
|
||||
previous_anchor_value: "Группа СВК"
|
||||
});
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.intent.intent).toBe("receivables_confirmed_as_of_date");
|
||||
expect(result?.filters.extracted_filters.organization).toBe("ООО Альтернатива Плюс");
|
||||
expect(result?.filters.extracted_filters.as_of_date).toBe("2020-12-31");
|
||||
expect(result?.filters.extracted_filters.counterparty).toBeUndefined();
|
||||
expect(result?.baseReasons).toContain("counterparty_carryover_suppressed_for_broad_debt_polarity_question");
|
||||
expect(result?.baseReasons).not.toContain("counterparty_from_followup_context");
|
||||
});
|
||||
|
||||
it("does not inherit stale counterparty for broad payables mirror question", () => {
|
||||
const result = runAddressDecomposeStage("кому мы должны на конец 2020?", {
|
||||
previous_intent: "supplier_payouts_profile",
|
||||
previous_filters: {
|
||||
organization: "ООО Альтернатива Плюс",
|
||||
counterparty: "Группа СВК",
|
||||
period_from: "2020-01-01",
|
||||
period_to: "2020-12-31"
|
||||
},
|
||||
previous_anchor_type: "counterparty",
|
||||
previous_anchor_value: "Группа СВК"
|
||||
});
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.intent.intent).toBe("payables_confirmed_as_of_date");
|
||||
expect(result?.filters.extracted_filters.organization).toBe("ООО Альтернатива Плюс");
|
||||
expect(result?.filters.extracted_filters.counterparty).toBeUndefined();
|
||||
expect(result?.baseReasons).toContain("counterparty_carryover_suppressed_for_broad_debt_polarity_question");
|
||||
});
|
||||
|
||||
it("keeps referential counterparty carryover for debt question that explicitly says по нему", () => {
|
||||
const result = runAddressDecomposeStage("по нему сколько он нам должен на конец 2020?", {
|
||||
previous_intent: "customer_revenue_and_payments",
|
||||
previous_filters: {
|
||||
organization: "ООО Альтернатива Плюс",
|
||||
counterparty: "Группа СВК",
|
||||
period_from: "2020-01-01",
|
||||
period_to: "2020-12-31"
|
||||
},
|
||||
previous_anchor_type: "counterparty",
|
||||
previous_anchor_value: "Группа СВК"
|
||||
});
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.intent.intent).toBe("receivables_confirmed_as_of_date");
|
||||
expect(result?.filters.extracted_filters.counterparty).toBe("Группа СВК");
|
||||
expect(result?.baseReasons).toContain("counterparty_from_followup_context");
|
||||
});
|
||||
|
||||
it("keeps contract scope when follow-up asks for bank operations without explicit anchor", () => {
|
||||
const result = runAddressDecomposeStage("а теперь банковские операции", {
|
||||
previous_intent: "list_documents_by_contract",
|
||||
@@ -5542,6 +5668,13 @@ it("routes old purchase residue questions to aging-by-purchase-date", () => {
|
||||
expect(filters.as_of_date).toBe("2020-03-31");
|
||||
});
|
||||
|
||||
it("does not treat generic 'по остаткам' as a warehouse anchor", () => {
|
||||
const result = runAddressDecomposeStage("кайф - что там на складе по остаткам?");
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.intent.intent).toBe("inventory_on_hand_as_of_date");
|
||||
expect(result?.filters.extracted_filters.warehouse).toBeUndefined();
|
||||
});
|
||||
|
||||
it("builds exact balance query for inventory-on-hand snapshot", () => {
|
||||
const selected = selectAddressRecipe("inventory_on_hand_as_of_date", {
|
||||
as_of_date: "2020-03-31"
|
||||
|
||||
@@ -131,7 +131,7 @@ describe("assistant address follow-up carryover", () => {
|
||||
} as any);
|
||||
|
||||
expect(second.ok).toBe(true);
|
||||
expect(["factual", "factual_with_explanation"]).toContain(second.reply_type);
|
||||
expect(["factual", "factual_with_explanation", "partial_coverage"]).toContain(second.reply_type);
|
||||
expect(second.debug?.detected_mode).toBe("address_query");
|
||||
expect(second.debug?.detected_intent).toBe("list_documents_by_counterparty");
|
||||
expect(second.debug?.extracted_filters?.counterparty).toBe("свк");
|
||||
|
||||
@@ -28,13 +28,16 @@ function buildInput(overrides: Record<string, unknown> = {}) {
|
||||
hasOperationalAdminActionRequestSignal: () => false,
|
||||
hasOrganizationFactLookupSignal: () => false,
|
||||
hasOrganizationFactFollowupSignal: () => false,
|
||||
hasLivingChatSignal: () => false,
|
||||
shouldEmitOrganizationSelectionReply: () => false,
|
||||
hasAssistantCapabilityQuestionSignal: () => false,
|
||||
resolveOrganizationSelectionFromMessage: () => null,
|
||||
resolveDataScopeProbe: () => null,
|
||||
applyScriptGuard: (chatText: string) => chatText,
|
||||
applyGroundingGuard: (guardInput: Record<string, unknown>) => guardInput,
|
||||
buildAssistantSafetyRefusalReply: () => "safety",
|
||||
buildAssistantDataScopeContractReply: () => "scope",
|
||||
buildAssistantProactiveOrganizationOfferReply: () => "offer",
|
||||
buildAssistantOrganizationFactBoundaryReply: () => "boundary",
|
||||
buildAssistantDataScopeSelectionReply: () => "selection",
|
||||
buildAssistantOperationalBoundaryReply: () => "operational",
|
||||
|
||||
@@ -46,8 +46,10 @@ describe("assistant living chat attempt runtime input builder", () => {
|
||||
hasOperationalAdminActionRequestSignal: vi.fn(() => false),
|
||||
hasOrganizationFactLookupSignal: vi.fn(() => false),
|
||||
hasOrganizationFactFollowupSignal: vi.fn(() => false),
|
||||
hasLivingChatSignal: vi.fn(() => false),
|
||||
shouldEmitOrganizationSelectionReply: vi.fn(() => false),
|
||||
hasAssistantCapabilityQuestionSignal: vi.fn(() => false),
|
||||
resolveOrganizationSelectionFromMessage: vi.fn(() => null),
|
||||
resolveDataScopeProbe: vi.fn(async () => null),
|
||||
executeLlmChat,
|
||||
applyScriptGuard: vi.fn((text: string) => ({ text, applied: false, reason: null })),
|
||||
@@ -58,6 +60,7 @@ describe("assistant living chat attempt runtime input builder", () => {
|
||||
})),
|
||||
buildAssistantSafetyRefusalReply: vi.fn(() => "safety"),
|
||||
buildAssistantDataScopeContractReply: vi.fn(() => "scope"),
|
||||
buildAssistantProactiveOrganizationOfferReply: vi.fn(() => "offer"),
|
||||
buildAssistantOrganizationFactBoundaryReply: vi.fn(() => "boundary"),
|
||||
buildAssistantDataScopeSelectionReply: vi.fn(() => "selection"),
|
||||
buildAssistantOperationalBoundaryReply: vi.fn(() => "operational"),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { runAssistantLivingChatRuntime } from "../src/services/assistantLivingChatRuntimeAdapter";
|
||||
import { resolveOrganizationSelectionFromMessage } from "../src/services/assistantOrganizationMatcher";
|
||||
|
||||
function buildRuntimeInput(overrides: Record<string, unknown> = {}) {
|
||||
const executeLlmChat = vi.fn(async () => "llm-text");
|
||||
@@ -37,6 +38,7 @@ function buildRuntimeInput(overrides: Record<string, unknown> = {}) {
|
||||
hasLivingChatSignal: () => true,
|
||||
shouldEmitOrganizationSelectionReply: () => false,
|
||||
hasAssistantCapabilityQuestionSignal: () => false,
|
||||
resolveOrganizationSelectionFromMessage: () => null,
|
||||
resolveDataScopeProbe,
|
||||
executeLlmChat,
|
||||
applyScriptGuard: (chatText: string) => ({
|
||||
@@ -87,6 +89,47 @@ describe("assistant living chat runtime adapter", () => {
|
||||
expect(output.debug?.living_chat_data_scope_probe_org_count).toBe(1);
|
||||
});
|
||||
|
||||
it("probes data scope before LLM when a fresh chat turn is a bare organization name", async () => {
|
||||
const executeLlmChat = vi.fn(async () => "llm-text");
|
||||
const resolveDataScopeProbe = vi.fn(async () => ({
|
||||
status: "resolved",
|
||||
channel: "default",
|
||||
organizations: [
|
||||
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
|
||||
"\u041e\u041e\u041e \u041b\u0430\u0439\u0441\u0432\u0443\u0434"
|
||||
],
|
||||
error: null
|
||||
}));
|
||||
const input = buildRuntimeInput({
|
||||
userMessage: "\u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
|
||||
modeDecision: { mode: "chat", reason: "non_domain_query_indexed" },
|
||||
hasLivingChatSignal: () => false,
|
||||
shouldEmitOrganizationSelectionReply: (_message: string, organization: string | null) => Boolean(organization),
|
||||
resolveOrganizationSelectionFromMessage,
|
||||
resolveDataScopeProbe,
|
||||
executeLlmChat,
|
||||
buildAssistantDataScopeSelectionReply: (organization: string | null) => `selection:${organization ?? "none"}`
|
||||
});
|
||||
|
||||
const output = await runAssistantLivingChatRuntime(input);
|
||||
|
||||
expect(output.handled).toBe(true);
|
||||
expect(output.chatText).toBe(
|
||||
"selection:\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"
|
||||
);
|
||||
expect(output.debug?.living_chat_response_source).toBe("deterministic_data_scope_selection_contract");
|
||||
expect(output.debug?.living_chat_bare_scope_probe_attempted).toBe(true);
|
||||
expect(output.debug?.living_chat_bare_scope_probe_matched_organization).toBe(
|
||||
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"
|
||||
);
|
||||
expect(output.debug?.assistant_active_organization).toBe(
|
||||
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"
|
||||
);
|
||||
expect(output.debug?.living_chat_data_scope_probe_org_count).toBe(2);
|
||||
expect(resolveDataScopeProbe).toHaveBeenCalledTimes(1);
|
||||
expect(executeLlmChat).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("selects safety refusal branch for dangerous capability meta query", async () => {
|
||||
const executeLlmChat = vi.fn(async () => "llm-text");
|
||||
const input = buildRuntimeInput({
|
||||
|
||||
@@ -311,6 +311,104 @@ describe("assistant MCP discovery pilot executor", () => {
|
||||
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(6);
|
||||
});
|
||||
|
||||
it("recovers explicit-year business overview money coverage through monthly value-flow chunks", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
dataNeedGraph: {
|
||||
schema_version: "assistant_data_need_graph_v1",
|
||||
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||
subject_candidates: [],
|
||||
business_fact_family: "business_overview",
|
||||
action_family: "broad_evaluation",
|
||||
aggregation_need: null,
|
||||
time_scope_need: "explicit_period",
|
||||
comparison_need: null,
|
||||
ranking_need: null,
|
||||
proof_expectation: "bounded_inference",
|
||||
clarification_gaps: [],
|
||||
decomposition_candidates: [
|
||||
"collect_scoped_movements",
|
||||
"aggregate_checked_amounts",
|
||||
"aggregate_ranked_axis_values",
|
||||
"fetch_supporting_documents",
|
||||
"probe_coverage",
|
||||
"explain_evidence_basis"
|
||||
],
|
||||
forbidden_overclaim_flags: ["no_raw_model_claims", "no_profit_or_margin_claim_without_evidence"],
|
||||
reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"]
|
||||
},
|
||||
turnMeaning: {
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "broad_evaluation",
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||
explicit_date_scope: "2020"
|
||||
}
|
||||
});
|
||||
const broadIncomingRows = Array.from({ length: 200 }, (_, index) => ({
|
||||
Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`,
|
||||
Amount: 1,
|
||||
Counterparty: "Клиент из широкого запроса"
|
||||
}));
|
||||
const broadOutgoingRows = Array.from({ length: 200 }, (_, index) => ({
|
||||
Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`,
|
||||
Amount: 1,
|
||||
Counterparty: "Поставщик из широкого запроса"
|
||||
}));
|
||||
const incomingMonthlyResults = Array.from({ length: 12 }, (_, index) => ({
|
||||
rows: [
|
||||
{
|
||||
Period: `2020-${String(index + 1).padStart(2, "0")}-05T00:00:00`,
|
||||
Amount: (index + 1) * 100,
|
||||
Counterparty: index % 2 === 0 ? "Клиент А" : "Клиент Б"
|
||||
}
|
||||
]
|
||||
}));
|
||||
const outgoingMonthlyResults = Array.from({ length: 12 }, (_, index) => ({
|
||||
rows: [
|
||||
{
|
||||
Period: `2020-${String(index + 1).padStart(2, "0")}-10T00:00:00`,
|
||||
Amount: (index + 1) * 50,
|
||||
Counterparty: index % 2 === 0 ? "Поставщик А" : "Поставщик Б"
|
||||
}
|
||||
]
|
||||
}));
|
||||
const deps = buildSequentialDeps([
|
||||
{ rows: broadIncomingRows },
|
||||
...incomingMonthlyResults,
|
||||
{ rows: broadOutgoingRows },
|
||||
...outgoingMonthlyResults
|
||||
]);
|
||||
|
||||
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
|
||||
|
||||
expect(planner.discovery_plan.execution_budget.max_probe_count).toBe(30);
|
||||
expect(result.derived_business_overview).toMatchObject({
|
||||
organization_scope: "ООО Альтернатива Плюс",
|
||||
period_scope: "2020",
|
||||
incoming_customer_revenue: {
|
||||
total_amount: 7800,
|
||||
coverage_limited_by_probe_limit: false,
|
||||
coverage_recovered_by_period_chunking: true,
|
||||
period_chunking_granularity: "month"
|
||||
},
|
||||
outgoing_supplier_payout: {
|
||||
total_amount: 3900,
|
||||
coverage_limited_by_probe_limit: false,
|
||||
coverage_recovered_by_period_chunking: true,
|
||||
period_chunking_granularity: "month"
|
||||
},
|
||||
net_amount: 3900,
|
||||
coverage_limited_by_probe_limit: false
|
||||
});
|
||||
expect(result.evidence.confirmed_facts).toContain(
|
||||
"Денежное покрытие бизнес-обзора за год восстановлено через помесячные 1С-проверки, а не только через широкий общий запрос."
|
||||
);
|
||||
expect(result.evidence.unknown_facts).not.toContain(
|
||||
"Полное покрытие бизнес-обзора не подтверждено: хотя бы один денежный запрос достиг верхней границы выборки."
|
||||
);
|
||||
expect(result.reason_codes).toContain("pilot_business_overview_incoming_monthly_period_chunking_recovered_coverage");
|
||||
expect(result.reason_codes).toContain("pilot_business_overview_outgoing_monthly_period_chunking_recovered_coverage");
|
||||
});
|
||||
|
||||
it("marks bank-like counterparties in business-overview rankings before evidence wording", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
dataNeedGraph: {
|
||||
|
||||
@@ -288,6 +288,48 @@ describe("assistant MCP discovery planner", () => {
|
||||
expect(result.reason_codes).toContain("planner_instantiated_catalog_chain_template_business_overview");
|
||||
});
|
||||
|
||||
it("enables chunked coverage budget for explicit-year business overviews", () => {
|
||||
const result = planAssistantMcpDiscovery({
|
||||
dataNeedGraph: {
|
||||
schema_version: "assistant_data_need_graph_v1",
|
||||
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||
subject_candidates: [],
|
||||
business_fact_family: "business_overview",
|
||||
action_family: "broad_evaluation",
|
||||
aggregation_need: null,
|
||||
time_scope_need: "explicit_period",
|
||||
comparison_need: null,
|
||||
ranking_need: null,
|
||||
proof_expectation: "bounded_inference",
|
||||
clarification_gaps: [],
|
||||
decomposition_candidates: [
|
||||
"collect_scoped_movements",
|
||||
"aggregate_checked_amounts",
|
||||
"aggregate_ranked_axis_values",
|
||||
"fetch_supporting_documents",
|
||||
"probe_coverage",
|
||||
"explain_evidence_basis"
|
||||
],
|
||||
forbidden_overclaim_flags: [
|
||||
"no_raw_model_claims",
|
||||
"no_unchecked_business_health_claim",
|
||||
"no_profit_or_margin_claim_without_evidence"
|
||||
],
|
||||
reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"]
|
||||
},
|
||||
turnMeaning: {
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "broad_evaluation",
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||
explicit_date_scope: "2020"
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.selected_chain_id).toBe("business_overview");
|
||||
expect(result.discovery_plan.execution_budget.max_probe_count).toBe(30);
|
||||
expect(result.reason_codes).toContain("planner_enabled_chunked_coverage_probe_budget");
|
||||
});
|
||||
|
||||
it("keeps bidirectional value-flow comparison executable when checked totals are derived without aggregate_by_axis", () => {
|
||||
const result = planAssistantMcpDiscovery({
|
||||
dataNeedGraph: {
|
||||
|
||||
@@ -159,6 +159,60 @@ describe("assistant MCP discovery response candidate", () => {
|
||||
expect(candidate.reply_text).not.toContain("47 628 853");
|
||||
});
|
||||
|
||||
it("answers profit follow-ups with a direct cash-flow boundary before accounting result detail", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
turn_input: {
|
||||
adapter_status: "ready",
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "profit_margin_boundary",
|
||||
unsupported_but_understood_family: "profit_margin_boundary",
|
||||
explicit_date_scope: "2020"
|
||||
},
|
||||
data_need_graph: {
|
||||
business_fact_family: "business_overview",
|
||||
ranking_need: null,
|
||||
reason_codes: ["data_need_graph_family_business_overview"]
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
user_facing_response_allowed: true,
|
||||
business_fact_answer_allowed: true,
|
||||
requires_user_clarification: false,
|
||||
pilot: {
|
||||
pilot_scope: "business_overview_route_template_v1",
|
||||
derived_business_overview: {
|
||||
accounting_financial_result: {
|
||||
period_scope: "2020",
|
||||
final_result_direction: "loss",
|
||||
final_result_amount_human_ru: "7 136 815,85 руб.",
|
||||
net_margin_to_revenue_pct: -59.41
|
||||
}
|
||||
}
|
||||
},
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference",
|
||||
headline: "Коротко: по бухгалтерскому маршруту 90/91/99 за 2020 подтвержден учетный убыток.",
|
||||
confirmed_lines: [],
|
||||
inference_lines: [],
|
||||
unknown_lines: [],
|
||||
limitation_lines: [],
|
||||
next_step_line: null
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
expect(candidate.reply_text).toContain("нет, денежное операционное нетто не стоит считать чистой прибылью");
|
||||
expect(candidate.reply_text).toContain("по закрытию счетов 90/91/99 в 1С за 2020");
|
||||
expect(candidate.reply_text).toContain("учетный убыток");
|
||||
expect(candidate.reply_text).toContain("маржа к подтвержденной выручке -59.41%");
|
||||
expect(candidate.reply_text).not.toContain("бухгалтерскому маршруту");
|
||||
expect(candidate.reply_text).not.toContain("маржа к выручке 90.01");
|
||||
});
|
||||
|
||||
it("keeps vendor-risk boundary answers direct instead of compacting into a money overview", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
@@ -392,6 +446,8 @@ describe("assistant MCP discovery response candidate", () => {
|
||||
expect(candidate.reply_text).toContain("не полный бухгалтерский рейтинг доходности");
|
||||
expect(candidate.reply_text).toContain("не как чистую бухгалтерскую прибыль");
|
||||
expect(candidate.reply_text).toContain("проверка достигла лимита строк");
|
||||
expect(candidate.reply_text).toContain("выбрать конкретный год или квартал для дозапроса");
|
||||
expect(candidate.reply_text).toContain("без выдачи непроверенного итога");
|
||||
expect(candidate.reply_text).not.toContain("лимит выборки MCP");
|
||||
expect(candidate.reply_text).not.toContain("MCP-срез");
|
||||
expect(candidate.reply_text).not.toContain("Что подтверждено:");
|
||||
@@ -465,11 +521,65 @@ describe("assistant MCP discovery response candidate", () => {
|
||||
expect(candidate.reply_text).toContain("12 474 036,91 руб");
|
||||
expect(candidate.reply_text?.split("\n")[0]).toContain("крупнейший источник входящих денег: ГКУ УКРиС");
|
||||
expect(candidate.reply_text?.split("\n")[0]).toContain("крупнейший получатель исходящих денег: ООО Поставщик");
|
||||
expect(candidate.reply_text).toContain("денежный операционный показатель");
|
||||
expect(candidate.reply_text).toContain("операционный денежный показатель");
|
||||
expect(candidate.reply_text).not.toContain("Что можно сказать только как вывод:");
|
||||
expect(candidate.reply_text).not.toContain("Складской срез");
|
||||
});
|
||||
|
||||
it("labels organization-scoped bidirectional value-flow continuations as company scope", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
turn_input: {
|
||||
adapter_status: "ready",
|
||||
turn_meaning_ref: {
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||
explicit_date_scope: "2020"
|
||||
},
|
||||
data_need_graph: {
|
||||
business_fact_family: "value_flow",
|
||||
comparison_need: "incoming_vs_outgoing",
|
||||
reason_codes: ["data_need_graph_family_value_flow"]
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
user_facing_response_allowed: true,
|
||||
business_fact_answer_allowed: true,
|
||||
requires_user_clarification: false,
|
||||
pilot: {
|
||||
derived_bidirectional_value_flow: {
|
||||
counterparty: null,
|
||||
period_scope: "2020",
|
||||
incoming_customer_revenue: {
|
||||
total_amount_human_ru: "47 628 853,03 руб.",
|
||||
rows_with_amount: 44
|
||||
},
|
||||
outgoing_supplier_payout: {
|
||||
total_amount_human_ru: "43 763 351,53 руб.",
|
||||
rows_with_amount: 299
|
||||
},
|
||||
net_amount_human_ru: "3 865 501,50 руб.",
|
||||
net_direction: "net_incoming",
|
||||
coverage_limited_by_probe_limit: false
|
||||
}
|
||||
},
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference",
|
||||
headline: "Денежный поток подтвержден.",
|
||||
confirmed_lines: [],
|
||||
inference_lines: [],
|
||||
unknown_lines: [],
|
||||
limitation_lines: [],
|
||||
next_step_line: null
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
expect(candidate.reply_text?.split("\n")[0]).toContain("по компании ООО Альтернатива Плюс за период 2020");
|
||||
expect(candidate.reply_text).not.toContain("по контрагенту запрошенному контрагенту");
|
||||
});
|
||||
|
||||
it("does not present bank-like incoming leaders as ordinary client revenue", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
|
||||
@@ -606,6 +606,64 @@ describe("assistant MCP discovery response policy", () => {
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_candidate_applied");
|
||||
});
|
||||
|
||||
it("keeps exact bank operation replies over generic value-flow discovery candidates", () => {
|
||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||
currentReply:
|
||||
"Exact bank operation answer: incoming/outgoing rows include operation kind, payment purpose, and contract; do not classify the bank as a regular customer or supplier automatically.",
|
||||
currentReplySource: "address_query_runtime_v1",
|
||||
currentReplyType: "factual",
|
||||
addressRuntimeMeta: {
|
||||
detected_intent: "bank_operations_by_counterparty",
|
||||
selected_recipe: "address_bank_operations_by_counterparty_v1",
|
||||
mcp_call_status: "matched_non_empty",
|
||||
capability_route_mode: "exact",
|
||||
answer_grounding_check: {
|
||||
status: "grounded"
|
||||
},
|
||||
assistant_mcp_discovery_entry_point_v1: entryPoint({
|
||||
turn_input: {
|
||||
adapter_status: "ready",
|
||||
should_run_discovery: true,
|
||||
data_need_graph: {
|
||||
business_fact_family: "value_flow",
|
||||
subject_candidates: ["SBERBANK"],
|
||||
reason_codes: ["data_need_graph_built"]
|
||||
},
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "payout",
|
||||
explicit_entity_candidates: ["SBERBANK"],
|
||||
explicit_date_scope: "2020"
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
user_facing_response_allowed: true,
|
||||
business_fact_answer_allowed: true,
|
||||
requires_user_clarification: false,
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference",
|
||||
headline: "Generic value-flow answer.",
|
||||
confirmed_lines: ["Outgoing payout total only."],
|
||||
inference_lines: ["Generic supplier payout interpretation."],
|
||||
unknown_lines: ["Incoming role is unknown."],
|
||||
limitation_lines: [],
|
||||
next_step_line: null
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.applied).toBe(false);
|
||||
expect(result.decision).toBe("keep_current_reply");
|
||||
expect(result.reply_text).toContain("operation kind");
|
||||
expect(result.reason_codes).toContain("mcp_discovery_response_policy_keep_exact_bank_operations_address_reply");
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_candidate_applied");
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_value_flow_action_conflict_allows_candidate_override");
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_semantic_conflict_allows_candidate_override");
|
||||
});
|
||||
|
||||
it("overrides an exact ranking-shaped address reply when open-scope ranking still needs organization", () => {
|
||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||
currentReply:
|
||||
|
||||
@@ -196,6 +196,32 @@ describe("assistant MCP discovery turn input adapter", () => {
|
||||
expect(result.reason_codes).toContain("mcp_discovery_bidirectional_value_flow_signal_detected");
|
||||
});
|
||||
|
||||
it("keeps multi-token scoped counterparty from net wording when LLM entities are empty", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage: "А теперь по Группа СВК за 2020: сколько денег получили, сколько заплатили и какое нетто?",
|
||||
followupContext: {
|
||||
previous_filters: {
|
||||
organization: "ООО Альтернатива Плюс",
|
||||
period_from: "2020-01-01",
|
||||
period_to: "2020-12-31"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.adapter_status).toBe("ready");
|
||||
expect(result.should_run_discovery).toBe(true);
|
||||
expect(result.turn_meaning_ref).toMatchObject({
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "net_value_flow",
|
||||
explicit_entity_candidates: ["Группа СВК"],
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting",
|
||||
stale_replay_forbidden: true
|
||||
});
|
||||
expect(result.reason_codes).toContain("mcp_discovery_counterparty_from_raw_scope");
|
||||
});
|
||||
|
||||
it("overrides a supported exact current-turn payout route when the question asks for a payment amount", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage:
|
||||
@@ -1733,6 +1759,36 @@ describe("assistant MCP discovery turn input adapter", () => {
|
||||
expect(result.reason_codes).toContain("mcp_discovery_business_overview_raw_year_overrode_predecompose_as_of_scope");
|
||||
});
|
||||
|
||||
it("does not turn generic role tails into predecompose counterparties for business overview", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage:
|
||||
"Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток, если по назначению он не обычный клиент или поставщик.",
|
||||
assistantTurnMeaning: {
|
||||
asked_domain_family: "business_summary",
|
||||
asked_action_family: "broad_evaluation",
|
||||
unsupported_but_understood_family: "broad_business_evaluation",
|
||||
stale_replay_forbidden: true
|
||||
},
|
||||
predecomposeContract: {
|
||||
entities: {
|
||||
counterparty: "или поставщик",
|
||||
organization: "ООО Альтернатива Плюс"
|
||||
},
|
||||
period: {
|
||||
period_from: "2020-01-01",
|
||||
period_to: "2020-12-31",
|
||||
has_explicit_period: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.adapter_status).toBe("ready");
|
||||
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
|
||||
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
|
||||
expect(result.turn_meaning_ref?.explicit_organization_scope).toBe("ООО Альтернатива Плюс");
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_counterparty_from_predecompose");
|
||||
});
|
||||
|
||||
it("keeps all-time business overview from reusing a negated VAT period as active scope", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage:
|
||||
|
||||
@@ -2013,4 +2013,48 @@ describe("assistantTransitionPolicy", () => {
|
||||
expect(carryover?.followupContext?.previous_discovery_entity_candidates).toEqual(["\u041d\u0414\u0421"]);
|
||||
expect(carryover?.followupContext?.previous_discovery_pilot_scope).toBe("metadata_inspection_v1");
|
||||
});
|
||||
|
||||
it("lets short receivables-to-payables mirror override an LLM open-items expansion", () => {
|
||||
const policy = buildPolicy({
|
||||
findLastAddressAssistantItem: () => ({
|
||||
text: "\u041a\u043e\u0440\u043e\u0442\u043a\u043e: \u043d\u0430\u043c \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430 13.05.2026.",
|
||||
debug: {
|
||||
detected_intent: "receivables_confirmed_as_of_date",
|
||||
selected_recipe: "address_receivables_confirmed_as_of_date_v1",
|
||||
extracted_filters: {
|
||||
organization: "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
|
||||
as_of_date: "2026-05-13"
|
||||
}
|
||||
}
|
||||
}),
|
||||
hasAddressFollowupContextSignal: () => true,
|
||||
resolveDebtRoleSwapFollowupIntent: (message: string, previousIntent: string) =>
|
||||
message === "\u0430 \u043c\u044b \u043a\u043e\u043c\u0443?" &&
|
||||
previousIntent === "receivables_confirmed_as_of_date"
|
||||
? "payables_confirmed_as_of_date"
|
||||
: null,
|
||||
resolveAddressIntent: () => ({
|
||||
intent: "open_items_by_counterparty_or_contract"
|
||||
})
|
||||
});
|
||||
|
||||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||||
"\u0430 \u043c\u044b \u043a\u043e\u043c\u0443?",
|
||||
[],
|
||||
"\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c, \u043a\u043e\u043c\u0443 \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0438\u0442 \u0442\u0435\u043a\u0443\u0449\u0430\u044f \u0437\u0430\u0434\u043e\u043b\u0436\u0435\u043d\u043d\u043e\u0441\u0442\u044c",
|
||||
{
|
||||
predecomposeContract: {
|
||||
intent: "open_items_by_counterparty_or_contract"
|
||||
}
|
||||
},
|
||||
null
|
||||
);
|
||||
|
||||
expect(carryover?.followupContext?.previous_intent).toBe("payables_confirmed_as_of_date");
|
||||
expect(carryover?.followupContext?.target_intent).toBe("payables_confirmed_as_of_date");
|
||||
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||||
organization: "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
|
||||
as_of_date: "2026-05-13"
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user