Починить Hm-прогон ассистента по НДС, остаткам и MCP-ответам

This commit is contained in:
2026-05-25 22:51:15 +03:00
parent 36f7b4df03
commit c577ac6b00
37 changed files with 2066 additions and 371 deletions
@@ -244,7 +244,12 @@ describe("address follow-up temporal regressions", () => {
organization: 'ООО "Альтернатива Плюс"',
period_from: "2015-02-01",
period_to: "2015-02-28",
as_of_date: "2016-03-31"
as_of_date: "2015-02-28",
purchase_date_bridge_selected: "2015-02-05",
purchase_date_bridge_first: "2015-02-05",
purchase_date_bridge_last: "2015-07-22",
purchase_date_bridge_basis: "first_confirmed_purchase_default",
purchase_date_bridge_has_multiple: true
},
previous_anchor_type: "item",
previous_anchor_value: "Рабочая станция универсального специалиста"
@@ -254,7 +259,98 @@ describe("address follow-up temporal regressions", () => {
expect(result?.intent.intent).toBe("vat_liability_confirmed_for_tax_period");
expect(result?.filters.extracted_filters.period_from).toBe("2015-02-01");
expect(result?.filters.extracted_filters.period_to).toBe("2015-02-28");
expect(result?.baseReasons).toContain("period_from_followup_context");
expect(result?.filters.extracted_filters.as_of_date).toBe("2015-02-28");
expect(result?.filters.extracted_filters.purchase_date_bridge_selected).toBe("2015-02-05");
expect(result?.filters.extracted_filters.purchase_date_bridge_has_multiple).toBe(true);
expect(result?.baseReasons).toContain("period_from_purchase_date_bridge_followup_context");
expect(result?.baseReasons).toContain("purchase_date_bridge_from_followup_context");
});
it("does not reuse purchase-date VAT bridge when the next VAT question has an explicit period", () => {
const result = runAddressDecomposeStage("а какой ндс мы должны сгрузить на март 2020?", {
previous_intent: "vat_liability_confirmed_for_tax_period",
target_intent: "vat_liability_confirmed_for_tax_period",
previous_filters: {
item: "Рабочая станция универсального специалиста",
organization: 'ООО "Альтернатива Плюс"',
period_from: "2015-02-01",
period_to: "2015-02-28",
as_of_date: "2015-02-28",
purchase_date_bridge_selected: "2015-02-05",
purchase_date_bridge_first: "2015-02-05",
purchase_date_bridge_last: "2015-07-22",
purchase_date_bridge_basis: "first_confirmed_purchase_default",
purchase_date_bridge_has_multiple: true
},
previous_anchor_type: "organization",
previous_anchor_value: 'ООО "Альтернатива Плюс"'
});
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("vat_liability_confirmed_for_tax_period");
expect(result?.filters.extracted_filters.period_from).toBe("2020-03-01");
expect(result?.filters.extracted_filters.period_to).toBe("2020-03-31");
expect(result?.filters.extracted_filters.as_of_date).toBeUndefined();
expect(result?.filters.extracted_filters.purchase_date_bridge_selected).toBeUndefined();
expect(result?.baseReasons).toContain("purchase_date_bridge_suppressed_by_explicit_temporal_scope");
expect(result?.baseReasons).not.toContain("period_from_purchase_date_bridge_followup_context");
});
it("keeps first confirmed purchase VAT recalculation in the VAT lane without repeating VAT wording", () => {
const result = runAddressDecomposeStage("если у нее несколько закупок, посчитай по первой подтвержденной покупке", {
previous_intent: "vat_liability_confirmed_for_tax_period",
target_intent: "vat_liability_confirmed_for_tax_period",
previous_filters: {
organization: 'ООО "Альтернатива Плюс"',
period_from: "2015-02-01",
period_to: "2015-02-28",
as_of_date: "2015-02-28",
purchase_date_bridge_selected: "2015-02-05",
purchase_date_bridge_first: "2015-02-05",
purchase_date_bridge_last: "2015-07-22",
purchase_date_bridge_basis: "first_confirmed_purchase_default",
purchase_date_bridge_has_multiple: true
},
previous_anchor_type: "organization",
previous_anchor_value: 'ООО "Альтернатива Плюс"'
});
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("vat_liability_confirmed_for_tax_period");
expect(result?.filters.extracted_filters.purchase_date_bridge_selected).toBe("2015-02-05");
expect(result?.filters.extracted_filters.purchase_date_bridge_basis).toBe("first_confirmed_purchase");
expect(result?.filters.extracted_filters.period_from).toBe("2015-02-01");
expect(result?.filters.extracted_filters.period_to).toBe("2015-02-28");
expect(result?.baseReasons).toContain("intent_adjusted_to_inventory_purchase_date_vat_bridge");
expect(result?.baseReasons).toContain("purchase_date_bridge_from_followup_context");
});
it("keeps last confirmed purchase VAT recalculation and moves the tax period to the last purchase month", () => {
const result = runAddressDecomposeStage("а теперь по последней подтвержденной покупке", {
previous_intent: "vat_liability_confirmed_for_tax_period",
target_intent: "vat_liability_confirmed_for_tax_period",
previous_filters: {
organization: 'ООО "Альтернатива Плюс"',
period_from: "2015-02-01",
period_to: "2015-02-28",
as_of_date: "2015-02-28",
purchase_date_bridge_selected: "2015-02-05",
purchase_date_bridge_first: "2015-02-05",
purchase_date_bridge_last: "2015-07-22",
purchase_date_bridge_basis: "first_confirmed_purchase",
purchase_date_bridge_has_multiple: true
},
previous_anchor_type: "organization",
previous_anchor_value: 'ООО "Альтернатива Плюс"'
});
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("vat_liability_confirmed_for_tax_period");
expect(result?.filters.extracted_filters.purchase_date_bridge_selected).toBe("2015-07-22");
expect(result?.filters.extracted_filters.purchase_date_bridge_basis).toBe("last_confirmed_purchase");
expect(result?.filters.extracted_filters.period_from).toBe("2015-07-01");
expect(result?.filters.extracted_filters.period_to).toBe("2015-07-31");
expect(result?.filters.extracted_filters.as_of_date).toBe("2015-07-31");
});
it("clears counterparty noise from bare organization clarification selections that resume inventory", () => {
const result = runAddressDecomposeStage("АЛЬТЕРНАТИВА", {
@@ -4,6 +4,44 @@ import { runAddressDecomposeStage } from "../src/services/address_runtime/decomp
import { composeFactualReply } from "../src/services/address_runtime/composeStage";
describe("inventory aging follow-up", () => {
it("clears organization alias item on root old-stock questions with LLM organization semantics", () => {
const result = runAddressDecomposeStage(
'Есть ли у ООО "Альтернатива Плюс" остатки товара, которые закупались очень давно',
null,
{
scope_target_kind: "organization",
scope_target_text: 'ООО "Альтернатива Плюс"',
date_scope_kind: "implicit_current",
self_scope_detected: false,
selected_object_scope_detected: false
}
);
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("inventory_aging_by_purchase_date");
expect(result?.filters.extracted_filters.organization).toBe('ООО "Альтернатива Плюс"');
expect(result?.filters.extracted_filters.item).toBeUndefined();
expect(result?.baseReasons).toContain("item_cleared_as_organization_scope_alias_for_stock_aging");
});
it("does not mistake an explicit organization name for an inventory item in old-stock questions", () => {
const result = runAddressDecomposeStage('Есть ли у ООО "Альтернатива Плюс" остатки товара, которые закупались очень давно', {
previous_intent: "inventory_aging_by_purchase_date",
previous_filters: {
organization: 'ООО "Альтернатива Плюс"',
as_of_date: "2026-05-25"
},
previous_anchor_type: "organization",
previous_anchor_value: 'ООО "Альтернатива Плюс"'
});
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("inventory_aging_by_purchase_date");
expect(result?.filters.extracted_filters.organization).toBe('ООО "Альтернатива Плюс"');
expect(result?.filters.extracted_filters.item).toBeUndefined();
expect(result?.baseReasons).toContain("item_cleared_as_organization_scope_alias_for_stock_aging");
});
it("keeps carried date window for 'на эту дату' aging follow-up", () => {
const result = runAddressDecomposeStage("Какие остатки по товарам на эту дату относятся к старым закупкам", {
previous_intent: "inventory_purchase_provenance_for_item",
@@ -2182,6 +2182,49 @@ describe("address compose stage utf8 headers", () => {
expect(reply.text).toContain("книг продаж/покупок");
});
it("discloses ambiguous inventory purchase-date VAT bridge instead of hiding the selected date", () => {
const reply = composeFactualReply(
"vat_liability_confirmed_for_tax_period",
[
{
period: "2015-02-28T23:59:59Z",
registrator: "VAT_BOOK_SALES",
account_dt: "68.02",
account_kt: "",
amount: 3500000,
analytics: []
},
{
period: "2015-02-28T23:59:59Z",
registrator: "VAT_BOOK_PURCHASES",
account_dt: "19",
account_kt: "",
amount: 868612,
analytics: []
}
],
{
userMessage: "ндс можешь прикинуть на дату покупки рабочей станции?",
periodFrom: "2015-02-01",
periodTo: "2015-02-28",
asOfDate: "2015-02-28",
organizationHint: "ООО Альтернатива Плюс",
purchaseDateBridge: {
selectedPurchaseDate: "2015-02-05",
firstPurchaseDate: "2015-02-05",
lastPurchaseDate: "2015-07-22",
basis: "first_confirmed_purchase_default",
hasMultiplePurchaseDates: true
},
useRubCurrency: true
}
);
expect(reply.text).toContain("если брать первую подтвержденную дату закупки 05.02.2015");
expect(reply.text).toContain("у позиции несколько подтвержденных дат закупки");
expect(reply.text).toContain("05.02.2015..22.07.2015");
});
it("formats VAT forecast amounts in rubles and emphasizes numbers when requested", () => {
const reply = composeFactualReply(
"vat_payable_forecast",
@@ -5741,6 +5784,24 @@ it("routes old purchase residue questions to aging-by-purchase-date", () => {
expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Остатки.Счет.Код, \"\"), 1, 5) = \"41.01\"");
});
it("builds old-stock aging query from positive stock snapshot plus purchase history", () => {
const selected = selectAddressRecipe("inventory_aging_by_purchase_date", {
organization: 'ООО "Альтернатива Плюс"',
as_of_date: "2026-04-18"
});
expect(selected.selected_recipe?.recipe_id).toBe("address_inventory_aging_by_purchase_date_v1");
const plan = buildAddressRecipePlan(selected.selected_recipe!, {
organization: 'ООО "Альтернатива Плюс"',
as_of_date: "2026-04-18"
});
expect(plan.query).toContain("РегистрБухгалтерии.Хозрасчетный.Остатки(");
expect(plan.query).toContain("Документ.ПоступлениеТоваровУслуг.Товары");
expect(plan.query).toContain("Остатки.КоличествоРазвернутыйОстатокДт > 0");
expect(plan.query).toContain("Товары.Номенклатура В");
expect(plan.query).toContain("Товары.Ссылка.Дата <= ДАТАВРЕМЯ(2026, 4, 18, 23, 59, 59)");
});
it("renders confirmed inventory-on-hand snapshot from normalized rows", () => {
const reply = composeFactualReply(
"inventory_on_hand_as_of_date",
@@ -5776,6 +5837,59 @@ it("routes old purchase residue questions to aging-by-purchase-date", () => {
expect(reply.semantics?.balance_confirmed).toBe(true);
});
it("renders old-stock aging only for items that are still in positive stock", () => {
const reply = composeFactualReply(
"inventory_aging_by_purchase_date",
[
{
period: "2015-02-05T00:00:00Z",
registrator: "Поступление товаров и услуг 00000000001 от 05.02.2015",
account_dt: "41.01",
account_kt: "",
amount: 1000,
analytics: ["Старый проданный товар", 'ООО "Альтернатива Плюс"', "Поставщик А"],
item: "Старый проданный товар",
organization: 'ООО "Альтернатива Плюс"',
counterparty: "Поставщик А",
quantity: 1
},
{
period: "2017-03-10T00:00:00Z",
registrator: "Поступление товаров и услуг 00000000002 от 10.03.2017",
account_dt: "41.01",
account_kt: "",
amount: 2500,
analytics: ["Товар в остатке", 'ООО "Альтернатива Плюс"', "Поставщик Б"],
item: "Товар в остатке",
organization: 'ООО "Альтернатива Плюс"',
counterparty: "Поставщик Б",
quantity: 2
},
{
period: "2026-04-18T23:59:59Z",
registrator: "Остатки на дату",
account_dt: "41.01",
account_kt: "",
amount: 2500,
analytics: ["Товар в остатке", "Основной склад", 'ООО "Альтернатива Плюс"'],
item: "Товар в остатке",
warehouse: "Основной склад",
organization: 'ООО "Альтернатива Плюс"',
quantity: 2
}
],
{
asOfDate: "2026-04-18",
useRubCurrency: true
}
);
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
expect(reply.text.split("\n")[0]).toContain("Товар в остатке");
expect(reply.text).toContain("положительным остатком");
expect(reply.text).not.toContain("Старый проданный товар");
});
it("sorts inventory-on-hand positions by amount before rendering the top list", () => {
const reply = composeFactualReply(
"inventory_on_hand_as_of_date",
@@ -270,7 +270,7 @@ describe("address reply builders regressions", () => {
);
const firstLine = result?.text.split("\n")[0] ?? "";
expect(firstLine).toContain("К самым старым закупкам");
expect(firstLine).toContain("фактических положительных остатков");
expect(firstLine).not.toContain("2026-04-18");
});
@@ -245,4 +245,90 @@ describe("assistant deep turn response runtime adapter", () => {
})
);
});
it("sanitizes guarded MCP discovery candidates before finalizing the visible reply", () => {
const runPackagingRuntime = vi.fn(() => ({
messageId: "msg-a1",
investigationStateSnapshot: null,
droppedIntentSegments: [],
analysisContextForContract: null,
routesForDebug: [],
resolvedExecutionState: [],
safeAssistantReplyBase: "bad-base",
safeAssistantReply: "bad deep partial answer",
debug: { trace_id: "trace-1" },
assistantItem: {
message_id: "msg-a1",
session_id: "asst-1",
role: "assistant",
text: "bad deep partial answer",
reply_type: "partial_coverage",
created_at: "2026-04-10T00:00:00.000Z",
trace_id: "trace-1",
debug: null
},
deepAnalysisLogDetails: {}
}));
const sanitizeReply = vi.fn((value: string) =>
value.replace(/^\u041a\u043e\u0440\u043e\u0442\u043a\u043e:\s*/u, "")
);
const runFinalizeDeepTurn = vi.fn((input) => ({
response: {
ok: true,
session_id: "asst-1",
assistant_reply: input.assistantReply,
reply_type: input.replyType,
conversation_item: input.assistantItem,
conversation: [],
debug: input.debug
}
}));
const runtime = runAssistantDeepTurnResponseRuntime(
buildBaseInput({
composition: { reply_type: "partial_coverage" },
sanitizeReply,
addressRuntimeMetaForDeep: {
mcpDiscoveryRuntimeEntryPoint: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: "bridge_executed",
hot_runtime_wired: false,
discovery_attempted: true,
turn_input: { should_run_discovery: true },
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "Коротко: Discovery answer",
confirmed_lines: ["Confirmed fact"],
inference_lines: [],
unknown_lines: [],
limitation_lines: [],
next_step_line: null
}
},
reason_codes: []
}
},
runPackagingRuntime,
runFinalizeDeepTurn
})
);
expect(sanitizeReply).toHaveBeenCalledWith(expect.not.stringContaining("Коротко:"), "bad deep partial answer");
expect(runtime.response.assistant_reply).toContain("Discovery answer");
expect(runtime.response.assistant_reply).not.toContain("Коротко:");
expect(runFinalizeDeepTurn).toHaveBeenCalledWith(
expect.objectContaining({
assistantReply: expect.not.stringContaining("Коротко:"),
assistantItem: expect.objectContaining({
text: expect.not.stringContaining("Коротко:")
})
})
);
});
});
@@ -48,6 +48,37 @@ describe("assistant MCP discovery response policy", () => {
expect(result.reason_codes).toContain("mcp_discovery_response_policy_candidate_applied");
});
it("removes routine short-answer prefix from applied guarded candidates", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "route is not wired",
currentReplySource: "deterministic_unsupported_current_turn_boundary",
modeDecisionReason: "unsupported_current_turn_meaning_boundary",
addressRuntimeMeta: {
mcpDiscoveryRuntimeEntryPoint: entryPoint({
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "Коротко: Confirmed scoped answer.",
confirmed_lines: ["Коротко: Confirmed fact"],
inference_lines: [],
unknown_lines: [],
limitation_lines: [],
next_step_line: null
}
}
})
}
});
expect(result.applied).toBe(true);
expect(result.reply_text).toContain("Confirmed scoped answer.");
expect(result.reply_text).not.toContain("Коротко:");
});
it("keeps the current reply when the turn is not an unsupported boundary", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "regular chat",
@@ -214,6 +214,152 @@ describe("assistantTransitionPolicy", () => {
expect(contract.decision).toBe("continue_previous");
});
it("carries explicit purchase-date VAT bridge facts without silently losing ambiguity", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({
role: "assistant",
text: [
"По позиции Рабочая станция до 31.03.2016 однозначный поставщик не подтвержден.",
"- Первая найденная дата закупки: 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: "Рабочая станция"
}
})
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
"ндс можешь прикинуть на дату покупки рабочей станции?",
[],
null,
null,
null
);
expect(carryover?.followupContext?.target_intent).toBe("vat_liability_confirmed_for_tax_period");
expect(carryover?.followupContext?.previous_filters).toMatchObject({
period_from: "2015-02-01",
period_to: "2015-02-28",
as_of_date: "2015-02-28",
purchase_date_bridge_selected: "2015-02-05",
purchase_date_bridge_first: "2015-02-05",
purchase_date_bridge_last: "2015-07-22",
purchase_date_bridge_basis: "first_confirmed_purchase_default",
purchase_date_bridge_has_multiple: true
});
});
it("can retarget a follow-up VAT recalculation to the last confirmed purchase date", () => {
const items = [
{
role: "assistant",
text: [
"По позиции Рабочая станция до 31.03.2016 однозначный поставщик не подтвержден.",
"- Первая найденная дата закупки: 05.02.2015.",
"- Последняя найденная дата закупки: 22.07.2015."
].join("\n"),
debug: {
detected_intent: "inventory_purchase_provenance_for_item",
extracted_filters: {
item: "Рабочая станция",
organization: 'ООО "Альтернатива Плюс"'
}
}
},
{
role: "assistant",
text: "Коротко: если брать первую подтвержденную дату закупки 05.02.2015, НДС к уплате 100.",
debug: {
detected_intent: "vat_liability_confirmed_for_tax_period",
extracted_filters: {
item: "Рабочая станция",
organization: 'ООО "Альтернатива Плюс"',
purchase_date_bridge_selected: "2015-02-05"
}
}
}
];
const policy = buildPolicy({
findLastAddressAssistantItem: (sourceItems: unknown[]) => {
const list = sourceItems as Record<string, unknown>[];
return list[list.length - 1];
}
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
"а теперь по последней подтвержденной покупке",
items,
null,
null,
{
session_context: {
active_focus_object: {
object_type: "item",
label: "Рабочая станция"
}
}
}
);
expect(carryover?.followupContext?.target_intent).toBe("vat_liability_confirmed_for_tax_period");
expect(carryover?.followupContext?.previous_filters).toMatchObject({
period_from: "2015-07-01",
period_to: "2015-07-31",
as_of_date: "2015-07-31",
purchase_date_bridge_selected: "2015-07-22",
purchase_date_bridge_basis: "last_confirmed_purchase"
});
});
it("continues purchase-date VAT recalculation from prior VAT bridge filters without relying on active item focus", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({
role: "assistant",
text: "Коротко: если брать первую подтвержденную дату закупки 05.02.2015, НДС к уплате 100.",
debug: {
detected_intent: "vat_liability_confirmed_for_tax_period",
extracted_filters: {
organization: 'ООО "Альтернатива Плюс"',
period_from: "2015-02-01",
period_to: "2015-02-28",
as_of_date: "2015-02-28",
purchase_date_bridge_selected: "2015-02-05",
purchase_date_bridge_first: "2015-02-05",
purchase_date_bridge_last: "2015-07-22",
purchase_date_bridge_basis: "first_confirmed_purchase_default",
purchase_date_bridge_has_multiple: true
}
}
})
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
"если у нее несколько закупок, посчитай по первой подтвержденной покупке",
[],
null,
null,
null
);
expect(carryover?.followupContext?.target_intent).toBe("vat_liability_confirmed_for_tax_period");
expect(carryover?.followupContext?.previous_filters).toMatchObject({
period_from: "2015-02-01",
period_to: "2015-02-28",
as_of_date: "2015-02-28",
purchase_date_bridge_selected: "2015-02-05",
purchase_date_bridge_basis: "first_confirmed_purchase",
purchase_date_bridge_has_multiple: true
});
});
it("retargets displayed counterparty follow-up through shared referenced-entity carryover", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({