2061 lines
84 KiB
TypeScript
2061 lines
84 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
||
import { createAssistantTransitionPolicy } from "../src/services/assistantTransitionPolicy";
|
||
import { buildRootScopedCarryoverFilters } from "../src/services/assistantContinuityPolicy";
|
||
|
||
function toNonEmptyString(value: unknown): string | null {
|
||
if (value === null || value === undefined) {
|
||
return null;
|
||
}
|
||
const text = String(value).trim();
|
||
return text.length > 0 ? text : null;
|
||
}
|
||
|
||
function buildPolicy(overrides: Record<string, unknown> = {}) {
|
||
return createAssistantTransitionPolicy({
|
||
compactWhitespace: (value: string) => String(value ?? "").replace(/\s+/g, " ").trim(),
|
||
repairAddressMojibake: (value: string) => value,
|
||
countTokens: (value: string) => String(value ?? "").split(/\s+/).filter(Boolean).length,
|
||
shouldHandleAsAssistantCapabilityMetaQuery: () => false,
|
||
hasDataRetrievalRequestSignal: () => false,
|
||
findLastAddressAssistantItem: () => ({
|
||
text: "1. Рабочая станция",
|
||
debug: {
|
||
detected_intent: "inventory_purchase_documents_for_item",
|
||
extracted_filters: {
|
||
item: "Рабочая станция"
|
||
},
|
||
anchor_type: "item",
|
||
anchor_value_resolved: "Рабочая станция"
|
||
}
|
||
}),
|
||
findLastOrganizationClarificationAddressDebug: () => null,
|
||
mergeKnownOrganizations: (values: unknown[]) => values,
|
||
resolveOrganizationSelectionFromMessage: () => null,
|
||
toNonEmptyString,
|
||
buildAddressFollowupOffer: () => null,
|
||
isImplicitAddressContinuationByLlm: () => false,
|
||
isInventorySelectedObjectIntent: (intent: unknown) =>
|
||
[
|
||
"inventory_purchase_provenance_for_item",
|
||
"inventory_purchase_documents_for_item",
|
||
"inventory_sale_trace_for_item",
|
||
"inventory_profitability_for_item",
|
||
"inventory_purchase_to_sale_chain",
|
||
"inventory_aging_by_purchase_date"
|
||
].includes(String(intent ?? "")),
|
||
hasShortInventoryObjectFollowupSignal: () => false,
|
||
resolveDebtRoleSwapFollowupIntent: () => null,
|
||
hasAddressFollowupContextSignal: () => false,
|
||
extractDisplayedEntityIndexMention: () => null,
|
||
findRecentInventoryRootFrame: () => ({
|
||
intent: "inventory_on_hand_as_of_date",
|
||
filters: {
|
||
as_of_date: "2020-03-31",
|
||
organization: 'ООО "Альтернатива Плюс"'
|
||
},
|
||
anchorType: "organization",
|
||
anchorValue: 'ООО "Альтернатива Плюс"'
|
||
}),
|
||
hasInventoryRootTemporalFollowupSignal: (message: string) => /март 2020/i.test(message),
|
||
hasFollowupMarker: () => false,
|
||
hasReferentialPointer: () => false,
|
||
hasStandaloneAddressTopicSignal: () => false,
|
||
resolveAddressIntent: () => ({ intent: "unknown" }),
|
||
resolveAddressIntentFamily: (intent: unknown) => (intent ? String(intent) : null),
|
||
readAddressFilterString: (debug: Record<string, unknown>, key: string) =>
|
||
debug?.extracted_filters && typeof debug.extracted_filters === "object"
|
||
? toNonEmptyString((debug.extracted_filters as Record<string, unknown>)[key])
|
||
: null,
|
||
normalizeOrganizationScopeValue: (value: unknown) => toNonEmptyString(value),
|
||
isInventoryDrilldownFrameIntent: (intent: unknown) =>
|
||
[
|
||
"inventory_purchase_provenance_for_item",
|
||
"inventory_purchase_documents_for_item",
|
||
"inventory_sale_trace_for_item",
|
||
"inventory_profitability_for_item",
|
||
"inventory_purchase_to_sale_chain",
|
||
"inventory_aging_by_purchase_date"
|
||
].includes(String(intent ?? "")),
|
||
isInventoryRootFrameIntent: (intent: unknown) => String(intent ?? "") === "inventory_on_hand_as_of_date",
|
||
findRecentAddressFilterValue: () => null,
|
||
hasForeignAccountingPivotOverInventoryMessage: () => false,
|
||
buildRootScopedCarryoverFilters: (
|
||
previousFilters: Record<string, unknown>,
|
||
inventoryRootFrame: Record<string, unknown>
|
||
) => ({
|
||
organization:
|
||
toNonEmptyString(inventoryRootFrame?.filters?.organization) ?? toNonEmptyString(previousFilters?.organization),
|
||
warehouse:
|
||
toNonEmptyString(inventoryRootFrame?.filters?.warehouse) ?? toNonEmptyString(previousFilters?.warehouse),
|
||
as_of_date:
|
||
toNonEmptyString(previousFilters?.as_of_date) ?? toNonEmptyString(inventoryRootFrame?.filters?.as_of_date),
|
||
period_from:
|
||
toNonEmptyString(previousFilters?.period_from) ?? toNonEmptyString(inventoryRootFrame?.filters?.period_from),
|
||
period_to:
|
||
toNonEmptyString(previousFilters?.period_to) ?? toNonEmptyString(inventoryRootFrame?.filters?.period_to)
|
||
}),
|
||
inferDisplayedEntityTypeFromIntent: () => "item",
|
||
extractDisplayedAddressEntityCandidates: () => [],
|
||
resolveDisplayedAddressEntityMention: () => null,
|
||
...overrides
|
||
});
|
||
}
|
||
|
||
describe("assistantTransitionPolicy", () => {
|
||
it("promotes inventory temporal follow-up into root-scoped carryover", () => {
|
||
const policy = buildPolicy();
|
||
|
||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||
"остатки на март 2020",
|
||
[],
|
||
null,
|
||
null,
|
||
{
|
||
session_context: {
|
||
active_focus_object: {
|
||
object_type: "item",
|
||
label: "Рабочая станция"
|
||
}
|
||
}
|
||
}
|
||
);
|
||
|
||
expect(carryover?.followupSelectionMode).toBe("carry_root_context");
|
||
expect(carryover?.followupContext?.root_context_only).toBe(true);
|
||
expect(carryover?.followupContext?.target_intent).toBe("inventory_on_hand_as_of_date");
|
||
expect(carryover?.followupContext?.root_intent).toBe("inventory_on_hand_as_of_date");
|
||
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||
as_of_date: "2020-03-31",
|
||
organization: 'ООО "Альтернатива Плюс"'
|
||
});
|
||
});
|
||
|
||
it("promotes same-date inventory restatement after drilldown into root-scoped carryover", () => {
|
||
const policy = buildPolicy({
|
||
hasInventoryRootTemporalFollowupSignal: () => false
|
||
});
|
||
|
||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||
"покажи еще раз остатки на эту же дату",
|
||
[],
|
||
null,
|
||
null,
|
||
null
|
||
);
|
||
|
||
expect(carryover?.followupSelectionMode).toBe("carry_root_context");
|
||
expect(carryover?.followupContext?.root_context_only).toBe(true);
|
||
expect(carryover?.followupContext?.previous_intent).toBeUndefined();
|
||
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||
as_of_date: "2020-03-31",
|
||
organization: 'ООО "Альтернатива Плюс"'
|
||
});
|
||
expect(carryover?.followupContext?.root_intent).toBe("inventory_on_hand_as_of_date");
|
||
});
|
||
|
||
it("builds continuation contract from extracted root carryover", () => {
|
||
const policy = buildPolicy();
|
||
|
||
const contract = policy.buildAddressDialogContinuationContractV2(
|
||
"остатки на эту дату",
|
||
"остатки на эту дату",
|
||
{
|
||
followupContext: {
|
||
root_intent: "inventory_on_hand_as_of_date",
|
||
previous_anchor_type: "item",
|
||
previous_anchor_value: "Рабочая станция"
|
||
},
|
||
previousSourceIntent: "inventory_purchase_documents_for_item",
|
||
previousAddressIntent: null,
|
||
followupSelectionMode: "carry_root_context",
|
||
hasImplicitContinuationSignal: true
|
||
},
|
||
{
|
||
predecomposeContract: {
|
||
intent: "unknown"
|
||
}
|
||
}
|
||
);
|
||
|
||
expect(contract.decision).toBe("continue_previous");
|
||
expect(contract.target_intent).toBe("inventory_on_hand_as_of_date");
|
||
expect(contract.decision_reasons).toContain("root_context_only_carryover");
|
||
expect(contract.decision_reasons).toContain("implicit_continuation_by_llm");
|
||
expect(contract.anchor_type).toBe("item");
|
||
expect(contract.anchor_value).toBe("Рабочая станция");
|
||
});
|
||
|
||
it("prefers carryover target intent over llm contract drift in continuation contract", () => {
|
||
const policy = buildPolicy();
|
||
|
||
const contract = policy.buildAddressDialogContinuationContractV2(
|
||
"покажи договор по гамме",
|
||
"покажи договор по гамме",
|
||
{
|
||
followupContext: {
|
||
previous_intent: "customer_revenue_and_payments",
|
||
target_intent: "list_contracts_by_counterparty",
|
||
previous_anchor_type: "counterparty",
|
||
previous_anchor_value: "Гамма-мебель, ООО"
|
||
},
|
||
previousSourceIntent: "customer_revenue_and_payments",
|
||
previousAddressIntent: "customer_revenue_and_payments",
|
||
followupSelectionMode: "carry_referenced_entity",
|
||
hasImplicitContinuationSignal: false
|
||
},
|
||
{
|
||
predecomposeContract: {
|
||
intent: "unknown"
|
||
}
|
||
}
|
||
);
|
||
|
||
expect(contract.target_intent).toBe("list_contracts_by_counterparty");
|
||
expect(contract.decision).toBe("continue_previous");
|
||
});
|
||
|
||
it("retargets displayed counterparty follow-up through shared referenced-entity carryover", () => {
|
||
const policy = buildPolicy({
|
||
findLastAddressAssistantItem: () => ({
|
||
text: "1. SVK Group\n2. Gamma",
|
||
debug: {
|
||
detected_intent: "customer_revenue_and_payments",
|
||
extracted_filters: {
|
||
organization: 'ООО "Альтернатива Плюс"',
|
||
period_from: "2017-01-01",
|
||
period_to: "2017-12-31"
|
||
}
|
||
}
|
||
}),
|
||
hasAddressFollowupContextSignal: () => true,
|
||
inferDisplayedEntityTypeFromIntent: () => "counterparty",
|
||
extractDisplayedAddressEntityCandidates: () => [{ entityType: "counterparty", value: "SVK Group" }],
|
||
resolveDisplayedAddressEntityMention: () => ({ entityType: "counterparty", value: "SVK Group" }),
|
||
resolveAddressIntent: () => ({ intent: "unknown" })
|
||
});
|
||
|
||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||
"покажи договоры по СВК",
|
||
[],
|
||
null,
|
||
null,
|
||
null
|
||
);
|
||
|
||
expect(carryover?.followupSelectionMode).toBe("carry_referenced_entity");
|
||
expect(carryover?.followupContext?.target_intent).toBe("list_contracts_by_counterparty");
|
||
expect(carryover?.followupContext?.previous_anchor_type).toBe("counterparty");
|
||
expect(carryover?.followupContext?.previous_anchor_value).toBe("SVK Group");
|
||
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||
organization: 'ООО "Альтернатива Плюс"',
|
||
counterparty: "SVK Group",
|
||
period_from: "2017-01-01",
|
||
period_to: "2017-12-31"
|
||
});
|
||
expect(carryover?.followupContext?.resolved_counterparty_from_display).toBe(true);
|
||
});
|
||
|
||
it("retargets same-date inventory follow-up away from receivables intent", () => {
|
||
const policy = buildPolicy({
|
||
findLastAddressAssistantItem: () => ({
|
||
text: "Подтвержденная дебиторская задолженность на 31.03.2020 собрана.",
|
||
debug: {
|
||
detected_intent: "receivables_confirmed_as_of_date",
|
||
extracted_filters: {
|
||
organization: 'ООО "Альтернатива Плюс"',
|
||
as_of_date: "2020-03-31",
|
||
period_from: "2020-03-01",
|
||
period_to: "2020-03-31"
|
||
},
|
||
anchor_type: "organization",
|
||
anchor_value_resolved: 'ООО "Альтернатива Плюс"'
|
||
}
|
||
}),
|
||
hasAddressFollowupContextSignal: () => true,
|
||
hasReferentialPointer: () => true,
|
||
findRecentInventoryRootFrame: () => null,
|
||
resolveAddressIntent: () => ({ intent: "unknown" })
|
||
});
|
||
|
||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||
"остатки по складу на эту же дату",
|
||
[],
|
||
null,
|
||
null,
|
||
null
|
||
);
|
||
|
||
expect(carryover?.followupSelectionMode).toBe("carry_previous_intent");
|
||
expect(carryover?.followupContext?.previous_intent).toBe("receivables_confirmed_as_of_date");
|
||
expect(carryover?.followupContext?.target_intent).toBe("inventory_on_hand_as_of_date");
|
||
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||
organization: 'ООО "Альтернатива Плюс"',
|
||
as_of_date: "2020-03-31",
|
||
period_from: "2020-03-01",
|
||
period_to: "2020-03-31"
|
||
});
|
||
expect(carryover?.followupContext?.root_context_only).toBeUndefined();
|
||
});
|
||
|
||
it("hydrates selected-item carryover through shared continuity helper for short object follow-up", () => {
|
||
const policy = buildPolicy({
|
||
findLastAddressAssistantItem: () => ({
|
||
text: "Подтвержден складской срез по выбранной позиции.",
|
||
debug: {
|
||
detected_intent: "inventory_on_hand_as_of_date",
|
||
extracted_filters: {
|
||
organization: 'ООО "Альтернатива Плюс"',
|
||
as_of_date: "2020-03-31"
|
||
}
|
||
}
|
||
}),
|
||
hasAddressFollowupContextSignal: () => true,
|
||
hasShortInventoryObjectFollowupSignal: () => true,
|
||
findRecentInventoryRootFrame: () => null,
|
||
resolveAddressIntent: () => ({ intent: "unknown" })
|
||
});
|
||
|
||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||
"по этой позиции покажи документы",
|
||
[],
|
||
null,
|
||
null,
|
||
{
|
||
session_context: {
|
||
active_focus_object: {
|
||
object_type: "item",
|
||
label: "Workstation Focus"
|
||
}
|
||
}
|
||
}
|
||
);
|
||
|
||
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||
organization: 'ООО "Альтернатива Плюс"',
|
||
as_of_date: "2020-03-31",
|
||
item: "Workstation Focus"
|
||
});
|
||
expect(carryover?.followupContext?.previous_anchor_type).toBe("item");
|
||
expect(carryover?.followupContext?.previous_anchor_value).toBe("Workstation Focus");
|
||
});
|
||
|
||
it("hydrates follow-up organization from shared assistant authority when local history filters are empty", () => {
|
||
const policy = buildPolicy({
|
||
findLastAddressAssistantItem: () => ({
|
||
text: "Подтвержденная дебиторская задолженность на 31.03.2020 собрана.",
|
||
debug: {
|
||
detected_intent: "receivables_confirmed_as_of_date",
|
||
extracted_filters: {
|
||
as_of_date: "2020-03-31",
|
||
period_from: "2020-03-01",
|
||
period_to: "2020-03-31"
|
||
},
|
||
anchor_type: "organization",
|
||
anchor_value_resolved: null
|
||
}
|
||
}),
|
||
hasAddressFollowupContextSignal: () => true,
|
||
hasReferentialPointer: () => true,
|
||
findRecentInventoryRootFrame: () => null,
|
||
findRecentAddressFilterValue: () => null,
|
||
resolveAddressIntent: () => ({ intent: "unknown" })
|
||
});
|
||
|
||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||
"остатки по складу на эту же дату",
|
||
[
|
||
{
|
||
role: "assistant",
|
||
text: "Компания уже выбрана в живом чате.",
|
||
debug: {
|
||
assistant_active_organization: 'ООО "Альтернатива Плюс"',
|
||
assistant_known_organizations: ['ООО "Альтернатива Плюс"', 'ООО "Лайт"']
|
||
}
|
||
}
|
||
],
|
||
null,
|
||
null,
|
||
null
|
||
);
|
||
|
||
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||
organization: 'ООО "Альтернатива Плюс"',
|
||
as_of_date: "2020-03-31",
|
||
period_from: "2020-03-01",
|
||
period_to: "2020-03-31"
|
||
});
|
||
expect(carryover?.followupContext?.target_intent).toBe("inventory_on_hand_as_of_date");
|
||
});
|
||
|
||
it("hydrates carryover anchor from shared debug helpers when explicit anchor fields are absent", () => {
|
||
const policy = buildPolicy({
|
||
findLastAddressAssistantItem: () => ({
|
||
text: "Подтвержденный складской срез собран.",
|
||
debug: {
|
||
detected_intent: "inventory_on_hand_as_of_date",
|
||
extracted_filters: {
|
||
item: "Рабочая станция",
|
||
period_from: "2020-03-01"
|
||
},
|
||
address_root_frame_context: {
|
||
as_of_date: "2020-03-31",
|
||
period_to: "2020-03-31"
|
||
},
|
||
anchor_type: null,
|
||
anchor_value_resolved: null
|
||
}
|
||
}),
|
||
hasAddressFollowupContextSignal: () => true,
|
||
hasReferentialPointer: () => true,
|
||
findRecentInventoryRootFrame: () => null,
|
||
findRecentAddressFilterValue: () => null,
|
||
resolveAddressIntent: () => ({ intent: "unknown" })
|
||
});
|
||
|
||
const carryover = policy.resolveAddressFollowupCarryoverContext("по этой позиции", [], null, null, null);
|
||
|
||
expect(carryover?.followupContext?.previous_anchor_type).toBe("item");
|
||
expect(carryover?.followupContext?.previous_anchor_value).toBe("Рабочая станция");
|
||
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||
item: "Рабочая станция",
|
||
period_from: "2020-03-01"
|
||
});
|
||
});
|
||
|
||
it("bridges selected-item purchase provenance into a VAT period follow-up", () => {
|
||
const item = "Рабочая станция универсального специалиста";
|
||
const policy = buildPolicy({
|
||
findLastAddressAssistantItem: () => ({
|
||
text: [
|
||
`По позиции ${item} однозначный поставщик не подтвержден.`,
|
||
"Подтверждение:",
|
||
"- Первая найденная дата закупки: 05.02.2015.",
|
||
"- Последняя найденная дата закупки: 22.07.2015."
|
||
].join("\n"),
|
||
debug: {
|
||
detected_intent: "inventory_purchase_provenance_for_item",
|
||
extracted_filters: {
|
||
item,
|
||
organization: 'ООО "Альтернатива Плюс"',
|
||
as_of_date: "2016-03-31"
|
||
},
|
||
anchor_type: "item",
|
||
anchor_value_resolved: item
|
||
}
|
||
}),
|
||
hasAddressFollowupContextSignal: () => false,
|
||
findRecentInventoryRootFrame: () => null,
|
||
resolveAddressIntent: () => ({ intent: "unknown" })
|
||
});
|
||
|
||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||
"ндс можешь прикинуть на дату покупки рабочей станции?",
|
||
[],
|
||
null,
|
||
null,
|
||
{
|
||
session_context: {
|
||
active_focus_object: {
|
||
object_type: "item",
|
||
label: item,
|
||
provenance_result_set_id: "rs-provenance"
|
||
},
|
||
active_result_set_id: "rs-provenance"
|
||
},
|
||
result_sets: [
|
||
{
|
||
result_set_id: "rs-provenance",
|
||
intent: "inventory_purchase_provenance_for_item",
|
||
entity_refs: [
|
||
{
|
||
index: 1,
|
||
entity_type: "item",
|
||
value: "Поступление товаров и услуг 00000000023 от 05.02.2015 0:00:00"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
);
|
||
|
||
expect(carryover?.followupContext?.previous_intent).toBe("inventory_purchase_provenance_for_item");
|
||
expect(carryover?.followupContext?.target_intent).toBe("vat_liability_confirmed_for_tax_period");
|
||
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||
item,
|
||
organization: 'ООО "Альтернатива Плюс"',
|
||
period_from: "2015-02-01",
|
||
period_to: "2015-02-28"
|
||
});
|
||
});
|
||
|
||
it("keeps selected-object continuity for purchase-date VAT bridge even when an inventory root frame exists", () => {
|
||
const item = "Рабочая станция универсального специалиста";
|
||
const policy = buildPolicy({
|
||
findLastAddressAssistantItem: () => ({
|
||
text: [
|
||
`По позиции ${item} однозначный поставщик не подтвержден.`,
|
||
"Подтверждение:",
|
||
"- Первая найденная дата закупки: 05.02.2015."
|
||
].join("\n"),
|
||
debug: {
|
||
detected_intent: "inventory_purchase_provenance_for_item",
|
||
extracted_filters: {
|
||
item,
|
||
organization: 'ООО "Альтернатива Плюс"',
|
||
as_of_date: "2016-03-31"
|
||
},
|
||
anchor_type: "item",
|
||
anchor_value_resolved: item
|
||
}
|
||
}),
|
||
hasAddressFollowupContextSignal: () => false,
|
||
hasForeignAccountingPivotOverInventoryMessage: () => true,
|
||
resolveAddressIntent: () => ({ intent: "unknown" })
|
||
});
|
||
|
||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||
"ндс можешь прикинуть на дату покупки рабочей станции?",
|
||
[],
|
||
null,
|
||
null,
|
||
{
|
||
session_context: {
|
||
active_focus_object: {
|
||
object_type: "item",
|
||
label: item,
|
||
provenance_result_set_id: "rs-provenance"
|
||
},
|
||
active_result_set_id: "rs-provenance"
|
||
},
|
||
result_sets: [
|
||
{
|
||
result_set_id: "rs-provenance",
|
||
intent: "inventory_purchase_provenance_for_item",
|
||
entity_refs: [
|
||
{
|
||
index: 1,
|
||
entity_type: "item",
|
||
value: "Поступление товаров и услуг 00000000023 от 05.02.2015 0:00:00"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
);
|
||
|
||
expect(carryover?.followupSelectionMode).toBe("carry_previous_intent");
|
||
expect(carryover?.followupContext?.root_context_only).toBeUndefined();
|
||
expect(carryover?.followupContext?.previous_intent).toBe("inventory_purchase_provenance_for_item");
|
||
expect(carryover?.followupContext?.target_intent).toBe("vat_liability_confirmed_for_tax_period");
|
||
});
|
||
|
||
it("keeps purchase-date VAT bridge after unsupported verification interrupt", () => {
|
||
const item = "Рабочая станция универрсального специалиста";
|
||
const provenanceItem = {
|
||
role: "assistant",
|
||
text: [
|
||
`По позиции ${item} однозначный поставщик не подтвержден.`,
|
||
"Подтверждение:",
|
||
"- Первая найденная дата закупки: 05.02.2015."
|
||
].join("\n"),
|
||
debug: {
|
||
execution_lane: "address_query",
|
||
answer_grounding_check: {
|
||
status: "grounded"
|
||
},
|
||
detected_intent: "inventory_purchase_provenance_for_item",
|
||
extracted_filters: {
|
||
item,
|
||
organization: 'ООО "Альтернатива Плюс"',
|
||
as_of_date: "2016-03-31"
|
||
},
|
||
anchor_type: "item",
|
||
anchor_value_resolved: item
|
||
}
|
||
} as any;
|
||
const unsupportedInterrupt = {
|
||
role: "assistant",
|
||
text: "Пока не могу точно подтвердить, что именно это ты имеешь в виду.",
|
||
debug: {
|
||
execution_lane: "address_query",
|
||
detected_intent: "unknown",
|
||
answer_grounding_check: {
|
||
status: "unsupported"
|
||
}
|
||
}
|
||
} as any;
|
||
const policy = buildPolicy({
|
||
findLastAddressAssistantItem: () => unsupportedInterrupt,
|
||
hasAddressFollowupContextSignal: () => false,
|
||
findRecentInventoryRootFrame: () => null,
|
||
resolveAddressIntent: () => ({ intent: "unknown" })
|
||
});
|
||
|
||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||
"ндс можешь прикинуть на дату покупки рабочей станции?",
|
||
[provenanceItem, unsupportedInterrupt],
|
||
null,
|
||
null,
|
||
{
|
||
session_context: {
|
||
active_focus_object: {
|
||
object_type: "item",
|
||
label: item,
|
||
provenance_result_set_id: "rs-provenance-interrupt"
|
||
},
|
||
active_result_set_id: "rs-provenance-interrupt"
|
||
},
|
||
result_sets: [
|
||
{
|
||
result_set_id: "rs-provenance-interrupt",
|
||
intent: "inventory_purchase_provenance_for_item",
|
||
entity_refs: [
|
||
{
|
||
index: 1,
|
||
entity_type: "item",
|
||
value: "Поступление товаров и услуг 00000000023 от 05.02.2015 0:00:00"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
);
|
||
|
||
expect(carryover?.followupContext?.previous_intent).toBe("inventory_purchase_provenance_for_item");
|
||
expect(carryover?.followupContext?.target_intent).toBe("vat_liability_confirmed_for_tax_period");
|
||
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||
item,
|
||
organization: 'ООО "Альтернатива Плюс"',
|
||
period_from: "2015-02-01",
|
||
period_to: "2015-02-28"
|
||
});
|
||
});
|
||
|
||
it("drops stale carryover for a fresh standalone topic from another intent family", () => {
|
||
const policy = buildPolicy({
|
||
findLastAddressAssistantItem: () => ({
|
||
text: "Прогноз НДС на март 2020 собран.",
|
||
debug: {
|
||
detected_intent: "vat_payable_forecast",
|
||
extracted_filters: {
|
||
period_from: "2020-03-01",
|
||
period_to: "2020-03-31"
|
||
}
|
||
}
|
||
}),
|
||
hasAddressFollowupContextSignal: () => true,
|
||
hasStandaloneAddressTopicSignal: () => true,
|
||
resolveAddressIntent: () => ({ intent: "inventory_on_hand_as_of_date" }),
|
||
resolveAddressIntentFamily: (intent: unknown) => {
|
||
if (String(intent ?? "").startsWith("vat_")) return "vat";
|
||
if (String(intent ?? "").startsWith("inventory_")) return "inventory";
|
||
return null;
|
||
}
|
||
});
|
||
|
||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||
"остаток на складе за май 2020",
|
||
[],
|
||
null,
|
||
null,
|
||
null
|
||
);
|
||
|
||
expect(carryover).toBeNull();
|
||
});
|
||
|
||
it("keeps document intent for short counterparty retarget wording with action verb", () => {
|
||
const policy = buildPolicy({
|
||
findLastAddressAssistantItem: () => ({
|
||
text: "Собран список документов по контрагенту Чапурнов.",
|
||
debug: {
|
||
detected_intent: "list_documents_by_counterparty",
|
||
extracted_filters: {
|
||
counterparty: "Чапурнов"
|
||
},
|
||
anchor_type: "counterparty",
|
||
anchor_value_resolved: "Чапурнов"
|
||
}
|
||
}),
|
||
buildAddressFollowupOffer: () => ({
|
||
enabled: true,
|
||
source_intent: "list_documents_by_counterparty",
|
||
suggested_intents: ["bank_operations_by_counterparty"]
|
||
}),
|
||
isImplicitAddressContinuationByLlm: () => true
|
||
});
|
||
|
||
const carryover = policy.resolveAddressFollowupCarryoverContext("покажи по свк", [], null, null, null);
|
||
|
||
expect(carryover?.followupContext?.previous_intent).toBe("list_documents_by_counterparty");
|
||
expect(carryover?.followupSelectionMode).toBe("carry_previous_intent");
|
||
});
|
||
|
||
it("switches from documents to suggested bank operations on a pronoun follow-up with payment cue", () => {
|
||
const policy = buildPolicy({
|
||
findLastAddressAssistantItem: () => ({
|
||
text: "Собран список документов по контрагенту Жуковка 51.",
|
||
debug: {
|
||
detected_intent: "list_documents_by_counterparty",
|
||
extracted_filters: {
|
||
counterparty: "Р–СѓРєРѕРІРєР° 51"
|
||
},
|
||
anchor_type: "counterparty",
|
||
anchor_value_resolved: "РўРЎР– \\Р–СѓРєРѕРІРєР° 51\\"
|
||
}
|
||
}),
|
||
buildAddressFollowupOffer: () => ({
|
||
enabled: true,
|
||
source_intent: "list_documents_by_counterparty",
|
||
suggested_intents: ["bank_operations_by_counterparty", "list_contracts_by_counterparty"]
|
||
}),
|
||
hasAddressFollowupContextSignal: () => true,
|
||
hasReferentialPointer: () => true
|
||
});
|
||
|
||
const carryover = policy.resolveAddressFollowupCarryoverContext("а по нему платежи?", [], null, null, null);
|
||
|
||
expect(carryover?.followupContext?.previous_intent).toBe("bank_operations_by_counterparty");
|
||
expect(carryover?.followupContext?.target_intent).toBe("bank_operations_by_counterparty");
|
||
expect(carryover?.followupContext?.previous_anchor_type).toBe("counterparty");
|
||
expect(carryover?.followupSelectionMode).toBe("switch_to_suggested_intent");
|
||
expect(carryover?.hasSuggestedIntentPivotSignal).toBe(true);
|
||
|
||
const contract = policy.buildAddressDialogContinuationContractV2(
|
||
"а по нему платежи?",
|
||
"Покажи платежи, связанные с этим объектом",
|
||
carryover,
|
||
{
|
||
predecomposeContract: {
|
||
intent: "unknown"
|
||
}
|
||
}
|
||
);
|
||
|
||
expect(contract.decision).toBe("switch_to_suggested");
|
||
expect(contract.target_intent).toBe("bank_operations_by_counterparty");
|
||
expect(contract.decision_reasons).toContain("suggested_intent_followup_pivot");
|
||
});
|
||
|
||
it("switches from payments to suggested contracts on a pronoun follow-up even when generic follow-up signal is weak", () => {
|
||
const policy = buildPolicy({
|
||
findLastAddressAssistantItem: () => ({
|
||
text: "Собран список банковских операций по контрагенту ТСЖ \\Жуковка 51\\.",
|
||
debug: {
|
||
detected_intent: "bank_operations_by_counterparty",
|
||
extracted_filters: {
|
||
counterparty: "ТСЖ \\Жуковка 51\\"
|
||
},
|
||
anchor_type: "counterparty",
|
||
anchor_value_resolved: "ТСЖ \\Жуковка 51\\"
|
||
}
|
||
}),
|
||
buildAddressFollowupOffer: () => ({
|
||
enabled: true,
|
||
source_intent: "bank_operations_by_counterparty",
|
||
suggested_intents: ["list_documents_by_counterparty", "list_contracts_by_counterparty"]
|
||
}),
|
||
hasAddressFollowupContextSignal: () => false,
|
||
hasReferentialPointer: () => true
|
||
});
|
||
|
||
const carryover = policy.resolveAddressFollowupCarryoverContext("А по нему договоры?", [], null, null, null);
|
||
|
||
expect(carryover?.followupContext?.previous_intent).toBe("list_contracts_by_counterparty");
|
||
expect(carryover?.followupContext?.target_intent).toBe("list_contracts_by_counterparty");
|
||
expect(carryover?.followupContext?.previous_anchor_type).toBe("counterparty");
|
||
expect(carryover?.hasSuggestedIntentPivotSignal).toBe(true);
|
||
expect(carryover?.followupSelectionMode).toBe("switch_to_suggested_intent");
|
||
|
||
const contract = policy.buildAddressDialogContinuationContractV2(
|
||
"А по нему договоры?",
|
||
"Покажи договоры, связанные с указанным объектом",
|
||
carryover,
|
||
{
|
||
predecomposeContract: {
|
||
intent: "unknown"
|
||
}
|
||
}
|
||
);
|
||
|
||
expect(contract.decision).toBe("switch_to_suggested");
|
||
expect(contract.target_intent).toBe("list_contracts_by_counterparty");
|
||
expect(contract.decision_reasons).toContain("suggested_intent_followup_pivot");
|
||
});
|
||
|
||
it("keeps root-scoped carryover for foreign accounting pivot over inventory drilldown", () => {
|
||
const policy = buildPolicy({
|
||
findLastAddressAssistantItem: () => ({
|
||
text: "Собран sale trace по позиции.",
|
||
debug: {
|
||
detected_intent: "inventory_sale_trace_for_item",
|
||
extracted_filters: {
|
||
item: "Кромка с клеем 33 дуб ниагара 137 м",
|
||
organization: 'ООО "Альтернатива Плюс"',
|
||
as_of_date: "2021-03-31"
|
||
},
|
||
anchor_type: "item",
|
||
anchor_value_resolved: "Кромка с клеем 33 дуб ниагара 137 м"
|
||
}
|
||
}),
|
||
hasAddressFollowupContextSignal: () => true,
|
||
resolveAddressIntent: () => ({ intent: "vat_payable_confirmed_as_of_date" }),
|
||
resolveAddressIntentFamily: (intent: unknown) => {
|
||
if (String(intent ?? "").startsWith("vat_")) return "vat";
|
||
if (String(intent ?? "").startsWith("inventory_")) return "inventory";
|
||
return null;
|
||
},
|
||
hasForeignAccountingPivotOverInventoryMessage: () => true,
|
||
findRecentInventoryRootFrame: () => ({
|
||
intent: "inventory_on_hand_as_of_date",
|
||
filters: {
|
||
organization: 'ООО "Альтернатива Плюс"',
|
||
warehouse: "Основной склад",
|
||
as_of_date: "2021-03-31",
|
||
period_from: "2021-03-01",
|
||
period_to: "2021-03-31"
|
||
},
|
||
anchorType: "organization",
|
||
anchorValue: 'ООО "Альтернатива Плюс"'
|
||
})
|
||
});
|
||
|
||
const carryover = policy.resolveAddressFollowupCarryoverContext("а ндс?", [], null, null, null);
|
||
|
||
expect(carryover?.followupSelectionMode).toBe("carry_root_context");
|
||
expect(carryover?.followupContext?.root_context_only).toBe(true);
|
||
expect(carryover?.followupContext?.previous_intent).toBeUndefined();
|
||
expect(carryover?.followupContext?.root_intent).toBe("inventory_on_hand_as_of_date");
|
||
expect(carryover?.followupContext?.previous_filters).toEqual({
|
||
organization: 'ООО "Альтернатива Плюс"',
|
||
warehouse: "Основной склад",
|
||
as_of_date: "2021-03-31",
|
||
period_from: "2021-03-01",
|
||
period_to: "2021-03-31"
|
||
});
|
||
});
|
||
|
||
it("prefers the freshest previous date scope over a stale inventory root frame during same-date pivot", () => {
|
||
const filters = buildRootScopedCarryoverFilters(
|
||
{
|
||
organization: 'ООО "Альтернатива Плюс"',
|
||
as_of_date: "2020-03-31",
|
||
period_from: "2020-03-01",
|
||
period_to: "2020-03-31"
|
||
},
|
||
{
|
||
filters: {
|
||
organization: 'ООО "Альтернатива Плюс"',
|
||
warehouse: "Основной склад",
|
||
as_of_date: "2021-03-31",
|
||
period_from: "2021-03-01",
|
||
period_to: "2021-03-31"
|
||
}
|
||
}
|
||
);
|
||
|
||
expect(filters).toEqual({
|
||
organization: 'ООО "Альтернатива Плюс"',
|
||
warehouse: "Основной склад",
|
||
as_of_date: "2020-03-31",
|
||
period_from: "2020-03-01",
|
||
period_to: "2020-03-31"
|
||
});
|
||
});
|
||
|
||
it("does not attach address follow-up carryover to explicit capability-meta questions", () => {
|
||
const policy = buildPolicy({
|
||
shouldHandleAsAssistantCapabilityMetaQuery: (message: unknown) =>
|
||
/дельт[ауы]?\s+по\s+договорам/iu.test(String(message ?? "")),
|
||
hasAddressFollowupContextSignal: () => true
|
||
});
|
||
|
||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||
"ты умеешь считать дельту по договорам?",
|
||
[],
|
||
"проверить возможность расчета дельты по договорам",
|
||
{
|
||
predecomposeContract: {
|
||
mode: "address_query",
|
||
intent: "unknown"
|
||
}
|
||
},
|
||
null
|
||
);
|
||
|
||
expect(carryover).toBeNull();
|
||
});
|
||
|
||
it("retargets selected-object provenance follow-up from inventory root when semantic scope is already detected", () => {
|
||
const policy = buildPolicy({
|
||
hasAddressFollowupContextSignal: () => true,
|
||
findLastAddressAssistantItem: () => ({
|
||
text: "На 31.03.2016 на складе подтверждено 2 позиции.",
|
||
debug: {
|
||
detected_intent: "inventory_on_hand_as_of_date",
|
||
extracted_filters: {
|
||
organization: 'ООО "Альтернатива Плюс"',
|
||
warehouse: "Основной склад",
|
||
as_of_date: "2016-03-31",
|
||
period_from: "2016-03-01",
|
||
period_to: "2016-03-31"
|
||
},
|
||
anchor_type: "organization",
|
||
anchor_value_resolved: 'ООО "Альтернатива Плюс"'
|
||
}
|
||
})
|
||
});
|
||
|
||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||
'По выбранному объекту "Рабочая станция универсального специалиста (индивидуальное изготовление)": где взяли это?',
|
||
[],
|
||
null,
|
||
{
|
||
predecomposeContract: {
|
||
mode: "address_query",
|
||
intent: "unknown",
|
||
semantics: {
|
||
selected_object_scope_detected: true
|
||
}
|
||
}
|
||
},
|
||
null
|
||
);
|
||
|
||
expect(carryover?.followupContext?.target_intent).toBe("inventory_purchase_provenance_for_item");
|
||
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||
organization: 'ООО "Альтернатива Плюс"',
|
||
item: "Рабочая станция универсального специалиста (индивидуальное изготовление)"
|
||
});
|
||
expect(carryover?.followupContext?.previous_anchor_type).toBe("item");
|
||
});
|
||
|
||
it("prefers root-frame dates over stale drilldown filters when hydrating previous filters", () => {
|
||
const organization = "Org Alt";
|
||
const policy = buildPolicy({
|
||
findLastAddressAssistantItem: (_items: unknown[]) => ({
|
||
text: "Workstation drilldown",
|
||
debug: {
|
||
detected_intent: "inventory_purchase_documents_for_item",
|
||
extracted_filters: {
|
||
item: "Workstation",
|
||
organization,
|
||
as_of_date: "2021-04-15"
|
||
},
|
||
anchor_type: "item",
|
||
anchor_value_resolved: "Workstation",
|
||
address_root_frame_context: {
|
||
root_intent: "inventory_on_hand_as_of_date",
|
||
root_filters: {
|
||
organization,
|
||
warehouse: "Main Warehouse",
|
||
as_of_date: "2021-03-31",
|
||
period_from: "2021-03-01",
|
||
period_to: "2021-03-31"
|
||
},
|
||
root_anchor_type: "organization",
|
||
root_anchor_value: organization,
|
||
current_frame_kind: "inventory_drilldown"
|
||
}
|
||
}
|
||
}),
|
||
findRecentInventoryRootFrame: () => null,
|
||
hasInventoryRootTemporalFollowupSignal: (message: string) => /эту же дату/i.test(message)
|
||
});
|
||
|
||
const items = [
|
||
{
|
||
role: "assistant",
|
||
text: "Workstation drilldown",
|
||
debug: {
|
||
execution_lane: "address_query",
|
||
answer_grounding_check: { status: "grounded" },
|
||
detected_intent: "inventory_purchase_documents_for_item",
|
||
extracted_filters: {
|
||
item: "Workstation",
|
||
organization,
|
||
as_of_date: "2021-04-15"
|
||
},
|
||
anchor_type: "item",
|
||
anchor_value_resolved: "Workstation",
|
||
address_root_frame_context: {
|
||
root_intent: "inventory_on_hand_as_of_date",
|
||
root_filters: {
|
||
organization,
|
||
warehouse: "Main Warehouse",
|
||
as_of_date: "2021-03-31",
|
||
period_from: "2021-03-01",
|
||
period_to: "2021-03-31"
|
||
},
|
||
root_anchor_type: "organization",
|
||
root_anchor_value: organization,
|
||
current_frame_kind: "inventory_drilldown"
|
||
}
|
||
}
|
||
}
|
||
];
|
||
|
||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||
"остатки на эту же дату",
|
||
items as any,
|
||
null,
|
||
null,
|
||
null
|
||
);
|
||
|
||
expect(carryover?.followupContext?.root_context_only).toBe(true);
|
||
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||
organization,
|
||
warehouse: "Main Warehouse",
|
||
as_of_date: "2021-03-31",
|
||
period_from: "2021-03-01",
|
||
period_to: "2021-03-31"
|
||
});
|
||
expect(carryover?.followupContext?.previous_filters?.as_of_date).not.toBe("2021-04-15");
|
||
});
|
||
|
||
it("drops carryover when current-turn meaning forbids stale replay", () => {
|
||
const policy = buildPolicy({
|
||
findLastAddressAssistantItem: () => ({
|
||
text: "Documents by previous counterparty",
|
||
debug: {
|
||
execution_lane: "address_query",
|
||
answer_grounding_check: { status: "grounded" },
|
||
detected_intent: "list_documents_by_counterparty",
|
||
extracted_filters: {
|
||
counterparty: "Previous Counterparty",
|
||
organization: "Org Alt"
|
||
},
|
||
anchor_type: "counterparty",
|
||
anchor_value_resolved: "Previous Counterparty"
|
||
}
|
||
}),
|
||
hasAddressFollowupContextSignal: () => true,
|
||
resolveAssistantTurnMeaning: () => ({
|
||
schema_version: "assistant_turn_meaning_v1",
|
||
asked_domain_family: "counterparty",
|
||
asked_action_family: "counterparty_value_or_turnover",
|
||
unsupported_but_understood_family: "counterparty_value_or_turnover",
|
||
explicit_entity_candidates: [
|
||
{
|
||
type: "counterparty",
|
||
value: "svk",
|
||
source: "current_turn_loose_entity_tail"
|
||
}
|
||
],
|
||
stale_replay_forbidden: true
|
||
})
|
||
});
|
||
|
||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||
"\u043a\u0430\u043a\u043e\u0439 \u043e\u0431\u043e\u0440\u043e\u0442 \u0431\u044b\u043b \u0441\u0432\u043a",
|
||
[],
|
||
null,
|
||
null,
|
||
null
|
||
);
|
||
|
||
expect(carryover).toBeNull();
|
||
});
|
||
|
||
it("drops carryover for a supported current-turn counterparty revenue pivot with a new entity", () => {
|
||
const policy = buildPolicy({
|
||
findLastAddressAssistantItem: () => ({
|
||
text: "Documents by previous counterparty",
|
||
debug: {
|
||
execution_lane: "address_query",
|
||
answer_grounding_check: { status: "grounded" },
|
||
detected_intent: "list_documents_by_counterparty",
|
||
extracted_filters: {
|
||
counterparty: "Previous Counterparty",
|
||
organization: "Org Alt"
|
||
},
|
||
anchor_type: "counterparty",
|
||
anchor_value_resolved: "Previous Counterparty"
|
||
}
|
||
}),
|
||
hasAddressFollowupContextSignal: () => true,
|
||
isImplicitAddressContinuationByLlm: () => true,
|
||
resolveAssistantTurnMeaning: () => ({
|
||
schema_version: "assistant_turn_meaning_v1",
|
||
asked_domain_family: "counterparty",
|
||
asked_action_family: "counterparty_value_or_turnover",
|
||
explicit_intent_candidate: "customer_revenue_and_payments",
|
||
intent_override_strength: "explicit_current_turn_intent",
|
||
explicit_entity_candidates: [
|
||
{
|
||
type: "counterparty",
|
||
value: "svk",
|
||
source: "current_turn_loose_entity_tail"
|
||
}
|
||
],
|
||
stale_replay_forbidden: false
|
||
}),
|
||
resolveAddressIntent: () => ({ intent: "customer_revenue_and_payments" }),
|
||
resolveAddressIntentFamily: () => "counterparty"
|
||
});
|
||
|
||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||
"\u043a\u0430\u043a\u043e\u0439 \u043e\u0431\u043e\u0440\u043e\u0442 \u0431\u044b\u043b \u0441\u0432\u043a",
|
||
[],
|
||
null,
|
||
null,
|
||
null
|
||
);
|
||
|
||
expect(carryover).toBeNull();
|
||
});
|
||
|
||
it("drops carryover for broad business evaluation so lifecycle context does not stick to the new question", () => {
|
||
const policy = buildPolicy({
|
||
findLastAddressAssistantItem: () => ({
|
||
text: "Lifecycle answer",
|
||
debug: {
|
||
execution_lane: "address_query",
|
||
answer_grounding_check: { status: "grounded" },
|
||
detected_intent: "counterparty_activity_lifecycle",
|
||
extracted_filters: {
|
||
organization: 'ООО "Альтернатива Плюс"',
|
||
period_to: "2020-12-31"
|
||
},
|
||
anchor_type: "organization",
|
||
anchor_value_resolved: 'ООО "Альтернатива Плюс"'
|
||
}
|
||
}),
|
||
hasAddressFollowupContextSignal: () => true,
|
||
resolveAssistantTurnMeaning: () => ({
|
||
schema_version: "assistant_turn_meaning_v1",
|
||
asked_domain_family: "business_summary",
|
||
asked_action_family: "broad_evaluation",
|
||
explicit_intent_candidate: null,
|
||
unsupported_but_understood_family: "broad_business_evaluation",
|
||
explicit_entity_candidates: [],
|
||
stale_replay_forbidden: true
|
||
})
|
||
});
|
||
|
||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||
"Как ты оценишь деятельность компании?",
|
||
[],
|
||
null,
|
||
null,
|
||
null
|
||
);
|
||
|
||
expect(carryover).toBeNull();
|
||
});
|
||
|
||
it("reuses grounded MCP discovery payout context for a short year-switch follow-up", () => {
|
||
const policy = buildPolicy({
|
||
findLastAddressAssistantItem: () => null,
|
||
hasAddressFollowupContextSignal: () => true
|
||
});
|
||
|
||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||
"а теперь за 2021?",
|
||
[
|
||
{
|
||
role: "assistant",
|
||
text: "Подтверждены исходящие платежи по Группа СВК за 2020 год.",
|
||
debug: {
|
||
execution_lane: "living_chat",
|
||
mcp_discovery_response_applied: true,
|
||
assistant_active_organization: "ООО Альтернатива Плюс",
|
||
assistant_mcp_discovery_entry_point_v1: {
|
||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||
entry_status: "bridge_executed",
|
||
turn_input: {
|
||
turn_meaning_ref: {
|
||
asked_action_family: "payout",
|
||
explicit_entity_candidates: ["Группа СВК"],
|
||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||
explicit_date_scope: "2020"
|
||
}
|
||
},
|
||
bridge: {
|
||
bridge_status: "answer_draft_ready",
|
||
business_fact_answer_allowed: true,
|
||
pilot: {
|
||
pilot_scope: "counterparty_supplier_payout_query_movements_v1"
|
||
},
|
||
answer_draft: {
|
||
answer_mode: "confirmed_with_bounded_inference"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
],
|
||
null,
|
||
null,
|
||
null
|
||
);
|
||
|
||
expect(carryover?.followupSelectionMode).toBe("carry_previous_intent");
|
||
expect(carryover?.followupContext?.previous_intent).toBe("supplier_payouts_profile");
|
||
expect(carryover?.followupContext?.target_intent).toBe("supplier_payouts_profile");
|
||
expect(carryover?.followupContext?.previous_discovery_pilot_scope).toBe(
|
||
"counterparty_supplier_payout_query_movements_v1"
|
||
);
|
||
expect(carryover?.followupContext?.previous_anchor_type).toBe("counterparty");
|
||
expect(carryover?.followupContext?.previous_anchor_value).toBe("Группа СВК");
|
||
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||
counterparty: "Группа СВК",
|
||
organization: "ООО Альтернатива Плюс",
|
||
period_from: "2020-01-01",
|
||
period_to: "2020-12-31"
|
||
});
|
||
});
|
||
|
||
it("carries business overview boundary context through short capability-shaped follow-up", () => {
|
||
const policy = buildPolicy({
|
||
findLastAddressAssistantItem: () => null,
|
||
shouldHandleAsAssistantCapabilityMetaQuery: () => true,
|
||
hasDataRetrievalRequestSignal: () => false,
|
||
hasAddressFollowupContextSignal: () => false
|
||
});
|
||
const organization = "ООО Альтернатива Плюс";
|
||
|
||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||
"то есть просрочку доказать нельзя, коротко почему?",
|
||
[
|
||
{
|
||
role: "assistant",
|
||
text: "На 2020-12-31 подтвержденной просрочки нет: в договорах срок оплаты не установлен.",
|
||
debug: {
|
||
execution_lane: "living_chat",
|
||
mcp_discovery_response_applied: true,
|
||
assistant_active_organization: organization,
|
||
assistant_mcp_discovery_entry_point_v1: {
|
||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||
entry_status: "bridge_executed",
|
||
turn_input: {
|
||
turn_meaning_ref: {
|
||
asked_domain_family: "business_overview",
|
||
asked_action_family: "debt_due_date_boundary",
|
||
unsupported_but_understood_family: "debt_due_date_boundary",
|
||
explicit_organization_scope: organization,
|
||
explicit_date_scope: "2020"
|
||
}
|
||
},
|
||
bridge: {
|
||
bridge_status: "answer_draft_ready",
|
||
business_fact_answer_allowed: true,
|
||
pilot: {
|
||
pilot_scope: "business_overview_route_template_v1"
|
||
},
|
||
answer_draft: {
|
||
answer_mode: "confirmed_with_bounded_inference"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
],
|
||
null,
|
||
null,
|
||
null
|
||
);
|
||
|
||
expect(carryover?.followupSelectionMode).toBe("carry_previous_intent");
|
||
expect(carryover?.followupContext?.previous_discovery_pilot_scope).toBe(
|
||
"business_overview_route_template_v1"
|
||
);
|
||
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||
organization,
|
||
period_from: "2020-01-01",
|
||
period_to: "2020-12-31"
|
||
});
|
||
});
|
||
it("carries resolved entity candidates from grounded entity-resolution discovery into followup context", () => {
|
||
const policy = buildPolicy({
|
||
findLastAddressAssistantItem: () => null,
|
||
hasAddressFollowupContextSignal: () => true
|
||
});
|
||
|
||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||
"по нему документы за 2020 год",
|
||
[
|
||
{
|
||
role: "assistant",
|
||
text: "В текущем каталожном срезе 1С по запросу \"СВК\" найден контрагент \"Группа СВК\".",
|
||
debug: {
|
||
execution_lane: "living_chat",
|
||
mcp_discovery_response_applied: true,
|
||
assistant_mcp_discovery_entry_point_v1: {
|
||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||
entry_status: "bridge_executed",
|
||
turn_input: {
|
||
turn_meaning_ref: {
|
||
asked_domain_family: "entity_resolution",
|
||
asked_action_family: "search_business_entity",
|
||
explicit_entity_candidates: ["СВК"]
|
||
}
|
||
},
|
||
bridge: {
|
||
bridge_status: "answer_draft_ready",
|
||
business_fact_answer_allowed: true,
|
||
pilot: {
|
||
pilot_scope: "entity_resolution_search_v1",
|
||
derived_entity_resolution: {
|
||
requested_entity: "СВК",
|
||
resolution_status: "resolved",
|
||
resolved_entity: "Группа СВК",
|
||
ambiguity_candidates: []
|
||
}
|
||
},
|
||
answer_draft: {
|
||
answer_mode: "confirmed_with_bounded_inference"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
],
|
||
null,
|
||
null,
|
||
null
|
||
);
|
||
|
||
expect(carryover?.followupContext?.previous_discovery_pilot_scope).toBe("entity_resolution_search_v1");
|
||
expect(carryover?.followupContext?.previous_discovery_entity_candidates).toEqual(["Группа СВК", "СВК"]);
|
||
expect(carryover?.followupContext?.previous_anchor_type).toBe("counterparty");
|
||
expect(carryover?.followupContext?.previous_anchor_value).toBe("Группа СВК");
|
||
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||
counterparty: "Группа СВК"
|
||
});
|
||
});
|
||
|
||
it("carries ambiguity candidates from entity-resolution discovery into followup context", () => {
|
||
const policy = buildPolicy({
|
||
findLastAddressAssistantItem: () => null,
|
||
hasAddressFollowupContextSignal: () => true
|
||
});
|
||
|
||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||
"СВК-А",
|
||
[
|
||
{
|
||
role: "assistant",
|
||
text: "По каталогу 1С нашлось несколько похожих контрагентов: СВК-А, СВК-Б.",
|
||
debug: {
|
||
execution_lane: "living_chat",
|
||
mcp_discovery_response_applied: true,
|
||
assistant_mcp_discovery_entry_point_v1: {
|
||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||
entry_status: "bridge_executed",
|
||
turn_input: {
|
||
turn_meaning_ref: {
|
||
asked_domain_family: "entity_resolution",
|
||
asked_action_family: "search_business_entity",
|
||
explicit_entity_candidates: ["СВК"]
|
||
}
|
||
},
|
||
bridge: {
|
||
bridge_status: "answer_draft_ready",
|
||
business_fact_answer_allowed: false,
|
||
pilot: {
|
||
pilot_scope: "entity_resolution_search_v1",
|
||
derived_entity_resolution: {
|
||
requested_entity: "СВК",
|
||
resolution_status: "ambiguous",
|
||
resolved_entity: null,
|
||
ambiguity_candidates: ["СВК-А", "СВК-Б"]
|
||
}
|
||
},
|
||
answer_draft: {
|
||
answer_mode: "needs_clarification"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
],
|
||
null,
|
||
null,
|
||
null
|
||
);
|
||
|
||
expect(carryover?.followupContext?.previous_discovery_pilot_scope).toBe("entity_resolution_search_v1");
|
||
expect(carryover?.followupContext?.previous_discovery_entity_resolution_status).toBe("ambiguous");
|
||
expect(carryover?.followupContext?.previous_discovery_entity_candidates).toEqual(["СВК", "СВК-А", "СВК-Б"]);
|
||
expect(carryover?.followupContext?.previous_discovery_entity_ambiguity_candidates).toEqual([
|
||
"СВК-А",
|
||
"СВК-Б"
|
||
]);
|
||
});
|
||
|
||
it("keeps exact payout carryover for a short net follow-up without restating counterparty or year", () => {
|
||
const policy = buildPolicy({
|
||
findLastAddressAssistantItem: () => ({
|
||
role: "assistant",
|
||
text: "Платежи по Группа СВК за 2021",
|
||
debug: {
|
||
execution_lane: "address_query",
|
||
answer_grounding_check: { status: "grounded" },
|
||
detected_intent: "customer_revenue_and_payments",
|
||
selected_recipe: "address_customer_revenue_and_payments_v1",
|
||
extracted_filters: {
|
||
counterparty: "Группа СВК",
|
||
period_from: "2021-01-01",
|
||
period_to: "2021-12-31"
|
||
},
|
||
anchor_type: "counterparty",
|
||
anchor_value_resolved: "Группа СВК"
|
||
}
|
||
}),
|
||
hasAddressFollowupContextSignal: () => true
|
||
});
|
||
|
||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||
"а какое нетто?",
|
||
[],
|
||
null,
|
||
null,
|
||
null
|
||
);
|
||
|
||
expect(carryover?.followupSelectionMode).toBe("carry_previous_intent");
|
||
expect(carryover?.followupContext?.previous_intent).toBe("customer_revenue_and_payments");
|
||
expect(carryover?.followupContext?.target_intent).toBe("customer_revenue_and_payments");
|
||
expect(carryover?.followupContext?.previous_anchor_type).toBe("counterparty");
|
||
expect(carryover?.followupContext?.previous_anchor_value).toBe("Группа СВК");
|
||
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||
counterparty: "Группа СВК",
|
||
period_from: "2021-01-01",
|
||
period_to: "2021-12-31"
|
||
});
|
||
});
|
||
|
||
it("carries ranking need from grounded discovery into followup context", () => {
|
||
const policy = buildPolicy({
|
||
findLastAddressAssistantItem: () => null,
|
||
hasAddressFollowupContextSignal: () => true
|
||
});
|
||
|
||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||
"по ООО Альтернатива Плюс",
|
||
[
|
||
{
|
||
role: "assistant",
|
||
text: "Нужно уточнить организацию, чтобы продолжить поиск по контрагентам.",
|
||
debug: {
|
||
execution_lane: "living_chat",
|
||
mcp_discovery_response_applied: true,
|
||
assistant_mcp_discovery_entry_point_v1: {
|
||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||
entry_status: "bridge_executed",
|
||
turn_input: {
|
||
data_need_graph: {
|
||
business_fact_family: "value_flow",
|
||
ranking_need: "top_desc",
|
||
subject_candidates: [],
|
||
clarification_gaps: ["organization"]
|
||
},
|
||
turn_meaning_ref: {
|
||
asked_domain_family: "counterparty_value",
|
||
asked_action_family: "turnover",
|
||
explicit_date_scope: "2020",
|
||
seeded_ranking_need: "top_desc"
|
||
}
|
||
},
|
||
bridge: {
|
||
bridge_status: "answer_draft_ready",
|
||
business_fact_answer_allowed: false,
|
||
pilot: {
|
||
pilot_scope: "counterparty_value_flow_query_movements_v1"
|
||
},
|
||
answer_draft: {
|
||
answer_mode: "needs_clarification"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
],
|
||
null,
|
||
null,
|
||
null
|
||
);
|
||
|
||
expect(carryover?.followupContext?.previous_discovery_pilot_scope).toBe(
|
||
"counterparty_value_flow_query_movements_v1"
|
||
);
|
||
expect(carryover?.followupContext?.previous_discovery_ranking_need).toBe("top_desc");
|
||
expect(carryover?.followupContext?.target_intent).toBe("customer_revenue_and_payments");
|
||
});
|
||
|
||
it("carries resumable discovery loop state into followup context", () => {
|
||
const policy = buildPolicy({
|
||
findLastAddressAssistantItem: () => null,
|
||
hasAddressFollowupContextSignal: () => true
|
||
});
|
||
|
||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||
"ООО Альтернатива Плюс",
|
||
[
|
||
{
|
||
role: "assistant",
|
||
text: "Нужно уточнить организацию, чтобы продолжить рейтинг.",
|
||
debug: {
|
||
execution_lane: "living_chat",
|
||
mcp_discovery_response_applied: true,
|
||
assistant_mcp_discovery_entry_point_v1: {
|
||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||
entry_status: "bridge_executed",
|
||
turn_input: {
|
||
data_need_graph: {
|
||
business_fact_family: "value_flow",
|
||
ranking_need: "top_desc",
|
||
subject_candidates: [],
|
||
clarification_gaps: ["organization"]
|
||
},
|
||
turn_meaning_ref: {
|
||
asked_domain_family: "counterparty_value",
|
||
asked_action_family: "turnover",
|
||
explicit_date_scope: "2020",
|
||
seeded_ranking_need: "top_desc"
|
||
}
|
||
},
|
||
bridge: {
|
||
bridge_status: "needs_clarification",
|
||
business_fact_answer_allowed: false,
|
||
pilot: {
|
||
pilot_scope: "counterparty_value_flow_query_movements_v1"
|
||
},
|
||
loop_state: {
|
||
schema_version: "assistant_mcp_discovery_loop_state_v1",
|
||
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
|
||
loop_status: "awaiting_clarification",
|
||
selected_chain_id: "value_flow_ranking",
|
||
pilot_scope: "counterparty_value_flow_query_movements_v1",
|
||
asked_domain_family: "counterparty_value",
|
||
asked_action_family: "turnover",
|
||
unsupported_but_understood_family: "counterparty_value_or_turnover",
|
||
ranking_need: "top_desc",
|
||
pending_axes: ["organization"],
|
||
provided_axes: ["aggregate_axis", "amount", "coverage_target"],
|
||
explicit_entity_candidates: [],
|
||
explicit_organization_scope: null,
|
||
explicit_date_scope: "2020"
|
||
},
|
||
answer_draft: {
|
||
answer_mode: "needs_clarification"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
],
|
||
null,
|
||
null,
|
||
null
|
||
);
|
||
|
||
expect(carryover?.followupContext?.previous_discovery_loop_status).toBe("awaiting_clarification");
|
||
expect(carryover?.followupContext?.previous_discovery_loop_selected_chain_id).toBe("value_flow_ranking");
|
||
expect(carryover?.followupContext?.previous_discovery_loop_pending_axes).toEqual(["organization"]);
|
||
expect(carryover?.followupContext?.previous_discovery_loop_provided_axes).toEqual([
|
||
"aggregate_axis",
|
||
"amount",
|
||
"coverage_target"
|
||
]);
|
||
expect(carryover?.followupContext?.previous_discovery_loop_asked_domain_family).toBe("counterparty_value");
|
||
expect(carryover?.followupContext?.previous_discovery_loop_asked_action_family).toBe("turnover");
|
||
expect(carryover?.followupContext?.previous_discovery_loop_unsupported_family).toBe(
|
||
"counterparty_value_or_turnover"
|
||
);
|
||
});
|
||
|
||
it("treats a plain organization reply as continuation of a pending route-candidate organization scope", () => {
|
||
const orgName =
|
||
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
||
const policy = buildPolicy({
|
||
findLastAddressAssistantItem: () => ({
|
||
role: "assistant",
|
||
text: "\u041d\u0443\u0436\u043d\u043e \u0443\u0442\u043e\u0447\u043d\u0438\u0442\u044c \u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044e.",
|
||
debug: {
|
||
execution_lane: "living_chat",
|
||
detected_intent: "customer_revenue_and_payments",
|
||
mcp_discovery_response_applied: true,
|
||
assistant_mcp_discovery_entry_point_v1: {
|
||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||
entry_status: "bridge_executed",
|
||
bridge: {
|
||
bridge_status: "needs_clarification",
|
||
business_fact_answer_allowed: false,
|
||
pilot: {
|
||
pilot_scope: "counterparty_value_flow_query_movements_v1"
|
||
},
|
||
loop_state: {
|
||
schema_version: "assistant_mcp_discovery_loop_state_v1",
|
||
loop_status: "awaiting_clarification",
|
||
selected_chain_id: "value_flow_ranking",
|
||
pilot_scope: "counterparty_value_flow_query_movements_v1",
|
||
asked_domain_family: "counterparty_value",
|
||
asked_action_family: "turnover",
|
||
unsupported_but_understood_family: "counterparty_value_or_turnover",
|
||
ranking_need: "top_desc",
|
||
pending_axes: ["organization"],
|
||
provided_axes: ["aggregate_axis", "amount", "coverage_target"],
|
||
explicit_date_scope: "2020"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}),
|
||
hasAddressFollowupContextSignal: () => false
|
||
});
|
||
|
||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||
orgName,
|
||
[],
|
||
null,
|
||
{
|
||
predecomposeContract: {
|
||
mode: "unsupported",
|
||
intent: "unknown",
|
||
entities: { organization: orgName },
|
||
semantics: { anchor_kind: "organization", anchor_value: orgName }
|
||
}
|
||
},
|
||
null
|
||
);
|
||
|
||
expect(carryover?.followupContext?.previous_discovery_loop_status).toBe("awaiting_clarification");
|
||
expect(carryover?.followupContext?.previous_discovery_loop_selected_chain_id).toBe("value_flow_ranking");
|
||
expect(carryover?.followupContext?.previous_discovery_loop_pending_axes).toEqual(["organization"]);
|
||
expect(carryover?.followupContext?.target_intent).toBe("customer_revenue_and_payments");
|
||
});
|
||
|
||
it("carries grounded metadata downstream route hints into followup context", () => {
|
||
const policy = buildPolicy({
|
||
findLastAddressAssistantItem: () => null,
|
||
hasAddressFollowupContextSignal: () => true
|
||
});
|
||
|
||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||
"then documents",
|
||
[
|
||
{
|
||
role: "assistant",
|
||
text: "Metadata surface confirmed.",
|
||
debug: {
|
||
execution_lane: "living_chat",
|
||
mcp_discovery_response_applied: true,
|
||
assistant_mcp_discovery_entry_point_v1: {
|
||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||
entry_status: "bridge_executed",
|
||
turn_input: {
|
||
turn_meaning_ref: {
|
||
asked_domain_family: "metadata",
|
||
asked_action_family: "inspect_documents",
|
||
explicit_entity_candidates: ["SVK"],
|
||
explicit_date_scope: "2020"
|
||
}
|
||
},
|
||
bridge: {
|
||
bridge_status: "answer_draft_ready",
|
||
business_fact_answer_allowed: true,
|
||
pilot: {
|
||
pilot_scope: "metadata_inspection_v1",
|
||
derived_metadata_surface: {
|
||
selected_entity_set: "Документ",
|
||
selected_surface_objects: ["Документ.СчетФактураВыданный"],
|
||
downstream_route_family: "document_evidence",
|
||
route_family_selection_basis: "selected_entity_set",
|
||
recommended_next_primitive: "query_documents",
|
||
ambiguity_detected: false
|
||
}
|
||
},
|
||
answer_draft: {
|
||
answer_mode: "confirmed_with_bounded_inference"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
],
|
||
null,
|
||
null,
|
||
null
|
||
);
|
||
|
||
expect(carryover?.followupContext?.previous_discovery_pilot_scope).toBe("metadata_inspection_v1");
|
||
expect(carryover?.followupContext?.previous_discovery_metadata_route_family).toBe("document_evidence");
|
||
expect(carryover?.followupContext?.previous_discovery_metadata_route_family_selection_basis).toBe("selected_entity_set");
|
||
expect(carryover?.followupContext?.previous_discovery_metadata_selected_entity_set).toBe("Документ");
|
||
expect(carryover?.followupContext?.previous_discovery_metadata_selected_surface_objects).toEqual([
|
||
"Документ.СчетФактураВыданный"
|
||
]);
|
||
expect(carryover?.followupContext?.previous_discovery_metadata_recommended_next_primitive).toBe("query_documents");
|
||
expect(carryover?.followupContext?.previous_discovery_metadata_ambiguity_detected).toBeUndefined();
|
||
});
|
||
it("carries metadata ambiguity entity sets into follow-up context for downstream lane arbitration", () => {
|
||
const policy = buildPolicy({
|
||
findLastAddressAssistantItem: () => ({
|
||
text: "metadata ambiguity",
|
||
debug: {
|
||
execution_lane: "living_chat",
|
||
mcp_discovery_response_applied: true,
|
||
assistant_mcp_discovery_entry_point_v1: {
|
||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||
entry_status: "bridge_executed",
|
||
turn_input: {
|
||
turn_meaning_ref: {
|
||
asked_domain_family: "metadata",
|
||
asked_action_family: "inspect_documents",
|
||
explicit_entity_candidates: ["SVK"],
|
||
explicit_date_scope: "2020"
|
||
}
|
||
},
|
||
bridge: {
|
||
bridge_status: "answer_draft_ready",
|
||
business_fact_answer_allowed: true,
|
||
pilot: {
|
||
pilot_scope: "metadata_inspection_v1",
|
||
derived_metadata_surface: {
|
||
selected_entity_set: null,
|
||
downstream_route_family: null,
|
||
ambiguity_detected: true,
|
||
ambiguity_entity_sets: ["Документ", "РегистрНакопления"]
|
||
}
|
||
},
|
||
answer_draft: {
|
||
answer_mode: "confirmed_with_bounded_inference"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}),
|
||
hasAddressFollowupContextSignal: () => true,
|
||
hasReferentialPointer: () => false,
|
||
resolveAddressIntent: () => ({ intent: "unknown" }),
|
||
resolveAddressIntentFamily: () => null,
|
||
resolveAssistantTurnMeaning: () => null
|
||
});
|
||
|
||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||
"по документам",
|
||
[{ kind: "assistant", text: "metadata ambiguity" }],
|
||
"по документам",
|
||
{ predecomposeContract: { intent: "unknown" } },
|
||
null
|
||
);
|
||
|
||
expect(carryover?.followupContext?.previous_discovery_metadata_ambiguity_detected).toBe(true);
|
||
expect(carryover?.followupContext?.previous_discovery_metadata_ambiguity_entity_sets).toEqual([
|
||
"Документ",
|
||
"РегистрНакопления"
|
||
]);
|
||
});
|
||
it("preserves metadata ambiguity choice sets through a clarification assistant turn", () => {
|
||
const policy = buildPolicy({
|
||
findLastAddressAssistantItem: () => ({
|
||
text: "уточните: по документам или по движениям?",
|
||
debug: {
|
||
execution_lane: "living_chat",
|
||
mcp_discovery_response_applied: true,
|
||
assistant_mcp_discovery_entry_point_v1: {
|
||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||
entry_status: "bridge_executed",
|
||
turn_input: {
|
||
turn_meaning_ref: {
|
||
asked_domain_family: "metadata",
|
||
asked_action_family: "resolve_next_lane",
|
||
explicit_entity_candidates: ["SVK"],
|
||
metadata_ambiguity_entity_sets: ["Документ", "РегистрНакопления"],
|
||
explicit_date_scope: "2020",
|
||
unsupported_but_understood_family: "metadata_lane_choice_clarification"
|
||
}
|
||
},
|
||
bridge: {
|
||
bridge_status: "needs_clarification",
|
||
business_fact_answer_allowed: false,
|
||
pilot: {
|
||
pilot_scope: "metadata_inspection_v1"
|
||
},
|
||
answer_draft: {
|
||
answer_mode: "needs_clarification"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}),
|
||
hasAddressFollowupContextSignal: () => true,
|
||
hasReferentialPointer: () => false,
|
||
resolveAddressIntent: () => ({ intent: "unknown" }),
|
||
resolveAddressIntentFamily: () => null,
|
||
resolveAssistantTurnMeaning: () => null
|
||
});
|
||
|
||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||
"по движениям",
|
||
[{ kind: "assistant", text: "уточните: по документам или по движениям?" }],
|
||
"по движениям",
|
||
{ predecomposeContract: { intent: "unknown" } },
|
||
null
|
||
);
|
||
|
||
expect(carryover?.followupContext?.previous_discovery_pilot_scope).toBe("metadata_inspection_v1");
|
||
expect(carryover?.followupContext?.previous_discovery_metadata_ambiguity_detected).toBe(true);
|
||
expect(carryover?.followupContext?.previous_discovery_metadata_ambiguity_entity_sets).toEqual([
|
||
"Документ",
|
||
"РегистрНакопления"
|
||
]);
|
||
});
|
||
it("switches to VAT tax-period intent while preserving carried period filters", () => {
|
||
const policy = buildPolicy({
|
||
findLastAddressAssistantItem: () => ({
|
||
text: "Подтвержденная дебиторская задолженность на 31.05.2017 собрана.",
|
||
debug: {
|
||
detected_intent: "receivables_confirmed_as_of_date",
|
||
extracted_filters: {
|
||
organization: 'ООО "Альтернатива Плюс"',
|
||
as_of_date: "2017-05-31",
|
||
period_from: "2017-05-01",
|
||
period_to: "2017-05-31"
|
||
},
|
||
anchor_type: "organization",
|
||
anchor_value_resolved: 'ООО "Альтернатива Плюс"'
|
||
}
|
||
}),
|
||
hasAddressFollowupContextSignal: () => true,
|
||
hasReferentialPointer: (value: unknown) => /этот период/i.test(String(value ?? "")),
|
||
resolveAddressIntent: () => ({ intent: "unknown" }),
|
||
resolveAddressIntentFamily: (intent: unknown) => {
|
||
if (String(intent ?? "").startsWith("receivables_")) return "receivables";
|
||
if (String(intent ?? "").startsWith("vat_")) return "vat";
|
||
return null;
|
||
},
|
||
resolveAssistantTurnMeaning: () => ({
|
||
schema_version: "assistant_turn_meaning_v1",
|
||
asked_domain_family: "vat",
|
||
asked_action_family: "confirmed_tax_period",
|
||
explicit_intent_candidate: "vat_liability_confirmed_for_tax_period",
|
||
explicit_entity_candidates: [],
|
||
intent_override_strength: "explicit_current_turn_intent",
|
||
stale_replay_forbidden: false
|
||
})
|
||
});
|
||
|
||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||
"а какой ндс мы должны примерно заплатить за этот период?",
|
||
[],
|
||
"Какой НДС должен быть уплачен за текущий период?",
|
||
{
|
||
predecomposeContract: {
|
||
intent: "unknown"
|
||
}
|
||
},
|
||
null
|
||
);
|
||
|
||
expect(carryover?.followupSelectionMode).toBe("carry_previous_intent");
|
||
expect(carryover?.followupContext?.previous_intent).toBe("receivables_confirmed_as_of_date");
|
||
expect(carryover?.followupContext?.target_intent).toBe("vat_liability_confirmed_for_tax_period");
|
||
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||
organization: 'ООО "Альтернатива Плюс"',
|
||
as_of_date: "2017-05-31",
|
||
period_from: "2017-05-01",
|
||
period_to: "2017-05-31"
|
||
});
|
||
});
|
||
it("carries metadata-scoped subjectless loop state through follow-up context", () => {
|
||
const policy = buildPolicy({
|
||
findLastAddressAssistantItem: () => ({
|
||
text: "\u043d\u0443\u0436\u043d\u044b \u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044f \u0438 \u043f\u0435\u0440\u0438\u043e\u0434",
|
||
debug: {
|
||
execution_lane: "living_chat",
|
||
mcp_discovery_response_applied: true,
|
||
assistant_mcp_discovery_entry_point_v1: {
|
||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||
entry_status: "bridge_executed",
|
||
turn_input: {
|
||
turn_meaning_ref: {
|
||
asked_domain_family: "movements",
|
||
asked_action_family: "list_movements",
|
||
metadata_scope_hint: "\u041d\u0414\u0421",
|
||
subject_resolution_optional: true,
|
||
unsupported_but_understood_family: "movement_evidence",
|
||
stale_replay_forbidden: true
|
||
}
|
||
},
|
||
bridge: {
|
||
bridge_status: "needs_clarification",
|
||
business_fact_answer_allowed: false,
|
||
pilot: {
|
||
pilot_scope: "counterparty_movement_evidence_query_movements_v1"
|
||
},
|
||
loop_state: {
|
||
schema_version: "assistant_mcp_discovery_loop_state_v1",
|
||
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
|
||
loop_status: "awaiting_clarification",
|
||
selected_chain_id: "movement_evidence",
|
||
pilot_scope: "counterparty_movement_evidence_query_movements_v1",
|
||
asked_domain_family: "movements",
|
||
asked_action_family: "list_movements",
|
||
unsupported_but_understood_family: "movement_evidence",
|
||
ranking_need: null,
|
||
pending_axes: ["organization", "period"],
|
||
provided_axes: [],
|
||
explicit_entity_candidates: [],
|
||
metadata_scope_hint: "\u041d\u0414\u0421",
|
||
subject_resolution_optional: true,
|
||
explicit_organization_scope: null,
|
||
explicit_date_scope: null
|
||
},
|
||
answer_draft: {
|
||
answer_mode: "needs_clarification"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}),
|
||
hasAddressFollowupContextSignal: () => true,
|
||
hasReferentialPointer: () => false,
|
||
resolveAddressIntent: () => ({ intent: "unknown" }),
|
||
resolveAddressIntentFamily: () => null,
|
||
resolveAssistantTurnMeaning: () => null
|
||
});
|
||
|
||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||
"\u043f\u043e \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
|
||
[{ kind: "assistant", text: "\u043d\u0443\u0436\u043d\u044b \u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044f \u0438 \u043f\u0435\u0440\u0438\u043e\u0434" }],
|
||
"\u043f\u043e \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
|
||
{ predecomposeContract: { intent: "unknown" } },
|
||
null
|
||
);
|
||
|
||
expect(carryover?.followupContext?.previous_discovery_loop_selected_chain_id).toBe("movement_evidence");
|
||
expect(carryover?.followupContext?.previous_discovery_loop_pending_axes).toEqual([
|
||
"organization",
|
||
"period"
|
||
]);
|
||
expect(carryover?.followupContext?.previous_discovery_loop_metadata_scope_hint).toBe(
|
||
"\u041d\u0414\u0421"
|
||
);
|
||
expect(carryover?.followupContext?.previous_discovery_loop_subject_resolution_optional).toBe(true);
|
||
});
|
||
it("does not backfill metadata scope into counterparty carryover during lane choice follow-up", () => {
|
||
const policy = buildPolicy({
|
||
findLastAddressAssistantItem: () => ({
|
||
text: "\u0443\u0442\u043e\u0447\u043d\u0438\u0442\u0435: \u043f\u043e \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u043c \u0438\u043b\u0438 \u043f\u043e \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f\u043c?",
|
||
debug: {
|
||
execution_lane: "living_chat",
|
||
mcp_discovery_response_applied: true,
|
||
assistant_mcp_discovery_entry_point_v1: {
|
||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||
entry_status: "bridge_executed",
|
||
turn_input: {
|
||
turn_meaning_ref: {
|
||
asked_domain_family: "metadata",
|
||
asked_action_family: "resolve_next_lane",
|
||
explicit_entity_candidates: ["\u041d\u0414\u0421"],
|
||
metadata_scope_hint: "\u041d\u0414\u0421",
|
||
metadata_ambiguity_entity_sets: [
|
||
"\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442",
|
||
"\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u041d\u0430\u043a\u043e\u043f\u043b\u0435\u043d\u0438\u044f"
|
||
],
|
||
unsupported_but_understood_family: "metadata_lane_choice_clarification",
|
||
stale_replay_forbidden: true
|
||
}
|
||
},
|
||
bridge: {
|
||
bridge_status: "needs_clarification",
|
||
business_fact_answer_allowed: false,
|
||
pilot: {
|
||
pilot_scope: "metadata_inspection_v1"
|
||
},
|
||
loop_state: {
|
||
schema_version: "assistant_mcp_discovery_loop_state_v1",
|
||
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
|
||
loop_status: "awaiting_clarification",
|
||
selected_chain_id: "metadata_lane_clarification",
|
||
pilot_scope: "metadata_inspection_v1",
|
||
asked_domain_family: "metadata",
|
||
asked_action_family: "resolve_next_lane",
|
||
unsupported_but_understood_family: "metadata_lane_choice_clarification",
|
||
ranking_need: null,
|
||
pending_axes: ["lane_family_choice"],
|
||
provided_axes: [],
|
||
explicit_entity_candidates: ["\u041d\u0414\u0421"],
|
||
metadata_scope_hint: "\u041d\u0414\u0421",
|
||
subject_resolution_optional: false,
|
||
explicit_organization_scope: null,
|
||
explicit_date_scope: null
|
||
},
|
||
answer_draft: {
|
||
answer_mode: "needs_clarification"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}),
|
||
hasAddressFollowupContextSignal: () => true,
|
||
hasReferentialPointer: () => false,
|
||
resolveAddressIntent: () => ({ intent: "unknown" }),
|
||
resolveAddressIntentFamily: () => null,
|
||
resolveAssistantTurnMeaning: () => null
|
||
});
|
||
|
||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||
"\u043f\u043e \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f\u043c",
|
||
[{ kind: "assistant", text: "\u0443\u0442\u043e\u0447\u043d\u0438\u0442\u0435: \u043f\u043e \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u043c \u0438\u043b\u0438 \u043f\u043e \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f\u043c?" }],
|
||
"\u043f\u043e \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f\u043c",
|
||
{ predecomposeContract: { intent: "unknown" } },
|
||
null
|
||
);
|
||
|
||
expect(carryover?.followupContext?.previous_filters?.counterparty).toBeUndefined();
|
||
expect(carryover?.followupContext?.previous_anchor_type).toBeUndefined();
|
||
expect(carryover?.followupContext?.previous_anchor_value).toBeNull();
|
||
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"
|
||
});
|
||
});
|
||
});
|