524 lines
20 KiB
TypeScript
524 lines
20 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
||
import { createAssistantTransitionPolicy } from "../src/services/assistantTransitionPolicy";
|
||
import { buildRootScopedCarryoverFiltersForTests } from "../src/services/assistantService";
|
||
|
||
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,
|
||
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 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("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("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("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 = buildRootScopedCarryoverFiltersForTests(
|
||
{
|
||
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"
|
||
});
|
||
});
|
||
});
|