Укрепить семантику бизнес-ответов адресного контура
This commit is contained in:
@@ -255,6 +255,28 @@ describe("counterparty shipment item flow and open-items routing", () => {
|
||||
expect(reply.text).not.toContain("Контрагент: Группа. Найдено документов");
|
||||
});
|
||||
|
||||
it("keeps document follow-up answer compact for larger counterparty lists", () => {
|
||||
const rows = Array.from({ length: 7 }, (_, index) => ({
|
||||
period: `2021-11-${String(index + 1).padStart(2, "0")}T12:00:00Z`,
|
||||
registrator: `Документ ${index + 1}`,
|
||||
account_dt: "0",
|
||||
account_kt: "0",
|
||||
amount: 1000 + index,
|
||||
analytics: ["Группа СВК", "Договор № 1-ПМ/2020"],
|
||||
organization: "ООО Альтернатива Плюс"
|
||||
}));
|
||||
|
||||
const reply = composeFactualReply("list_documents_by_counterparty", rows, {
|
||||
counterpartyHint: "Группа СВК"
|
||||
});
|
||||
|
||||
expect(reply.text).toContain("Контрагент: Группа СВК. Найдено документов: 7.");
|
||||
expect(reply.text).toContain("Показаны первые 5 из 7 документов");
|
||||
expect(reply.text).toContain("Документ 5");
|
||||
expect(reply.text).not.toContain("Документ 6");
|
||||
expect(reply.text.split("\n").filter((line) => line.startsWith("Контрагент:")).length).toBe(1);
|
||||
});
|
||||
|
||||
it("keeps current resolved counterparty label over stale follow-up anchor during short retarget", async () => {
|
||||
executeAddressMcpQueryMock
|
||||
.mockResolvedValueOnce({
|
||||
@@ -427,6 +449,14 @@ describe("counterparty shipment item flow and open-items routing", () => {
|
||||
expect(result?.response_type).toBe("FACTUAL_LIST");
|
||||
expect(result?.debug.detected_intent).toBe("open_items_by_counterparty_or_contract");
|
||||
expect(String(result?.reply_text ?? "")).toContain("счету 60");
|
||||
expect(String(result?.reply_text ?? "").split("\n")[0]).toContain("Коротко:");
|
||||
expect(String(result?.reply_text ?? "").split("\n")[0]).toContain("точный открытый остаток");
|
||||
expect(String(result?.reply_text ?? "")).toContain("не подтвержден");
|
||||
expect(String(result?.reply_text ?? "")).toContain("предварительные сигналы");
|
||||
expect(result?.debug.address_coverage_evidence_v1?.requested_result_mode).toBe("confirmed_balance");
|
||||
expect(result?.debug.address_coverage_evidence_v1?.result_mode).toBe("heuristic_candidates");
|
||||
expect(result?.debug.address_coverage_evidence_v1?.coverage_status).toBe("partial");
|
||||
expect(result?.debug.address_coverage_evidence_v1?.balance_confirmed).toBe(false);
|
||||
|
||||
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
|
||||
expect(query).toContain("РегистрБухгалтерии.Хозрасчетный.ДвиженияССубконто");
|
||||
|
||||
@@ -14,6 +14,29 @@ describe("address filter extractor regressions", () => {
|
||||
expect(extracted.warnings).toContain("period_derived_from_month_phrase");
|
||||
expect(extracted.warnings).not.toContain("period_derived_from_tax_quarter_for_confirmed_vat_liability");
|
||||
});
|
||||
|
||||
it("keeps explicit year window for confirmed VAT tax-period intent", () => {
|
||||
const extracted = extractAddressFilters(
|
||||
"\u0447\u0442\u043e \u0441 \u043d\u0434\u0441 \u0437\u0430 2020 \u0433\u043e\u0434 \u043f\u043e \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
|
||||
"vat_liability_confirmed_for_tax_period"
|
||||
);
|
||||
|
||||
expect(extracted.extracted_filters.period_from).toBe("2020-01-01");
|
||||
expect(extracted.extracted_filters.period_to).toBe("2020-12-31");
|
||||
expect(extracted.warnings).toContain("period_derived_from_year_phrase");
|
||||
expect(extracted.warnings).not.toContain("period_derived_from_tax_quarter_for_confirmed_vat_liability");
|
||||
});
|
||||
|
||||
it("drops pronoun-only counterparty anchors for chain follow-ups", () => {
|
||||
const extracted = extractAddressFilters(
|
||||
"\u043f\u043e\u043a\u0430\u0436\u0438 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b \u043f\u043e \u044d\u0442\u043e\u0439 \u0446\u0435\u043f\u043e\u0447\u043a\u0435",
|
||||
"list_documents_by_counterparty"
|
||||
);
|
||||
|
||||
expect(extracted.extracted_filters.counterparty).toBeUndefined();
|
||||
expect(extracted.warnings).toContain("counterparty_anchor_dropped_low_quality");
|
||||
});
|
||||
|
||||
it("extracts a compact counterparty tail for customer revenue profile", () => {
|
||||
const extracted = extractAddressFilters(
|
||||
"\u043a\u0430\u043a\u043e\u0439 \u043e\u0431\u043e\u0440\u043e\u0442 \u0431\u044b\u043b \u0441\u0432\u043a",
|
||||
|
||||
@@ -291,4 +291,27 @@ describe("address follow-up temporal regressions", () => {
|
||||
expect(movements?.filters.extracted_filters.counterparty).toBe("Группа СВК");
|
||||
expect(movements?.baseReasons).toContain("counterparty_from_followup_context");
|
||||
});
|
||||
|
||||
it("replaces pronoun chain anchors from counterparty follow-up context", () => {
|
||||
const followupContext = {
|
||||
previous_intent: "customer_revenue_and_payments" as const,
|
||||
target_intent: "list_documents_by_counterparty" as const,
|
||||
previous_filters: {
|
||||
organization: "ООО Альтернатива Плюс",
|
||||
counterparty: "Группа СВК"
|
||||
},
|
||||
previous_anchor_type: "counterparty" as const,
|
||||
previous_anchor_value: "Группа СВК",
|
||||
resolved_counterparty_from_display: true
|
||||
};
|
||||
|
||||
const documents = runAddressDecomposeStage(
|
||||
"покажи документы по этой цепочке и не смешивай Группа СВК с организацией ООО Альтернатива Плюс",
|
||||
followupContext
|
||||
);
|
||||
|
||||
expect(documents?.intent.intent).toBe("list_documents_by_counterparty");
|
||||
expect(documents?.filters.extracted_filters.counterparty).toBe("Группа СВК");
|
||||
expect(documents?.baseReasons).toContain("counterparty_from_followup_context");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -67,6 +67,17 @@ describe("address route expectations contract", () => {
|
||||
expect(audit.reason).toBe("route_expectation_matched");
|
||||
});
|
||||
|
||||
it("matches open-items route as a supported factual route", () => {
|
||||
const audit = evaluateAddressRouteExpectation({
|
||||
intent: "open_items_by_counterparty_or_contract",
|
||||
selectedRecipe: "address_open_items_by_party_or_contract_v1",
|
||||
requestedResultMode: "confirmed_balance",
|
||||
resultMode: "confirmed_balance"
|
||||
});
|
||||
expect(audit.status).toBe("matched");
|
||||
expect(audit.reason).toBe("route_expectation_matched");
|
||||
});
|
||||
|
||||
it("detects selected recipe mismatch", () => {
|
||||
const audit = evaluateAddressRouteExpectation({
|
||||
intent: "payables_confirmed_as_of_date",
|
||||
|
||||
@@ -25,6 +25,32 @@ describe("address truth gate policy", () => {
|
||||
expect(gate.reason_codes).toContain("limited_category_empty_match");
|
||||
});
|
||||
|
||||
it("keeps open-items movement fallback partial even when heuristic rows are found", () => {
|
||||
const gate = resolveAddressTruthGate({
|
||||
intent: "open_items_by_counterparty_or_contract",
|
||||
filters: {
|
||||
account: "60",
|
||||
period_from: "2020-08-01",
|
||||
period_to: "2020-08-31",
|
||||
organization: "ООО Альтернатива Плюс"
|
||||
},
|
||||
selectedRecipe: "address_open_items_by_party_or_contract_v1",
|
||||
rowsMatched: 8,
|
||||
runtimeReadiness: "LIVE_QUERYABLE_WITH_LIMITS",
|
||||
reasons: [
|
||||
"confirmed_balance_unavailable_fallback_to_heuristic_candidates",
|
||||
"open_items_account_query_override_to_movements"
|
||||
],
|
||||
routeExpectationStatus: "matched",
|
||||
replyType: "factual"
|
||||
});
|
||||
|
||||
expect(gate.truth_gate_status).toBe("partial_supported");
|
||||
expect(gate.carryover_eligibility).toBe("root_only");
|
||||
expect(gate.blocked_or_limited_explanation).toBe("evidence_or_coverage_is_partial");
|
||||
expect(gate.reason_codes).toContain("confirmed_balance_unavailable_fallback_to_heuristic_candidates");
|
||||
});
|
||||
|
||||
it("keeps selected-item limited answers object-scoped", () => {
|
||||
const gate = resolveAddressTruthGate({
|
||||
intent: "inventory_sale_trace_for_item",
|
||||
|
||||
@@ -468,6 +468,26 @@ describe("assistant living chat runtime adapter", () => {
|
||||
expect(executeLlmChat).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("builds honest memory checkpoint reply when there is no selected context", async () => {
|
||||
const executeLlmChat = vi.fn(async () => "raw-llm");
|
||||
const input = buildRuntimeInput({
|
||||
userMessage:
|
||||
"Сделай короткий стартовый чек контекста: есть ли уже выбранная компания или контрагент в текущем диалоге; если нет, скажи честно и не выдумывай память про Группа СВК.",
|
||||
modeDecision: { mode: "chat", reason: "memory_recap_followup_detected" },
|
||||
sessionItems: [],
|
||||
executeLlmChat
|
||||
});
|
||||
|
||||
const output = await runAssistantLivingChatRuntime(input);
|
||||
|
||||
expect(output.handled).toBe(true);
|
||||
expect(output.chatText).toContain("не вижу выбранной компании");
|
||||
expect(output.chatText).toContain("Группа СВК");
|
||||
expect(output.chatText).toContain("не подтверждена");
|
||||
expect(output.debug?.living_chat_response_source).toBe("deterministic_memory_recap_contract");
|
||||
expect(executeLlmChat).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("builds deterministic memory recap for prior grounded MCP discovery counterparty context", async () => {
|
||||
const executeLlmChat = vi.fn(async () => "raw-llm");
|
||||
const input = buildRuntimeInput({
|
||||
|
||||
@@ -1007,6 +1007,39 @@ describe("assistant orchestration contract", () => {
|
||||
expect(decision.livingReason).toBe("memory_recap_followup_detected");
|
||||
});
|
||||
|
||||
it("routes startup memory checkpoint without selected context to deterministic chat", () => {
|
||||
const question =
|
||||
"Сделай короткий стартовый чек контекста: есть ли уже выбранная компания или контрагент в текущем диалоге; если нет, скажи честно и не выдумывай память про Группа СВК.";
|
||||
const decision = resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: question,
|
||||
effectiveAddressUserMessage: question,
|
||||
followupContext: null,
|
||||
llmPreDecomposeMeta: {
|
||||
applied: true,
|
||||
llmCanonicalCandidateDetected: true,
|
||||
predecomposeContract: {
|
||||
mode: "unsupported",
|
||||
mode_confidence: "low",
|
||||
intent: "customer_revenue_and_payments",
|
||||
intent_confidence: "high"
|
||||
},
|
||||
semanticExtractionContract: {
|
||||
valid: true,
|
||||
apply_canonical_recommended: true,
|
||||
reason_codes: ["unsupported_low_confidence_contract"]
|
||||
}
|
||||
} as any,
|
||||
sessionItems: [],
|
||||
useMock: false
|
||||
} as any);
|
||||
|
||||
expect(decision.runAddressLane).toBe(false);
|
||||
expect(decision.toolGateDecision).toBe("skip_address_lane");
|
||||
expect(decision.toolGateReason).toBe("memory_recap_followup_detected");
|
||||
expect(decision.livingMode).toBe("chat");
|
||||
expect(decision.livingReason).toBe("memory_recap_followup_detected");
|
||||
});
|
||||
|
||||
it("keeps documentary inventory chain verification in address lane for supported exact intent", () => {
|
||||
const question =
|
||||
"Есть ли документально подтвержденная цепочка: поставщик Гамма-мебель, ООО -> товар Шкаф картотечный 1000*400*2100 -> покупатель Департамент капитального ремонта города Москвы";
|
||||
|
||||
@@ -32,6 +32,34 @@ describe("assistant MCP discovery data need graph", () => {
|
||||
expect(result.forbidden_overclaim_flags).toContain("no_unchecked_fact_totals");
|
||||
});
|
||||
|
||||
it("defaults explicit-counterparty bidirectional value-flow without period to bounded all-time scope", () => {
|
||||
const result = buildAssistantMcpDiscoveryDataNeedGraph({
|
||||
semanticDataNeed: "counterparty value-flow evidence",
|
||||
rawUtterance: "how much money passed with SVK, incoming and outgoing?",
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "net_value_flow",
|
||||
explicit_entity_candidates: ["SVK"]
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.business_fact_family).toBe("value_flow");
|
||||
expect(result.comparison_need).toBe("incoming_vs_outgoing");
|
||||
expect(result.time_scope_need).toBe("all_time_scope");
|
||||
expect(result.clarification_gaps).toEqual([]);
|
||||
expect(result.proof_expectation).toBe("coverage_checked_fact");
|
||||
expect(result.decomposition_candidates).toEqual([
|
||||
"resolve_entity_reference",
|
||||
"collect_incoming_movements",
|
||||
"collect_outgoing_movements",
|
||||
"aggregate_checked_amounts",
|
||||
"probe_coverage"
|
||||
]);
|
||||
expect(result.reason_codes).toContain(
|
||||
"data_need_graph_subject_bidirectional_value_flow_defaults_to_all_time_scope"
|
||||
);
|
||||
});
|
||||
|
||||
it("marks metadata lane choice as a clarification-required graph", () => {
|
||||
const result = buildAssistantMcpDiscoveryDataNeedGraph({
|
||||
semanticDataNeed: "metadata lane clarification",
|
||||
|
||||
@@ -1011,6 +1011,45 @@ describe("assistant MCP discovery planner", () => {
|
||||
expect(result.reason_codes).toContain("planner_instantiated_catalog_chain_template_value_flow_comparison");
|
||||
});
|
||||
|
||||
it("keeps explicit-counterparty bidirectional comparison executable over bounded all-time scope", () => {
|
||||
const result = planAssistantMcpDiscovery({
|
||||
dataNeedGraph: {
|
||||
schema_version: "assistant_data_need_graph_v1",
|
||||
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||
subject_candidates: ["SVK"],
|
||||
business_fact_family: "value_flow",
|
||||
action_family: "net_value_flow",
|
||||
aggregation_need: null,
|
||||
time_scope_need: "all_time_scope",
|
||||
comparison_need: "incoming_vs_outgoing",
|
||||
ranking_need: null,
|
||||
proof_expectation: "coverage_checked_fact",
|
||||
clarification_gaps: [],
|
||||
decomposition_candidates: ["resolve_entity_reference", "collect_incoming_movements", "collect_outgoing_movements", "probe_coverage"],
|
||||
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
|
||||
reason_codes: [
|
||||
"data_need_graph_built",
|
||||
"data_need_graph_comparison_incoming_vs_outgoing",
|
||||
"data_need_graph_subject_bidirectional_value_flow_defaults_to_all_time_scope"
|
||||
]
|
||||
},
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "net_value_flow",
|
||||
explicit_entity_candidates: ["SVK"],
|
||||
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting"
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.planner_status).toBe("ready_for_execution");
|
||||
expect(result.selected_chain_id).toBe("value_flow_comparison");
|
||||
expect(result.proposed_primitives).toEqual(["resolve_entity_reference", "query_movements", "probe_coverage"]);
|
||||
expect(result.required_axes).toEqual(["counterparty", "all_time_scope", "amount", "coverage_target"]);
|
||||
expect(result.catalog_review.review_status).toBe("catalog_compatible");
|
||||
expect(result.discovery_plan.clarification_gaps).toEqual([]);
|
||||
expect(result.reason_codes).toContain("planner_ready_for_guarded_mcp_execution");
|
||||
});
|
||||
|
||||
it("builds an inference-safe lifecycle plan with evidence explanation", () => {
|
||||
const result = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
|
||||
@@ -114,9 +114,11 @@ describe("assistant MCP discovery response candidate", () => {
|
||||
})
|
||||
);
|
||||
|
||||
expect(candidate.reply_text).toContain("самый доходный год");
|
||||
expect(candidate.reply_text).toContain("в доступном проверенном MCP-срезе");
|
||||
expect(candidate.reply_text).toContain("лидирует 2015");
|
||||
expect(candidate.reply_text).toContain("2015");
|
||||
expect(candidate.reply_text).toContain("136 723 459,73 руб.");
|
||||
expect(candidate.reply_text).toContain("не полный бухгалтерский рейтинг доходности");
|
||||
expect(candidate.reply_text).toContain("не как чистую бухгалтерскую прибыль");
|
||||
expect(candidate.reply_text).toContain("лимит выборки MCP");
|
||||
expect(candidate.reply_text).not.toContain("Что подтверждено:");
|
||||
@@ -162,6 +164,12 @@ describe("assistant MCP discovery response candidate", () => {
|
||||
total_amount_human_ru: "11 536 836,23 руб."
|
||||
}
|
||||
],
|
||||
top_suppliers: [
|
||||
{
|
||||
axis_value: "ООО Поставщик",
|
||||
total_amount_human_ru: "2 200 000 руб."
|
||||
}
|
||||
],
|
||||
yearly_breakdown: []
|
||||
}
|
||||
},
|
||||
@@ -181,12 +189,219 @@ describe("assistant MCP discovery response candidate", () => {
|
||||
expect(candidate.reply_text).toContain("за 2017");
|
||||
expect(candidate.reply_text).toContain("получили 16 932 063,96 руб.");
|
||||
expect(candidate.reply_text).toContain("исходящие платежи/списания 4 458 027,05 руб.");
|
||||
expect(candidate.reply_text).toContain("12 474 036,91 руб.");
|
||||
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("денежный operating-flow proxy");
|
||||
expect(candidate.reply_text).not.toContain("Что можно сказать только как вывод:");
|
||||
expect(candidate.reply_text).not.toContain("Складской срез");
|
||||
});
|
||||
|
||||
it("mentions separate counterparty scope in company plus counterparty business summaries", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
turn_input: {
|
||||
adapter_status: "ready",
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "broad_evaluation",
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||
business_overview_separate_entity_candidates: ["Группа СВК"]
|
||||
},
|
||||
data_need_graph: {
|
||||
business_fact_family: "business_overview",
|
||||
subject_candidates: [],
|
||||
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: {
|
||||
period_scope: null,
|
||||
incoming_customer_revenue: {
|
||||
total_amount_human_ru: "157 192 981,43 руб.",
|
||||
coverage_limited_by_probe_limit: true
|
||||
},
|
||||
outgoing_supplier_payout: {
|
||||
total_amount_human_ru: "35 439 044,74 руб.",
|
||||
coverage_limited_by_probe_limit: true
|
||||
},
|
||||
net_amount_human_ru: "121 753 936,69 руб.",
|
||||
net_direction: "net_incoming",
|
||||
top_customers: [],
|
||||
yearly_breakdown: []
|
||||
}
|
||||
},
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference",
|
||||
headline: "Company summary.",
|
||||
confirmed_lines: [],
|
||||
inference_lines: [],
|
||||
unknown_lines: [],
|
||||
limitation_lines: [],
|
||||
next_step_line: null
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
expect(candidate.reply_text).toContain("по компании ООО Альтернатива Плюс");
|
||||
expect(candidate.reply_text).toContain("Группа СВК");
|
||||
expect(candidate.reply_text?.split("\n")[0]).toContain("суммы компании не переношу");
|
||||
expect(candidate.reply_text).toContain("нельзя делать вывод о выручке, долге или прибыльности");
|
||||
expect(candidate.reply_text).toContain("без отдельного контрагентского среза");
|
||||
});
|
||||
|
||||
it("adds missing proof boundaries for broad all-time business overviews", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
turn_input: {
|
||||
adapter_status: "ready",
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "broad_evaluation",
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс"
|
||||
},
|
||||
data_need_graph: {
|
||||
business_fact_family: "business_overview",
|
||||
subject_candidates: [],
|
||||
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: {
|
||||
period_scope: null,
|
||||
incoming_customer_revenue: {
|
||||
total_amount_human_ru: "157 192 981,43 руб.",
|
||||
coverage_limited_by_probe_limit: true
|
||||
},
|
||||
outgoing_supplier_payout: {
|
||||
total_amount_human_ru: "35 439 044,74 руб.",
|
||||
coverage_limited_by_probe_limit: true
|
||||
},
|
||||
net_amount_human_ru: "121 753 936,69 руб.",
|
||||
net_direction: "net_incoming",
|
||||
top_customers: [],
|
||||
top_suppliers: [],
|
||||
yearly_breakdown: []
|
||||
}
|
||||
},
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference",
|
||||
headline: "Company summary.",
|
||||
confirmed_lines: [],
|
||||
inference_lines: [],
|
||||
unknown_lines: [],
|
||||
limitation_lines: [],
|
||||
next_step_line: null
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
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("capability_id");
|
||||
});
|
||||
|
||||
it("reuses previous counterparty value-flow bundle in company plus counterparty summaries", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
turn_input: {
|
||||
adapter_status: "ready",
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "broad_evaluation",
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||
business_overview_separate_entity_candidates: ["Группа СВК"],
|
||||
previous_counterparty_value_flow_bundle: {
|
||||
counterparty: "Группа СВК",
|
||||
incoming_customer_revenue: {
|
||||
total_amount_human_ru: "20 653 490 руб.",
|
||||
rows_with_amount: 26,
|
||||
rows_matched: 26,
|
||||
first_movement_date: "2020-07-27",
|
||||
latest_movement_date: "2021-11-10"
|
||||
},
|
||||
outgoing_supplier_payout: {
|
||||
total_amount_human_ru: "2 129 651 руб.",
|
||||
rows_with_amount: 1,
|
||||
rows_matched: 1,
|
||||
first_movement_date: "2022-01-20",
|
||||
latest_movement_date: "2022-01-20"
|
||||
},
|
||||
net_amount_human_ru: "18 523 839 руб.",
|
||||
net_direction: "net_incoming"
|
||||
},
|
||||
previous_counterparty_document_bundle: {
|
||||
counterparty: "Группа СВК",
|
||||
document_count: 19
|
||||
}
|
||||
},
|
||||
data_need_graph: {
|
||||
business_fact_family: "business_overview",
|
||||
subject_candidates: [],
|
||||
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: {
|
||||
period_scope: null,
|
||||
incoming_customer_revenue: { total_amount_human_ru: "157 192 981,43 руб." },
|
||||
outgoing_supplier_payout: { total_amount_human_ru: "35 439 044,74 руб." },
|
||||
net_amount_human_ru: "121 753 936,69 руб.",
|
||||
net_direction: "net_incoming",
|
||||
top_customers: [],
|
||||
yearly_breakdown: []
|
||||
}
|
||||
},
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference",
|
||||
headline: "Company summary.",
|
||||
confirmed_lines: [],
|
||||
inference_lines: [],
|
||||
unknown_lines: [],
|
||||
limitation_lines: [],
|
||||
next_step_line: null
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const firstLine = candidate.reply_text?.split("\n")[0] ?? "";
|
||||
expect(firstLine).toContain("отдельно по Группа СВК: получили 20 653 490 руб.");
|
||||
expect(firstLine).toContain("можно утверждать только эти подтвержденные срезы");
|
||||
expect(firstLine).toContain("нельзя называть это чистой прибылью");
|
||||
expect(candidate.reply_text).toContain("Отдельно по контрагенту Группа СВК: подтверждено получили 20 653 490 руб.");
|
||||
expect(candidate.reply_text).toContain("заплатили 2 129 651 руб.");
|
||||
expect(candidate.reply_text).toContain("Можно утверждать:");
|
||||
expect(candidate.reply_text).toContain("Нельзя утверждать:");
|
||||
expect(candidate.reply_text).toContain("документы по цепочке: найдено 19");
|
||||
expect(candidate.reply_text).toContain("ранее подтвержденный контрагентский срез");
|
||||
});
|
||||
|
||||
it("localizes value-flow evidence without leaking pilot mechanics", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
@@ -294,6 +509,63 @@ describe("assistant MCP discovery response candidate", () => {
|
||||
expect(candidate.reply_text).not.toContain("query_movements");
|
||||
});
|
||||
|
||||
it("uses a compact direct first line for derived bidirectional value-flow totals", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
user_facing_response_allowed: true,
|
||||
business_fact_answer_allowed: true,
|
||||
requires_user_clarification: false,
|
||||
pilot: {
|
||||
pilot_scope: "counterparty_bidirectional_value_flow_query_movements_v1",
|
||||
derived_bidirectional_value_flow: {
|
||||
counterparty: "SVK",
|
||||
period_scope: "2020",
|
||||
net_amount_human_ru: "8 500,50 руб.",
|
||||
net_direction: "net_incoming",
|
||||
coverage_limited_by_probe_limit: false,
|
||||
incoming_customer_revenue: {
|
||||
rows_matched: 2,
|
||||
rows_with_amount: 2,
|
||||
total_amount_human_ru: "12 500,50 руб.",
|
||||
first_movement_date: "2020-01-15",
|
||||
latest_movement_date: "2020-02-20"
|
||||
},
|
||||
outgoing_supplier_payout: {
|
||||
rows_matched: 1,
|
||||
rows_with_amount: 1,
|
||||
total_amount_human_ru: "4 000 руб.",
|
||||
first_movement_date: "2020-03-10",
|
||||
latest_movement_date: "2020-03-10"
|
||||
}
|
||||
}
|
||||
},
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference",
|
||||
headline: "По данным 1С найдены строки входящих и исходящих денежных движений.",
|
||||
confirmed_lines: ["1C bidirectional value-flow rows were checked for counterparty SVK: incoming=found, outgoing=found"],
|
||||
inference_lines: ["Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows"],
|
||||
unknown_lines: [],
|
||||
limitation_lines: [],
|
||||
next_step_line: null
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const firstLine = candidate.reply_text?.split("\n")[0] ?? "";
|
||||
expect(firstLine).toContain("Коротко:");
|
||||
expect(firstLine).toContain("SVK");
|
||||
expect(firstLine).toContain("получили 12 500,50 руб.");
|
||||
expect(firstLine).toContain("заплатили 4 000 руб.");
|
||||
expect(firstLine).toContain("нетто в нашу сторону: 8 500,50 руб.");
|
||||
expect(candidate.reply_text).toContain("Основа:");
|
||||
expect(candidate.reply_text).not.toContain("Что подтверждено");
|
||||
expect(candidate.reply_text).not.toContain("pilot_");
|
||||
expect(candidate.reply_text).not.toContain("query_movements");
|
||||
});
|
||||
|
||||
it("keeps monthly breakdown lines user-facing and localizes monthly inference basis", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
|
||||
@@ -135,6 +135,69 @@ describe("assistant MCP discovery response policy", () => {
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_not_discovery_ready_address_candidate");
|
||||
});
|
||||
|
||||
it("lets a grounded business overview candidate override a semantically wrong exact address recipe", () => {
|
||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||
currentReply: "Supplier and stock overlap was confirmed for 2020.",
|
||||
currentReplySource: "address_query_runtime_v1",
|
||||
currentReplyType: "factual",
|
||||
addressRuntimeMeta: {
|
||||
detected_intent: "inventory_supplier_stock_overlap_as_of_date",
|
||||
selected_recipe: "address_inventory_supplier_stock_overlap_as_of_date_v1",
|
||||
mcp_call_status: "matched_non_empty",
|
||||
truth_mode: "confirmed",
|
||||
capability_binding_status: "bound",
|
||||
capability_binding_violations: [],
|
||||
answer_shape_contract: {
|
||||
reply_type: "factual",
|
||||
capability_contract_id: "inventory_inventory_supplier_stock_overlap_as_of_date"
|
||||
},
|
||||
assistant_mcp_discovery_entry_point_v1: entryPoint({
|
||||
turn_input: {
|
||||
adapter_status: "ready",
|
||||
should_run_discovery: true,
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "business_summary",
|
||||
asked_action_family: "broad_evaluation",
|
||||
unsupported_but_understood_family: "broad_business_evaluation",
|
||||
explicit_organization_scope: "OOO Alternative Plus",
|
||||
explicit_date_scope: "2020"
|
||||
},
|
||||
data_need_graph: {
|
||||
business_fact_family: "business_overview",
|
||||
clarification_gaps: []
|
||||
}
|
||||
},
|
||||
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: "Business overview was assembled from confirmed 1C evidence.",
|
||||
confirmed_lines: [
|
||||
"Incoming customer money flow: 200000.00 RUB.",
|
||||
"Outgoing supplier payouts: 150000.00 RUB."
|
||||
],
|
||||
inference_lines: ["Net confirmed cash-flow spread is +50000.00 RUB; this is not profit."],
|
||||
unknown_lines: ["Profit and formal margin are not confirmed by this overview."],
|
||||
limitation_lines: ["The overview is limited to checked 1C rows."],
|
||||
next_step_line: "Check profit, VAT, debt quality, and inventory liquidity next."
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.applied).toBe(true);
|
||||
expect(result.decision).toBe("apply_candidate");
|
||||
expect(result.reply_source).toBe("mcp_discovery_response_candidate_guarded");
|
||||
expect(result.reply_text).toContain("Business overview");
|
||||
expect(result.reply_text).toContain("Incoming customer money flow");
|
||||
expect(result.reason_codes).toContain("mcp_discovery_response_policy_semantic_conflict_allows_candidate_override");
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_exact_matched_factual_address_reply");
|
||||
});
|
||||
|
||||
it("overrides exact inbound value-flow replies when the discovery turn meaning asks for payouts", () => {
|
||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||
currentReply: "Incoming turnover by SVK: 12 224 925.00 rub.",
|
||||
@@ -744,6 +807,65 @@ describe("assistant MCP discovery response policy", () => {
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_semantic_conflict_allows_candidate_override");
|
||||
});
|
||||
|
||||
it("keeps runtime-matched exact VAT replies over a stale business overview discovery seed", () => {
|
||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||
currentReply: "Short: confirmed VAT for 2020 is based on checked VAT rows.",
|
||||
currentReplySource: "address_query_runtime_v1",
|
||||
currentReplyType: "factual",
|
||||
addressRuntimeMeta: {
|
||||
detected_intent: "vat_liability_confirmed_for_tax_period",
|
||||
selected_recipe: "address_vat_liability_confirmed_tax_period_v1",
|
||||
mcp_call_status: "matched_non_empty",
|
||||
truth_mode: "confirmed",
|
||||
capability_binding_status: "bound",
|
||||
capability_binding_violations: [],
|
||||
truth_gate_contract_status: "full_confirmed",
|
||||
assistant_truth_answer_policy_v1: {
|
||||
truth_gate: {
|
||||
coverage_status: "full",
|
||||
grounding_status: "grounded",
|
||||
source_truth_gate_status: "full_confirmed"
|
||||
},
|
||||
answer_shape: {
|
||||
reply_type: "factual",
|
||||
capability_contract_id: "confirmed_vat_liability_for_tax_period"
|
||||
}
|
||||
},
|
||||
assistant_state_transition_v1: {
|
||||
reason_codes: [
|
||||
"root_followup_continue_previous",
|
||||
"route_expectation_matched",
|
||||
"vat_period_inspection_bridge_signal_detected",
|
||||
"confirmed_balance_exact_vat_tax_period_intent"
|
||||
]
|
||||
},
|
||||
assistant_mcp_discovery_entry_point_v1: entryPoint({
|
||||
turn_input: {
|
||||
adapter_status: "ready",
|
||||
should_run_discovery: true,
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "broad_evaluation",
|
||||
unsupported_but_understood_family: "broad_business_evaluation"
|
||||
},
|
||||
data_need_graph: {
|
||||
business_fact_family: "business_overview",
|
||||
clarification_gaps: []
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.applied).toBe(false);
|
||||
expect(result.decision).toBe("keep_current_reply");
|
||||
expect(result.reply_text).toContain("confirmed VAT");
|
||||
expect(result.reason_codes).toContain(
|
||||
"mcp_discovery_response_policy_keep_runtime_matched_exact_reply_over_stale_discovery_turn_meaning"
|
||||
);
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_semantic_conflict_allows_candidate_override");
|
||||
});
|
||||
|
||||
it("keeps address lane answers when discovery was not requested for the current turn", () => {
|
||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||
currentReply: "supported exact route answer",
|
||||
|
||||
@@ -279,6 +279,66 @@ describe("assistant MCP discovery runtime bridge", () => {
|
||||
expect(result.answer_draft.confirmed_lines.join("\n")).toContain("заплатили");
|
||||
});
|
||||
|
||||
it("executes explicit-counterparty bidirectional comparison without period as bounded all-time scope", async () => {
|
||||
const result = await runAssistantMcpDiscoveryRuntimeBridge({
|
||||
dataNeedGraph: {
|
||||
schema_version: "assistant_data_need_graph_v1",
|
||||
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
|
||||
subject_candidates: ["SVK"],
|
||||
business_fact_family: "value_flow",
|
||||
action_family: "net_value_flow",
|
||||
aggregation_need: null,
|
||||
time_scope_need: "all_time_scope",
|
||||
comparison_need: "incoming_vs_outgoing",
|
||||
ranking_need: null,
|
||||
proof_expectation: "coverage_checked_fact",
|
||||
clarification_gaps: [],
|
||||
decomposition_candidates: [
|
||||
"resolve_entity_reference",
|
||||
"collect_incoming_movements",
|
||||
"collect_outgoing_movements",
|
||||
"probe_coverage"
|
||||
],
|
||||
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
|
||||
reason_codes: [
|
||||
"data_need_graph_built",
|
||||
"data_need_graph_comparison_incoming_vs_outgoing",
|
||||
"data_need_graph_subject_bidirectional_value_flow_defaults_to_all_time_scope"
|
||||
]
|
||||
},
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "net_value_flow",
|
||||
explicit_entity_candidates: ["SVK"],
|
||||
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting"
|
||||
},
|
||||
deps: buildBidirectionalDeps(
|
||||
[
|
||||
{ Period: "2020-01-10T00:00:00", Amount: 3200, Counterparty: "SVK" },
|
||||
{ Period: "2021-04-11T00:00:00", Amount: 1800, Counterparty: "SVK" }
|
||||
],
|
||||
[{ Period: "2022-02-12T00:00:00", Amount: 1400, Counterparty: "SVK" }]
|
||||
)
|
||||
});
|
||||
|
||||
expect(result.bridge_status).toBe("answer_draft_ready");
|
||||
expect(result.requires_user_clarification).toBe(false);
|
||||
expect(result.business_fact_answer_allowed).toBe(true);
|
||||
expect(result.planner.selected_chain_id).toBe("value_flow_comparison");
|
||||
expect(result.planner.required_axes).toContain("all_time_scope");
|
||||
expect(result.pilot.mcp_execution_performed).toBe(true);
|
||||
expect(result.pilot.derived_bidirectional_value_flow).toMatchObject({
|
||||
period_scope: null,
|
||||
incoming_customer_revenue: {
|
||||
total_amount: 5000
|
||||
},
|
||||
outgoing_supplier_payout: {
|
||||
total_amount: 1400
|
||||
}
|
||||
});
|
||||
expect(result.answer_draft.confirmed_lines.join("\n")).toContain("SVK");
|
||||
});
|
||||
|
||||
it("keeps document-ready plans bounded when the pilot finds no confirmed rows", async () => {
|
||||
const result = await runAssistantMcpDiscoveryRuntimeBridge({
|
||||
turnMeaning: {
|
||||
|
||||
@@ -133,6 +133,39 @@ describe("assistant MCP discovery turn input adapter", () => {
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_payout_signal_detected");
|
||||
});
|
||||
|
||||
it("keeps explicit counterparty money-flow basis questions executable without requiring a period", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage: "How much money passed with SVK, incoming and outgoing, and what documents or movements prove it?",
|
||||
assistantTurnMeaning: {
|
||||
asked_domain_family: "counterparty",
|
||||
asked_action_family: "list_documents",
|
||||
explicit_intent_candidate: "list_documents_by_counterparty",
|
||||
explicit_entity_candidates: [{ value: "SVK" }]
|
||||
},
|
||||
predecomposeContract: {
|
||||
entities: { counterparty: "SVK" },
|
||||
period: { scope: "unspecified", period_from: null, period_to: null }
|
||||
}
|
||||
});
|
||||
|
||||
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: ["SVK"],
|
||||
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting",
|
||||
stale_replay_forbidden: true
|
||||
});
|
||||
expect(result.turn_meaning_ref?.explicit_date_scope).toBeUndefined();
|
||||
expect(result.data_need_graph?.time_scope_need).toBe("all_time_scope");
|
||||
expect(result.data_need_graph?.clarification_gaps).toEqual([]);
|
||||
expect(result.data_need_graph?.reason_codes).toContain(
|
||||
"data_need_graph_subject_bidirectional_value_flow_defaults_to_all_time_scope"
|
||||
);
|
||||
expect(result.reason_codes).toContain("mcp_discovery_bidirectional_value_flow_signal_detected");
|
||||
});
|
||||
|
||||
it("extracts compact scoped counterparty from net follow-up wording when LLM entities are empty", () => {
|
||||
const orgName = "ООО Альтернатива Плюс";
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
@@ -938,6 +971,39 @@ describe("assistant MCP discovery turn input adapter", () => {
|
||||
expect(result.reason_codes).toContain("mcp_discovery_date_scope_from_followup_context");
|
||||
});
|
||||
|
||||
it("does not leak implicit current date into document follow-up after all-time bidirectional value-flow", () => {
|
||||
const orgName = "ООО Альтернатива Плюс";
|
||||
const counterpartyName = "Группа СВК";
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage: "а покажи документы по этой цепочке",
|
||||
followupContext: {
|
||||
previous_discovery_pilot_scope: "counterparty_bidirectional_value_flow_query_movements_v1",
|
||||
previous_filters: {
|
||||
counterparty: counterpartyName,
|
||||
organization: orgName,
|
||||
as_of_date: today
|
||||
},
|
||||
previous_anchor_type: "counterparty",
|
||||
previous_anchor_value: counterpartyName
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.adapter_status).toBe("ready");
|
||||
expect(result.should_run_discovery).toBe(true);
|
||||
expect(result.turn_meaning_ref).toMatchObject({
|
||||
asked_domain_family: "documents",
|
||||
asked_action_family: "list_documents",
|
||||
explicit_entity_candidates: [counterpartyName],
|
||||
explicit_organization_scope: orgName,
|
||||
unsupported_but_understood_family: "document_evidence",
|
||||
stale_replay_forbidden: true
|
||||
});
|
||||
expect(result.turn_meaning_ref?.explicit_date_scope).toBeUndefined();
|
||||
expect(result.reason_codes).toContain("mcp_discovery_value_flow_grounded_document_followup");
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_date_scope_from_followup_context");
|
||||
});
|
||||
|
||||
it("seeds short metadata follow-up from prior metadata discovery context", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage: "а по регистрам?",
|
||||
@@ -1531,6 +1597,40 @@ describe("assistant MCP discovery turn input adapter", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps a raw business-overview year over a predecompose as-of date derived from that year", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage:
|
||||
"\u0414\u0430\u0439 \u0432\u0437\u0440\u043e\u0441\u043b\u044b\u0439 \u0431\u0438\u0437\u043d\u0435\u0441-\u043e\u0431\u0437\u043e\u0440 \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441 \u0437\u0430 2020 \u0433\u043e\u0434: \u0434\u0435\u043d\u044c\u0433\u0438, \u041d\u0414\u0421, \u0434\u043e\u043b\u0433\u0438, \u0441\u043a\u043b\u0430\u0434.",
|
||||
assistantTurnMeaning: {
|
||||
asked_domain_family: "business_summary",
|
||||
asked_action_family: "broad_evaluation",
|
||||
unsupported_but_understood_family: "broad_business_evaluation",
|
||||
stale_replay_forbidden: true
|
||||
},
|
||||
predecomposeContract: {
|
||||
entities: {
|
||||
organization: "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"
|
||||
},
|
||||
period: {
|
||||
scope: "as_of",
|
||||
period_from: "2020-01-01",
|
||||
period_to: "2020-12-31",
|
||||
as_of_date: "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).toMatchObject({
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "broad_evaluation",
|
||||
explicit_date_scope: "2020"
|
||||
});
|
||||
expect(result.reason_codes).toContain("mcp_discovery_business_overview_raw_year_overrode_predecompose_as_of_scope");
|
||||
});
|
||||
|
||||
it("keeps all-time business overview from reusing a negated VAT period as active scope", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage:
|
||||
@@ -2937,7 +3037,7 @@ describe("assistant MCP discovery turn input adapter", () => {
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_counterparty_from_followup_context");
|
||||
});
|
||||
|
||||
it("keeps VAT-position follow-up inside business overview instead of stale inventory position", () => {
|
||||
it("lets an explicit VAT follow-up stay on the exact VAT route instead of stale business overview", () => {
|
||||
const orgName =
|
||||
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
@@ -2957,21 +3057,16 @@ describe("assistant MCP discovery turn input adapter", () => {
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.adapter_status).toBe("ready");
|
||||
expect(result.should_run_discovery).toBe(true);
|
||||
expect(result.semantic_data_need).toBe("business overview evidence with bounded analyst interpretation");
|
||||
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
|
||||
expect(result.data_need_graph?.subject_candidates).toEqual([]);
|
||||
expect(result.turn_meaning_ref).toMatchObject({
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "broad_evaluation",
|
||||
explicit_organization_scope: orgName,
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "broad_business_evaluation",
|
||||
stale_replay_forbidden: true
|
||||
});
|
||||
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
|
||||
expect(result.reason_codes).toContain("mcp_discovery_business_overview_continuation_from_followup_context");
|
||||
expect(result.adapter_status).toBe("not_applicable");
|
||||
expect(result.should_run_discovery).toBe(false);
|
||||
expect(result.semantic_data_need).toBeNull();
|
||||
expect(result.data_need_graph).toBeNull();
|
||||
expect(result.turn_meaning_ref).toBeNull();
|
||||
expect(result.reason_codes).toContain(
|
||||
"mcp_discovery_business_overview_continuation_suppressed_by_explicit_vat_question"
|
||||
);
|
||||
expect(result.reason_codes).toContain("mcp_discovery_not_applicable_for_supported_exact_turn");
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_business_overview_continuation_from_followup_context");
|
||||
});
|
||||
|
||||
it("routes business overview final-summary wording to the overview lane without document pseudo subject", () => {
|
||||
@@ -3012,4 +3107,108 @@ describe("assistant MCP discovery turn input adapter", () => {
|
||||
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
|
||||
expect(result.reason_codes).toContain("mcp_discovery_business_overview_continuation_from_followup_context");
|
||||
});
|
||||
|
||||
it("routes cross-scope executive summary over stale document carryover and keeps prior bundles", () => {
|
||||
const orgName =
|
||||
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
||||
const counterpartyName = "\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a";
|
||||
const valueFlowBundle = {
|
||||
counterparty: counterpartyName,
|
||||
incoming_total: 20653490,
|
||||
outgoing_total: 2129651,
|
||||
net_amount: 18523839
|
||||
};
|
||||
const documentBundle = {
|
||||
counterparty: counterpartyName,
|
||||
document_count: 19,
|
||||
direct_answer:
|
||||
"\u041a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442: \u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a. \u041d\u0430\u0439\u0434\u0435\u043d\u043e \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u043e\u0432: 19."
|
||||
};
|
||||
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage:
|
||||
"\u0421\u043e\u0431\u0435\u0440\u0438 \u043a\u043e\u0440\u043e\u0442\u043a\u0438\u0439 \u0438\u0442\u043e\u0433: \u0447\u0442\u043e \u043c\u044b \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u043b\u0438 \u043f\u043e \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438, \u0447\u0442\u043e \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e \u043f\u043e \u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a, \u043a\u0430\u043a\u0438\u0435 \u0432\u044b\u0432\u043e\u0434\u044b \u043c\u043e\u0436\u043d\u043e \u0434\u0435\u043b\u0430\u0442\u044c \u0438 \u043a\u0430\u043a\u0438\u0435 \u043d\u0435\u043b\u044c\u0437\u044f.",
|
||||
assistantTurnMeaning: {
|
||||
asked_domain_family: "counterparty",
|
||||
asked_action_family: "list_documents",
|
||||
explicit_intent_candidate: "list_documents_by_counterparty"
|
||||
},
|
||||
followupContext: {
|
||||
previous_discovery_pilot_scope: "counterparty_document_evidence_query_documents_v1",
|
||||
previous_intent: "list_documents_by_counterparty",
|
||||
target_intent: "list_documents_by_counterparty",
|
||||
previous_filters: {
|
||||
organization: orgName,
|
||||
counterparty: counterpartyName,
|
||||
as_of_date: "2026-05-09"
|
||||
},
|
||||
previous_anchor_type: "counterparty",
|
||||
previous_anchor_value: counterpartyName,
|
||||
previous_discovery_bidirectional_value_flow: valueFlowBundle,
|
||||
previous_discovery_document_summary: documentBundle
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.adapter_status).toBe("ready");
|
||||
expect(result.should_run_discovery).toBe(true);
|
||||
expect(result.semantic_data_need).toBe("business overview evidence with bounded analyst interpretation");
|
||||
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
|
||||
expect(result.data_need_graph?.subject_candidates).toEqual([]);
|
||||
expect(result.turn_meaning_ref).toMatchObject({
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "broad_evaluation",
|
||||
business_overview_separate_entity_candidates: [counterpartyName],
|
||||
previous_counterparty_value_flow_bundle: valueFlowBundle,
|
||||
previous_counterparty_document_bundle: documentBundle,
|
||||
explicit_organization_scope: orgName,
|
||||
unsupported_but_understood_family: "broad_business_evaluation",
|
||||
stale_replay_forbidden: true
|
||||
});
|
||||
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
|
||||
expect(result.reason_codes).toContain(
|
||||
"mcp_discovery_business_overview_preserved_explicit_counterparty_summary_scope"
|
||||
);
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_not_applicable_for_supported_exact_turn");
|
||||
});
|
||||
|
||||
it("preserves explicit counterparty scope for company plus counterparty business summaries", () => {
|
||||
const orgName = "ООО Альтернатива Плюс";
|
||||
const counterpartyName = "Группа СВК";
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage:
|
||||
"Собери короткий итог: что мы подтвердили по компании, что отдельно по Группа СВК, какие выводы можно делать и какие нельзя.",
|
||||
assistantTurnMeaning: {
|
||||
asked_domain_family: "business_summary",
|
||||
asked_action_family: "broad_evaluation",
|
||||
unsupported_but_understood_family: "broad_business_evaluation",
|
||||
stale_replay_forbidden: true
|
||||
},
|
||||
followupContext: {
|
||||
previous_discovery_pilot_scope: "counterparty_document_evidence_query_documents_v1",
|
||||
previous_filters: {
|
||||
organization: orgName,
|
||||
counterparty: counterpartyName
|
||||
},
|
||||
previous_anchor_type: "counterparty",
|
||||
previous_anchor_value: counterpartyName
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.adapter_status).toBe("ready");
|
||||
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
|
||||
expect(result.data_need_graph?.subject_candidates).toEqual([]);
|
||||
expect(result.turn_meaning_ref).toMatchObject({
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "broad_evaluation",
|
||||
business_overview_separate_entity_candidates: [counterpartyName],
|
||||
explicit_organization_scope: orgName,
|
||||
unsupported_but_understood_family: "broad_business_evaluation",
|
||||
stale_replay_forbidden: true
|
||||
});
|
||||
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
|
||||
expect(result.reason_codes).toContain(
|
||||
"mcp_discovery_business_overview_preserved_explicit_counterparty_summary_scope"
|
||||
);
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_business_overview_suppressed_stale_counterparty");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -69,6 +69,27 @@ describe("assistantMemoryRecapPolicy", () => {
|
||||
expect(signals.contextualMemoryRecapFollowupDetected).toBe(true);
|
||||
});
|
||||
|
||||
it("detects startup memory checkpoint without prior grounded context", () => {
|
||||
const signals = policy.resolveRouteMemorySignals({
|
||||
rawUserMessage:
|
||||
"Сделай короткий стартовый чек контекста: есть ли уже выбранная компания или контрагент в текущем диалоге; если нет, скажи честно и не выдумывай память про Группа СВК.",
|
||||
repairedRawUserMessage: "",
|
||||
effectiveAddressUserMessage: "",
|
||||
repairedEffectiveAddressUserMessage: "",
|
||||
dataScopeMetaQuery: false,
|
||||
capabilityMetaQuery: false,
|
||||
dataRetrievalSignal: false,
|
||||
strongDataSignal: true,
|
||||
aggregateBusinessAnalyticsSignal: false,
|
||||
lastGroundedAddressDebug: null,
|
||||
hasPriorAddressDebug: false,
|
||||
sessionItems: []
|
||||
});
|
||||
|
||||
expect(signals.contextualHistoricalCapabilityFollowupDetected).toBe(false);
|
||||
expect(signals.contextualMemoryRecapFollowupDetected).toBe(true);
|
||||
});
|
||||
|
||||
it("treats explicit recap wording over selected-object phrasing as memory follow-up even when data cues are present", () => {
|
||||
const signals = policy.resolveRouteMemorySignals({
|
||||
rawUserMessage: "а ты помнишь, что мы по этой позиции уже выяснили?",
|
||||
@@ -324,6 +345,25 @@ describe("assistantMemoryRecapPolicy", () => {
|
||||
expect(reply).toContain("подняли документы закупки");
|
||||
});
|
||||
|
||||
it("honestly reports empty memory when startup checkpoint has no selected context", () => {
|
||||
const reply = buildAddressMemoryRecapReply({
|
||||
organization: null,
|
||||
addressDebug: null,
|
||||
sessionItems: [],
|
||||
userMessage:
|
||||
"Сделай короткий стартовый чек контекста: есть ли уже выбранная компания или контрагент в текущем диалоге; если нет, скажи честно и не выдумывай память про Группа СВК.",
|
||||
toNonEmptyString: (value: unknown) => {
|
||||
const text = String(value ?? "").trim();
|
||||
return text.length > 0 ? text : null;
|
||||
}
|
||||
});
|
||||
|
||||
expect(reply).toContain("Коротко: в текущем диалоге я не вижу выбранной компании");
|
||||
expect(reply).toContain("Группа СВК");
|
||||
expect(reply).toContain("не подтверждена");
|
||||
expect(reply).not.toContain("Да, помню предыдущий адресный контур");
|
||||
});
|
||||
|
||||
it("resolves grounded answer inspection from shared memory context", () => {
|
||||
const context = resolveAssistantLivingChatMemoryContext({
|
||||
modeDecisionReason: "answer_inspection_followup_detected",
|
||||
|
||||
@@ -131,6 +131,85 @@ describe("assistant truth answer policy runtime adapter", () => {
|
||||
expect(policy.answer_shape.may_power_followup).toBe(true);
|
||||
});
|
||||
|
||||
it("downgrades stale full-confirmed truth gates when coverage evidence is only heuristic", () => {
|
||||
const policy = resolveAssistantTruthAnswerPolicyRuntime({
|
||||
addressDebug: {
|
||||
capability_id: "address_open_items_by_counterparty_or_contract",
|
||||
rows_matched: 8,
|
||||
answer_grounding_check: {
|
||||
status: "grounded",
|
||||
reasons: ["confirmed_balance_unavailable_fallback_to_heuristic_candidates"]
|
||||
},
|
||||
address_coverage_evidence_v1: {
|
||||
schema_version: "address_coverage_evidence_v1",
|
||||
policy_owner: "addressCoverageEvidencePolicy",
|
||||
requested_result_mode: "confirmed_balance",
|
||||
result_mode: "heuristic_candidates",
|
||||
evidence_strength: "medium",
|
||||
balance_confirmed: false,
|
||||
as_of_date_basis: "period_range",
|
||||
coverage_status: "partial",
|
||||
evidence_basis: "heuristic_candidates",
|
||||
reason_codes: [
|
||||
"coverage_status_partial",
|
||||
"result_mode_heuristic_candidates",
|
||||
"balance_confirmed_false"
|
||||
]
|
||||
},
|
||||
address_truth_gate_v1: {
|
||||
schema_version: "address_truth_gate_v1",
|
||||
policy_owner: "addressTruthGatePolicy",
|
||||
truth_gate_status: "full_confirmed",
|
||||
carryover_eligibility: "none",
|
||||
limited_reason_category: null,
|
||||
runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS",
|
||||
reason_codes: ["stale_full_confirmed_shadow"],
|
||||
blocked_or_limited_explanation: null
|
||||
}
|
||||
},
|
||||
replyType: "factual"
|
||||
});
|
||||
|
||||
expect(policy.truth_gate.coverage_status).toBe("partial");
|
||||
expect(policy.truth_gate.truth_mode).toBe("limited");
|
||||
expect(policy.truth_gate.evidence_grade).toBe("medium");
|
||||
expect(policy.truth_gate.reason_codes).toContain("confirmed_balance_unavailable_fallback_to_heuristic_candidates");
|
||||
expect(policy.answer_shape.answer_shape).toBe("limited_with_reason");
|
||||
expect(policy.answer_shape.must_include_limitation).toBe(true);
|
||||
});
|
||||
|
||||
it("downgrades stale full-confirmed truth gates from top-level heuristic result metadata", () => {
|
||||
const policy = resolveAssistantTruthAnswerPolicyRuntime({
|
||||
addressDebug: {
|
||||
capability_id: "address_open_items_by_counterparty_or_contract",
|
||||
rows_matched: 8,
|
||||
result_mode: "heuristic_candidates",
|
||||
evidence_strength: "medium",
|
||||
balance_confirmed: false,
|
||||
answer_grounding_check: {
|
||||
status: "grounded",
|
||||
reasons: ["confirmed_balance_unavailable_fallback_to_heuristic_candidates"]
|
||||
},
|
||||
address_truth_gate_v1: {
|
||||
schema_version: "address_truth_gate_v1",
|
||||
policy_owner: "addressTruthGatePolicy",
|
||||
truth_gate_status: "full_confirmed",
|
||||
carryover_eligibility: "none",
|
||||
limited_reason_category: null,
|
||||
runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS",
|
||||
reason_codes: [],
|
||||
blocked_or_limited_explanation: null
|
||||
}
|
||||
},
|
||||
replyType: "factual"
|
||||
});
|
||||
|
||||
expect(policy.truth_gate.coverage_status).toBe("partial");
|
||||
expect(policy.truth_gate.truth_mode).toBe("limited");
|
||||
expect(policy.truth_gate.evidence_grade).toBe("medium");
|
||||
expect(policy.answer_shape.answer_shape).toBe("limited_with_reason");
|
||||
});
|
||||
|
||||
it("keeps explicit temporal-limited factual answers limited in the truth contract", () => {
|
||||
const policy = resolveAssistantTruthAnswerPolicyRuntime({
|
||||
addressDebug: {
|
||||
|
||||
Reference in New Issue
Block a user