From 53e9151d5a35d6457e37c8c355dccac8be178f76 Mon Sep 17 00:00:00 2001 From: dctouch Date: Sat, 18 Apr 2026 16:09:55 +0300 Subject: [PATCH] =?UTF-8?q?=D0=90=D0=A0=D0=A7=20=D0=90=D0=9F11=20-=20?= =?UTF-8?q?=D0=90=D1=80=D1=85=D0=B8=D1=82=D0=B5=D0=BA=D1=82=D1=83=D1=80?= =?UTF-8?q?=D0=B0=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=20=D1=80=D0=B5=D0=B3?= =?UTF-8?q?=D1=80=D0=B5=D1=81=D1=81=D0=B0:=20=D0=90=D1=80=D1=85=D0=B8?= =?UTF-8?q?=D1=82=D0=B5=D0=BA=D1=82=D1=83=D1=80=D0=B0:=20=D1=81=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D0=B0=D1=82=D1=8C=20confirmed=20payables=20=D0=B8?= =?UTF-8?q?=20receivables=20snapshots=20business-first=20=D0=B1=D0=B5?= =?UTF-8?q?=D0=B7=20=D0=BD=D1=83=D0=BC=D0=B5=D1=80=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=BD=D0=BE=D0=B3=D0=BE=20report=20framing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ontinuity_stabilization_plan_2026-04-17.md | 11 +- .../services/address_runtime/composeStage.js | 90 +++++++------- .../services/address_runtime/composeStage.ts | 90 +++++++------- .../tests/addressQueryRuntimeM23.test.ts | 101 +++++++++++++--- ..._saved_session_runtime_job-mmTvku3mtK.json | 114 ++++++++++++++++++ 5 files changed, 287 insertions(+), 119 deletions(-) create mode 100644 llm_normalizer/data/eval_cases/assistant_saved_session_runtime_job-mmTvku3mtK.json diff --git a/docs/ARCH/11 - architecture_turnaround/11 - continuity_stabilization_plan_2026-04-17.md b/docs/ARCH/11 - architecture_turnaround/11 - continuity_stabilization_plan_2026-04-17.md index 28abadc..716a01a 100644 --- a/docs/ARCH/11 - architecture_turnaround/11 - continuity_stabilization_plan_2026-04-17.md +++ b/docs/ARCH/11 - architecture_turnaround/11 - continuity_stabilization_plan_2026-04-17.md @@ -260,7 +260,16 @@ Latest continuity-authority convergence evidence after the current route pass: - `list_contracts_by_counterparty`, `list_documents_by_contract`, `bank_operations_by_counterparty`, `bank_operations_by_contract`, and the generic factual-list fallback no longer leak `live address lane / catalog address lane` wording into the user-facing answer; - these list replies now start with direct business-first leads and keep the selected rows below, which preserves factual usefulness without exposing internal routing labels; - targeted utf8 header tests now explicitly protect against `lane` leakage in these list families; - - this is still not the end of shaping work: some long evidence-heavy replies and residual catalog-style blocks still need the same cleanup; +- the next human-answer-shaping cleanup pass is now applied to open-contract evidence-heavy replies: + - `list_open_contracts` and `open_contracts_confirmed_as_of_date` no longer open with numbered `Блок 1/2/3...` report framing and now start with direct business-first summaries; + - section headings are still structured, but they now read like user-facing guidance instead of an internal audit report, while keeping the same factual slices and evidence detail below; + - targeted open-contract tests now protect the no-`Блок 1` top-block shape, so future contour work cannot silently bring the report framing back; +- the next human-answer-shaping cleanup pass is now applied to confirmed debt snapshots: + - `payables_confirmed_as_of_date` and `receivables_confirmed_as_of_date` now open with business-first `Коротко: ...` summaries instead of numbered report framing; + - debt snapshot sections now keep the same factual structure, but top-level headings are user-facing (`Что учтено`, `Сводка`, `Категории...`) rather than `Блок 1/2/3...`; + - direct compose tests now protect the no-`Блок 1` top-block shape for both confirmed debt families; + - isolated runtime proof for the `payables_confirmed_as_of_date` `tryHandle` path still needs a wider rerun, because the narrow harness invocation currently returns `undefined` before semantic assertions and therefore is not reliable evidence for this shaping pass by itself; + - this is still not the end of shaping work: heuristic debt shortlists and some residual catalog-style blocks still need the same cleanup; - this pass does not yet finish full single-owner continuity, but it narrows one of the remaining seams where route arbitration and scope memory could disagree about whether the session was still grounded. ## Next Execution Slice (2026-04-18) diff --git a/llm_normalizer/backend/dist/services/address_runtime/composeStage.js b/llm_normalizer/backend/dist/services/address_runtime/composeStage.js index 36c4669..da06a5f 100644 --- a/llm_normalizer/backend/dist/services/address_runtime/composeStage.js +++ b/llm_normalizer/backend/dist/services/address_runtime/composeStage.js @@ -2774,20 +2774,19 @@ function composeFactualReplyBody(intent, rows, options = {}) { return `${index + 1}. ${item.contract} | контрагент: ${counterpartyLabel} | подтвержденный открытый остаток: ${formatMoneyRub(item.confirmedAmount)} | тип остатка: ${openContractSettlementKindLabel(item.settlementKind)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${accountsLabel}${evidenceLabel}${refsLabel}${specialReasonLabel}`; }); const lines = [ - `Собран подтвержденный срез открытых договоров на ${formatDateRu(asOfDate)}.`, - `Чистый коммерческий остаток: ${openContractNetBalanceDirectionLabel(commercialNetTotal)} ${formatMoneyRub(Math.abs(commercialNetTotal))}.`, - `Брутто коммерческих компонентов: ${formatMoneyRub(commercialGrossTotal)}.`, - `Специальные финансовые позиции: ${formatNumberWithDots(specialProfiles.length)} на ${formatMoneyRub(specialTotal)}.`, - `Спорные/некачественно нормализованные позиции: ${formatNumberWithDots(dirtyProfiles.length)} на ${formatMoneyRub(dirtyTotal)}.`, + `Коротко: на ${formatDateRu(asOfDate)} подтверждено открытых договоров с коммерческим остатком ${openContractNetBalanceDirectionLabel(commercialNetTotal)} ${formatMoneyRub(Math.abs(commercialNetTotal))}.`, + `Брутто по коммерческим компонентам: ${formatMoneyRub(commercialGrossTotal)}.`, + `Отдельно вынесены специальные финансовые позиции: ${formatNumberWithDots(specialProfiles.length)} на ${formatMoneyRub(specialTotal)}.`, + `Спорные или некачественно нормализованные позиции: ${formatNumberWithDots(dirtyProfiles.length)} на ${formatMoneyRub(dirtyTotal)}.`, "", - "Блок 1. Статус результата", - "- Результат: подтвержденный срез договоров с открытыми взаиморасчетами на дату.", - "- База ответа: остатки по счетам 60/62/76 с договорной аналитикой, без эвристического shortlist.", - "- Управленческий вид: по каждому договору показаны чистый остаток и состав по типам открытых расчетов.", - "- Базовая единица детализации: одна строка = один договор, один контрагент и один тип открытого остатка." + "Что это значит", + "- Это подтвержденный срез договоров с открытыми взаиморасчетами на дату.", + "- Основа ответа: остатки по счетам 60/62/76 с договорной аналитикой, без эвристического shortlist.", + "- По каждому договору показаны чистый остаток и состав по типам открытых расчетов.", + "- Одна строка = один договор, один контрагент и один тип открытого остатка." ]; lines.push(""); - lines.push("Блок 2. Что учтено"); + lines.push("Что учтено"); lines.push(`- Дата среза: ${formatDateRu(asOfDate)}.`); if (periodScopeLine) { lines.push(periodScopeLine); @@ -2796,7 +2795,7 @@ function composeFactualReplyBody(intent, rows, options = {}) { lines.push("- Контур: остатки по счетам 60/62/76."); lines.push("- Смешанные экономические смыслы не склеиваются: дебиторка, кредиторка, авансы и прочие остатки показаны раздельно."); lines.push(""); - lines.push("Блок 3. Сводка"); + lines.push("Сводка"); lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`); lines.push(`- Уникальных договоров: ${formatNumberWithDots(uniqueContracts.length)}.`); lines.push(`- Подтвержденных договор-контрагент профилей: ${formatNumberWithDots(contractProfiles.length)}.`); @@ -2811,42 +2810,42 @@ function composeFactualReplyBody(intent, rows, options = {}) { lines.push(`- Спорные/некачественно нормализованные позиции: ${formatNumberWithDots(dirtyProfiles.length)} на ${formatMoneyRub(dirtyTotal)}.`); if (commercialProfiles.length > 0) { lines.push(""); - lines.push("Блок 4. Чистый открытый остаток по договорам"); + lines.push("Чистый открытый остаток по договорам"); lines.push(...renderContractProfileLines(commercialProfiles, false)); } if (commercialReceivables.length > 0) { lines.push(""); - lines.push("Блок 5. Коммерческие дебиторские компоненты"); + lines.push("Коммерческие дебиторские компоненты"); lines.push(...renderConfirmedContractLines(commercialReceivables, false)); } if (commercialPayables.length > 0) { lines.push(""); - lines.push("Блок 6. Коммерческие кредиторские компоненты"); + lines.push("Коммерческие кредиторские компоненты"); lines.push(...renderConfirmedContractLines(commercialPayables, false)); } if (commercialAdvances.length > 0) { lines.push(""); - lines.push("Блок 7. Коммерческие авансовые компоненты"); + lines.push("Коммерческие авансовые компоненты"); lines.push(...renderConfirmedContractLines(commercialAdvances, false)); } if (commercialOther.length > 0) { lines.push(""); - lines.push("Блок 8. Прочие компоненты по 76"); + lines.push("Прочие компоненты по 76"); lines.push(...renderConfirmedContractLines(commercialOther, false)); } if (specialProfiles.length > 0) { lines.push(""); - lines.push("Блок 9. Финансовые/специальные позиции"); + lines.push("Финансовые и специальные позиции"); lines.push(...renderContractProfileLines(specialProfiles, true)); } if (dirtyProfiles.length > 0) { lines.push(""); - lines.push("Блок 10. Спорные/некачественно нормализованные позиции"); + lines.push("Спорные и некачественно нормализованные позиции"); lines.push(...renderContractProfileLines(dirtyProfiles, true)); } if (confirmedContracts.length === 0) { lines.push(""); - lines.push("Блок 4. Подтвержденные позиции"); + lines.push("Подтвержденные позиции"); lines.push("- На дату среза подтвержденные договоры с открытыми взаиморасчетами не найдены."); } return { @@ -2869,13 +2868,10 @@ function composeFactualReplyBody(intent, rows, options = {}) { const specialContracts = contracts.filter((item) => item.category !== "commercial"); const commercialTotal = commercialContracts.reduce((sum, item) => sum + item.totalAmount, 0); const lines = [ - `Итого по предварительному срезу открытых договоров${asOfDate ? ` на ${formatDateRu(asOfDate)}` : ""}: ${formatNumberWithDots(commercialContracts.length)} коммерческих договоров на ${formatMoneyRub(commercialTotal)}.`, + `Коротко: по предварительному срезу открытых договоров${asOfDate ? ` на ${formatDateRu(asOfDate)}` : ""} найдено ${formatNumberWithDots(commercialContracts.length)} коммерческих договоров на ${formatMoneyRub(commercialTotal)}.`, + "Это shortlist на проверку, а не финальный подтвержденный реестр.", "", - "Блок 1. Статус результата", - "- Результат: предварительный список договоров с возможными незакрытыми расчетами.", - "- Перед финансовым решением нужна сверка карточек договоров и взаиморасчетов в 1С.", - "", - "Блок 2. Что учтено", + "Что учтено", ...(asOfDate ? [`- Дата среза: ${formatDateRu(asOfDate)}.`] : periodFrom || periodTo @@ -2883,7 +2879,7 @@ function composeFactualReplyBody(intent, rows, options = {}) { : []), "- Контур: движения по счетам 60/62/76 и договорная аналитика.", "", - "Блок 3. Сводка", + "Сводка", `- Строк в выборке: ${formatNumberWithDots(rows.length)}.`, `- Договоров-кандидатов всего: ${formatNumberWithDots(contracts.length)}.`, `- Основной список (коммерческие): ${formatNumberWithDots(commercialContracts.length)}.`, @@ -2891,7 +2887,7 @@ function composeFactualReplyBody(intent, rows, options = {}) { ]; if (commercialContracts.length > 0) { lines.push(""); - lines.push("Блок 4. Основной список (коммерческие договоры)"); + lines.push("Основной список (коммерческие договоры)"); lines.push(...commercialContracts.slice(0, 10).map((item, index) => { const counterpartiesLabel = item.counterparties.length > 0 ? item.counterparties.join("; ") : "контрагент не определен"; const sourceRefsSuffix = item.sourceRefs.length > 0 ? ` | source refs: ${item.sourceRefs.slice(0, 2).join("; ")}` : ""; @@ -2899,7 +2895,7 @@ function composeFactualReplyBody(intent, rows, options = {}) { })); if (specialContracts.length > 0) { lines.push(""); - lines.push("Блок 5. Финансовые/спорные позиции (вынесены отдельно)"); + lines.push("Финансовые и спорные позиции (вынесены отдельно)"); lines.push(...specialContracts.slice(0, 8).map((item, index) => { const counterpartiesLabel = item.counterparties.length > 0 ? item.counterparties.join("; ") : "контрагент не определен"; const sourceRefsSuffix = item.sourceRefs.length > 0 ? ` | source refs: ${item.sourceRefs.slice(0, 2).join("; ")}` : ""; @@ -2909,7 +2905,7 @@ function composeFactualReplyBody(intent, rows, options = {}) { } else if (counterparties.length > 0) { lines.push(""); - lines.push("Блок 4. Контрагенты с сигналом незакрытых расчетов"); + lines.push("Контрагенты с сигналом незакрытых расчетов"); lines.push(`- Контрагентов с сигналом: ${formatNumberWithDots(counterparties.length)}.`); lines.push(...counterparties .slice(0, 8) @@ -2918,9 +2914,9 @@ function composeFactualReplyBody(intent, rows, options = {}) { } else { lines.push(""); - lines.push("Блок 4. Позиции не выделены"); - lines.push("- По текущему live-срезу не удалось выделить договоры с достаточным качеством идентификации."); - lines.push("Блок 5. Примеры исходных строк"); + lines.push("Позиции не выделены"); + lines.push("- По текущему срезу не удалось выделить договоры с достаточным качеством идентификации."); + lines.push("Примеры исходных строк"); lines.push(...formatTopRows(rows, 6)); } return { @@ -2951,13 +2947,11 @@ function composeFactualReplyBody(intent, rows, options = {}) { return acc; }, { supplier_or_contractor: 0, bank_or_credit: 0, tax_or_state: 0, other: 0 }); const lines = [ - `Итого подтвержденный долг на ${formatDateRu(payablesAsOfDate)}: ${formatMoneyRub(totalOutstandingAmount)}.`, - "", - "Блок 1. Статус результата", - "- Результат: подтвержденный срез обязательств к оплате." + `Коротко: подтвержденный долг к оплате на ${formatDateRu(payablesAsOfDate)} — ${formatMoneyRub(totalOutstandingAmount)}.`, + "Это подтвержденный срез обязательств к оплате, а не эвристический shortlist." ]; lines.push(""); - lines.push("Блок 2. Что учтено"); + lines.push("Что учтено"); lines.push(`- Дата среза: ${formatDateRu(payablesAsOfDate)}.`); if (periodScopeLine) { lines.push(periodScopeLine); @@ -2967,17 +2961,17 @@ function composeFactualReplyBody(intent, rows, options = {}) { lines.push(carryoverLine); } lines.push(""); - lines.push("Блок 3. Сводка"); + lines.push("Сводка"); lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`); lines.push(`- Контрагентов с подтвержденным остатком к оплате: ${formatNumberWithDots(confirmedBalances.length)}.`); lines.push(""); - lines.push("Блок 4. Категории обязательств"); + lines.push("Категории обязательств"); lines.push(`- ${liabilityCategoryLabel("supplier_or_contractor")}: ${formatNumberWithDots(categoryCounts.supplier_or_contractor)}.`); lines.push(`- ${liabilityCategoryLabel("bank_or_credit")}: ${formatNumberWithDots(categoryCounts.bank_or_credit)}.`); lines.push(`- ${liabilityCategoryLabel("tax_or_state")}: ${formatNumberWithDots(categoryCounts.tax_or_state)}.`); lines.push(`- ${liabilityCategoryLabel("other")}: ${formatNumberWithDots(categoryCounts.other)}.`); lines.push(""); - lines.push("Блок 5. Подтвержденные позиции к оплате"); + lines.push("Подтвержденные позиции к оплате"); if (confirmedBalances.length > 0) { lines.push(...confirmedBalances.slice(0, 10).flatMap((item, index) => [ `${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`, @@ -3018,13 +3012,11 @@ function composeFactualReplyBody(intent, rows, options = {}) { return acc; }, { supplier_or_contractor: 0, bank_or_credit: 0, tax_or_state: 0, other: 0 }); const lines = [ - `Итого подтвержденная дебиторская задолженность на ${formatDateRu(receivablesAsOfDate)}: ${formatMoneyRub(totalOutstandingAmount)}.`, - "", - "Блок 1. Статус результата", - "- Результат: подтвержденный срез дебиторской задолженности." + `Коротко: подтвержденная дебиторская задолженность на ${formatDateRu(receivablesAsOfDate)} — ${formatMoneyRub(totalOutstandingAmount)}.`, + "Это подтвержденный срез дебиторской задолженности, а не эвристический shortlist." ]; lines.push(""); - lines.push("Блок 2. Что учтено"); + lines.push("Что учтено"); lines.push(`- Дата среза: ${formatDateRu(receivablesAsOfDate)}.`); if (periodScopeLine) { lines.push(periodScopeLine); @@ -3034,17 +3026,17 @@ function composeFactualReplyBody(intent, rows, options = {}) { lines.push(carryoverLine); } lines.push(""); - lines.push("Блок 3. Сводка"); + lines.push("Сводка"); lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`); lines.push(`- Контрагентов с подтвержденным остатком к получению: ${formatNumberWithDots(confirmedBalances.length)}.`); lines.push(""); - lines.push("Блок 4. Категории дебиторской задолженности"); + lines.push("Категории дебиторской задолженности"); lines.push(`- ${receivablesCategoryLabel("supplier_or_contractor")}: ${formatNumberWithDots(categoryCounts.supplier_or_contractor)}.`); lines.push(`- ${receivablesCategoryLabel("bank_or_credit")}: ${formatNumberWithDots(categoryCounts.bank_or_credit)}.`); lines.push(`- ${receivablesCategoryLabel("tax_or_state")}: ${formatNumberWithDots(categoryCounts.tax_or_state)}.`); lines.push(`- ${receivablesCategoryLabel("other")}: ${formatNumberWithDots(categoryCounts.other)}.`); lines.push(""); - lines.push("Блок 5. Подтвержденные позиции к получению"); + lines.push("Подтвержденные позиции к получению"); if (confirmedBalances.length > 0) { lines.push(...confirmedBalances.slice(0, 10).flatMap((item, index) => [ `${index + 1}. ${item.name} | категория: ${receivablesCategoryLabel(item.category)} | остаток к получению: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`, diff --git a/llm_normalizer/backend/src/services/address_runtime/composeStage.ts b/llm_normalizer/backend/src/services/address_runtime/composeStage.ts index 87d53fd..8daf772 100644 --- a/llm_normalizer/backend/src/services/address_runtime/composeStage.ts +++ b/llm_normalizer/backend/src/services/address_runtime/composeStage.ts @@ -3588,21 +3588,20 @@ function composeFactualReplyBody( }); const lines: string[] = [ - `Собран подтвержденный срез открытых договоров на ${formatDateRu(asOfDate)}.`, - `Чистый коммерческий остаток: ${openContractNetBalanceDirectionLabel(commercialNetTotal)} ${formatMoneyRub(Math.abs(commercialNetTotal))}.`, - `Брутто коммерческих компонентов: ${formatMoneyRub(commercialGrossTotal)}.`, - `Специальные финансовые позиции: ${formatNumberWithDots(specialProfiles.length)} на ${formatMoneyRub(specialTotal)}.`, - `Спорные/некачественно нормализованные позиции: ${formatNumberWithDots(dirtyProfiles.length)} на ${formatMoneyRub(dirtyTotal)}.`, + `Коротко: на ${formatDateRu(asOfDate)} подтверждено открытых договоров с коммерческим остатком ${openContractNetBalanceDirectionLabel(commercialNetTotal)} ${formatMoneyRub(Math.abs(commercialNetTotal))}.`, + `Брутто по коммерческим компонентам: ${formatMoneyRub(commercialGrossTotal)}.`, + `Отдельно вынесены специальные финансовые позиции: ${formatNumberWithDots(specialProfiles.length)} на ${formatMoneyRub(specialTotal)}.`, + `Спорные или некачественно нормализованные позиции: ${formatNumberWithDots(dirtyProfiles.length)} на ${formatMoneyRub(dirtyTotal)}.`, "", - "Блок 1. Статус результата", - "- Результат: подтвержденный срез договоров с открытыми взаиморасчетами на дату.", - "- База ответа: остатки по счетам 60/62/76 с договорной аналитикой, без эвристического shortlist.", - "- Управленческий вид: по каждому договору показаны чистый остаток и состав по типам открытых расчетов.", - "- Базовая единица детализации: одна строка = один договор, один контрагент и один тип открытого остатка." + "Что это значит", + "- Это подтвержденный срез договоров с открытыми взаиморасчетами на дату.", + "- Основа ответа: остатки по счетам 60/62/76 с договорной аналитикой, без эвристического shortlist.", + "- По каждому договору показаны чистый остаток и состав по типам открытых расчетов.", + "- Одна строка = один договор, один контрагент и один тип открытого остатка." ]; lines.push(""); - lines.push("Блок 2. Что учтено"); + lines.push("Что учтено"); lines.push(`- Дата среза: ${formatDateRu(asOfDate)}.`); if (periodScopeLine) { lines.push(periodScopeLine); @@ -3612,7 +3611,7 @@ function composeFactualReplyBody( lines.push("- Смешанные экономические смыслы не склеиваются: дебиторка, кредиторка, авансы и прочие остатки показаны раздельно."); lines.push(""); - lines.push("Блок 3. Сводка"); + lines.push("Сводка"); lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`); lines.push(`- Уникальных договоров: ${formatNumberWithDots(uniqueContracts.length)}.`); lines.push(`- Подтвержденных договор-контрагент профилей: ${formatNumberWithDots(contractProfiles.length)}.`); @@ -3638,49 +3637,49 @@ function composeFactualReplyBody( if (commercialProfiles.length > 0) { lines.push(""); - lines.push("Блок 4. Чистый открытый остаток по договорам"); + lines.push("Чистый открытый остаток по договорам"); lines.push(...renderContractProfileLines(commercialProfiles, false)); } if (commercialReceivables.length > 0) { lines.push(""); - lines.push("Блок 5. Коммерческие дебиторские компоненты"); + lines.push("Коммерческие дебиторские компоненты"); lines.push(...renderConfirmedContractLines(commercialReceivables, false)); } if (commercialPayables.length > 0) { lines.push(""); - lines.push("Блок 6. Коммерческие кредиторские компоненты"); + lines.push("Коммерческие кредиторские компоненты"); lines.push(...renderConfirmedContractLines(commercialPayables, false)); } if (commercialAdvances.length > 0) { lines.push(""); - lines.push("Блок 7. Коммерческие авансовые компоненты"); + lines.push("Коммерческие авансовые компоненты"); lines.push(...renderConfirmedContractLines(commercialAdvances, false)); } if (commercialOther.length > 0) { lines.push(""); - lines.push("Блок 8. Прочие компоненты по 76"); + lines.push("Прочие компоненты по 76"); lines.push(...renderConfirmedContractLines(commercialOther, false)); } if (specialProfiles.length > 0) { lines.push(""); - lines.push("Блок 9. Финансовые/специальные позиции"); + lines.push("Финансовые и специальные позиции"); lines.push(...renderContractProfileLines(specialProfiles, true)); } if (dirtyProfiles.length > 0) { lines.push(""); - lines.push("Блок 10. Спорные/некачественно нормализованные позиции"); + lines.push("Спорные и некачественно нормализованные позиции"); lines.push(...renderContractProfileLines(dirtyProfiles, true)); } if (confirmedContracts.length === 0) { lines.push(""); - lines.push("Блок 4. Подтвержденные позиции"); + lines.push("Подтвержденные позиции"); lines.push("- На дату среза подтвержденные договоры с открытыми взаиморасчетами не найдены."); } @@ -3705,13 +3704,10 @@ function composeFactualReplyBody( const specialContracts = contracts.filter((item) => item.category !== "commercial"); const commercialTotal = commercialContracts.reduce((sum, item) => sum + item.totalAmount, 0); const lines: string[] = [ - `Итого по предварительному срезу открытых договоров${asOfDate ? ` на ${formatDateRu(asOfDate)}` : ""}: ${formatNumberWithDots(commercialContracts.length)} коммерческих договоров на ${formatMoneyRub(commercialTotal)}.`, + `Коротко: по предварительному срезу открытых договоров${asOfDate ? ` на ${formatDateRu(asOfDate)}` : ""} найдено ${formatNumberWithDots(commercialContracts.length)} коммерческих договоров на ${formatMoneyRub(commercialTotal)}.`, + "Это shortlist на проверку, а не финальный подтвержденный реестр.", "", - "Блок 1. Статус результата", - "- Результат: предварительный список договоров с возможными незакрытыми расчетами.", - "- Перед финансовым решением нужна сверка карточек договоров и взаиморасчетов в 1С.", - "", - "Блок 2. Что учтено", + "Что учтено", ...(asOfDate ? [`- Дата среза: ${formatDateRu(asOfDate)}.`] : periodFrom || periodTo @@ -3719,7 +3715,7 @@ function composeFactualReplyBody( : []), "- Контур: движения по счетам 60/62/76 и договорная аналитика.", "", - "Блок 3. Сводка", + "Сводка", `- Строк в выборке: ${formatNumberWithDots(rows.length)}.`, `- Договоров-кандидатов всего: ${formatNumberWithDots(contracts.length)}.`, `- Основной список (коммерческие): ${formatNumberWithDots(commercialContracts.length)}.`, @@ -3727,7 +3723,7 @@ function composeFactualReplyBody( ]; if (commercialContracts.length > 0) { lines.push(""); - lines.push("Блок 4. Основной список (коммерческие договоры)"); + lines.push("Основной список (коммерческие договоры)"); lines.push( ...commercialContracts.slice(0, 10).map((item, index) => { const counterpartiesLabel = @@ -3739,7 +3735,7 @@ function composeFactualReplyBody( ); if (specialContracts.length > 0) { lines.push(""); - lines.push("Блок 5. Финансовые/спорные позиции (вынесены отдельно)"); + lines.push("Финансовые и спорные позиции (вынесены отдельно)"); lines.push( ...specialContracts.slice(0, 8).map((item, index) => { const counterpartiesLabel = @@ -3752,7 +3748,7 @@ function composeFactualReplyBody( } } else if (counterparties.length > 0) { lines.push(""); - lines.push("Блок 4. Контрагенты с сигналом незакрытых расчетов"); + lines.push("Контрагенты с сигналом незакрытых расчетов"); lines.push(`- Контрагентов с сигналом: ${formatNumberWithDots(counterparties.length)}.`); lines.push( ...counterparties @@ -3765,9 +3761,9 @@ function composeFactualReplyBody( lines.push("- Договорные реквизиты выделены недостаточно надежно, поэтому показан контрагентный список для проверки."); } else { lines.push(""); - lines.push("Блок 4. Позиции не выделены"); - lines.push("- По текущему live-срезу не удалось выделить договоры с достаточным качеством идентификации."); - lines.push("Блок 5. Примеры исходных строк"); + lines.push("Позиции не выделены"); + lines.push("- По текущему срезу не удалось выделить договоры с достаточным качеством идентификации."); + lines.push("Примеры исходных строк"); lines.push(...formatTopRows(rows, 6)); } return { @@ -3805,14 +3801,12 @@ function composeFactualReplyBody( ); const lines: string[] = [ - `Итого подтвержденный долг на ${formatDateRu(payablesAsOfDate)}: ${formatMoneyRub(totalOutstandingAmount)}.`, - "", - "Блок 1. Статус результата", - "- Результат: подтвержденный срез обязательств к оплате." + `Коротко: подтвержденный долг к оплате на ${formatDateRu(payablesAsOfDate)} — ${formatMoneyRub(totalOutstandingAmount)}.`, + "Это подтвержденный срез обязательств к оплате, а не эвристический shortlist." ]; lines.push(""); - lines.push("Блок 2. Что учтено"); + lines.push("Что учтено"); lines.push(`- Дата среза: ${formatDateRu(payablesAsOfDate)}.`); if (periodScopeLine) { lines.push(periodScopeLine); @@ -3823,19 +3817,19 @@ function composeFactualReplyBody( } lines.push(""); - lines.push("Блок 3. Сводка"); + lines.push("Сводка"); lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`); lines.push(`- Контрагентов с подтвержденным остатком к оплате: ${formatNumberWithDots(confirmedBalances.length)}.`); lines.push(""); - lines.push("Блок 4. Категории обязательств"); + lines.push("Категории обязательств"); lines.push(`- ${liabilityCategoryLabel("supplier_or_contractor")}: ${formatNumberWithDots(categoryCounts.supplier_or_contractor)}.`); lines.push(`- ${liabilityCategoryLabel("bank_or_credit")}: ${formatNumberWithDots(categoryCounts.bank_or_credit)}.`); lines.push(`- ${liabilityCategoryLabel("tax_or_state")}: ${formatNumberWithDots(categoryCounts.tax_or_state)}.`); lines.push(`- ${liabilityCategoryLabel("other")}: ${formatNumberWithDots(categoryCounts.other)}.`); lines.push(""); - lines.push("Блок 5. Подтвержденные позиции к оплате"); + lines.push("Подтвержденные позиции к оплате"); if (confirmedBalances.length > 0) { lines.push( ...confirmedBalances.slice(0, 10).flatMap((item, index) => [ @@ -3885,14 +3879,12 @@ function composeFactualReplyBody( ); const lines: string[] = [ - `Итого подтвержденная дебиторская задолженность на ${formatDateRu(receivablesAsOfDate)}: ${formatMoneyRub(totalOutstandingAmount)}.`, - "", - "Блок 1. Статус результата", - "- Результат: подтвержденный срез дебиторской задолженности." + `Коротко: подтвержденная дебиторская задолженность на ${formatDateRu(receivablesAsOfDate)} — ${formatMoneyRub(totalOutstandingAmount)}.`, + "Это подтвержденный срез дебиторской задолженности, а не эвристический shortlist." ]; lines.push(""); - lines.push("Блок 2. Что учтено"); + lines.push("Что учтено"); lines.push(`- Дата среза: ${formatDateRu(receivablesAsOfDate)}.`); if (periodScopeLine) { lines.push(periodScopeLine); @@ -3903,19 +3895,19 @@ function composeFactualReplyBody( } lines.push(""); - lines.push("Блок 3. Сводка"); + lines.push("Сводка"); lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`); lines.push(`- Контрагентов с подтвержденным остатком к получению: ${formatNumberWithDots(confirmedBalances.length)}.`); lines.push(""); - lines.push("Блок 4. Категории дебиторской задолженности"); + lines.push("Категории дебиторской задолженности"); lines.push(`- ${receivablesCategoryLabel("supplier_or_contractor")}: ${formatNumberWithDots(categoryCounts.supplier_or_contractor)}.`); lines.push(`- ${receivablesCategoryLabel("bank_or_credit")}: ${formatNumberWithDots(categoryCounts.bank_or_credit)}.`); lines.push(`- ${receivablesCategoryLabel("tax_or_state")}: ${formatNumberWithDots(categoryCounts.tax_or_state)}.`); lines.push(`- ${receivablesCategoryLabel("other")}: ${formatNumberWithDots(categoryCounts.other)}.`); lines.push(""); - lines.push("Блок 5. Подтвержденные позиции к получению"); + lines.push("Подтвержденные позиции к получению"); if (confirmedBalances.length > 0) { lines.push( ...confirmedBalances.slice(0, 10).flatMap((item, index) => [ diff --git a/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts b/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts index e340454..8d3b354 100644 --- a/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts +++ b/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts @@ -509,10 +509,10 @@ describe("address compose stage utf8 headers", () => { ); expect(reply.responseType).toBe("FACTUAL_LIST"); - expect(reply.text).toContain("Итого по предварительному срезу открытых договоров"); - expect(reply.text).toContain("Блок 1. Статус результата"); - expect(reply.text).toContain("Результат: предварительный список договоров с возможными незакрытыми расчетами."); - expect(reply.text).toContain("Блок 4. Основной список (коммерческие договоры)"); + expect(reply.text).toContain("Коротко: по предварительному срезу открытых договоров"); + expect(reply.text).toContain("Это shortlist на проверку"); + expect(reply.text).toContain("Основной список (коммерческие договоры)"); + expect(reply.text).not.toContain("Блок 1"); expect(reply.semantics?.result_mode).toBe("heuristic_candidates"); expect(reply.semantics?.balance_confirmed).toBe(false); }); @@ -539,12 +539,13 @@ describe("address compose stage utf8 headers", () => { ); expect(reply.responseType).toBe("FACTUAL_LIST"); - expect(reply.text).toContain("Собран подтвержденный срез открытых договоров"); - expect(reply.text).toContain("Результат: подтвержденный срез договоров с открытыми взаиморасчетами на дату."); - expect(reply.text).toContain("Управленческий вид: по каждому договору показаны чистый остаток и состав по типам открытых расчетов."); - expect(reply.text).toContain("Базовая единица детализации: одна строка = один договор, один контрагент и один тип открытого остатка."); - expect(reply.text).toContain("Блок 4. Чистый открытый остаток по договорам"); - expect(reply.text).toContain("Блок 6. Коммерческие кредиторские компоненты"); + expect(reply.text).toContain("Коротко: на 31.03.2020 подтверждено открытых договоров"); + expect(reply.text).toContain("Это подтвержденный срез договоров с открытыми взаиморасчетами на дату."); + expect(reply.text).toContain("По каждому договору показаны чистый остаток и состав по типам открытых расчетов."); + expect(reply.text).toContain("Одна строка = один договор, один контрагент и один тип открытого остатка."); + expect(reply.text).toContain("Чистый открытый остаток по договорам"); + expect(reply.text).toContain("Коммерческие кредиторские компоненты"); + expect(reply.text).not.toContain("Блок 1"); expect(reply.semantics?.result_mode).toBe("confirmed_balance"); expect(reply.semantics?.balance_confirmed).toBe(true); }); @@ -586,15 +587,74 @@ describe("address compose stage utf8 headers", () => { } ); - expect(reply.text).toContain("Блок 4. Чистый открытый остаток по договорам"); - expect(reply.text).toContain("Блок 5. Коммерческие дебиторские компоненты"); - expect(reply.text).toContain("Блок 6. Коммерческие кредиторские компоненты"); - expect(reply.text).toContain("Блок 10. Спорные/некачественно нормализованные позиции"); + expect(reply.text).toContain("Чистый открытый остаток по договорам"); + expect(reply.text).toContain("Коммерческие дебиторские компоненты"); + expect(reply.text).toContain("Коммерческие кредиторские компоненты"); + expect(reply.text).toContain("Спорные и некачественно нормализованные позиции"); + expect(reply.text).not.toContain("Блок 1"); expect(reply.text).toContain("брутто компонентов"); expect(reply.text).not.toContain("счета: 62.01; 0"); expect(reply.text).toContain("договор не похож на устойчивый договорный реквизит"); }); + it("renders confirmed payables snapshot business-first without numbered report framing", () => { + const reply = composeFactualReply( + "payables_confirmed_as_of_date", + [ + { + period: "2020-05-31T23:59:59Z", + registrator: "Остатки на дату", + account_dt: "", + account_kt: "60.01", + amount: 125000, + analytics: ["ООО Ромашка", "Договор №19/15"] + } + ], + { + asOfDate: "2020-05-31", + useRubCurrency: true + } + ); + + expect(reply.responseType).toBe("FACTUAL_LIST"); + expect(reply.text).toContain("Коротко: подтвержденный долг к оплате на 31.05.2020"); + expect(reply.text).toContain("Это подтвержденный срез обязательств к оплате"); + expect(reply.text).toContain("Что учтено"); + expect(reply.text).toContain("Сводка"); + expect(reply.text).toContain("Категории обязательств"); + expect(reply.text).toContain("Подтвержденные позиции к оплате"); + expect(reply.text).not.toContain("Блок 1"); + }); + + it("renders confirmed receivables snapshot business-first without numbered report framing", () => { + const reply = composeFactualReply( + "receivables_confirmed_as_of_date", + [ + { + period: "2020-05-31T23:59:59Z", + registrator: "Остатки на дату", + account_dt: "62.01", + account_kt: "", + amount: 84000, + analytics: ["ООО Ромашка", "Договор №19/15"] + } + ], + { + asOfDate: "2020-05-31", + useRubCurrency: true + } + ); + + expect(reply.responseType).toBe("FACTUAL_LIST"); + expect(reply.text).toContain("Коротко: подтвержденная дебиторская задолженность на 31.05.2020"); + expect(reply.text).toContain("Это подтвержденный срез дебиторской задолженности"); + expect(reply.text).toContain("Что учтено"); + expect(reply.text).toContain("Сводка"); + expect(reply.text).toContain("Категории дебиторской задолженности"); + expect(reply.text).toContain("Подтвержденные позиции к получению"); + expect(reply.text).not.toContain("Блок 1"); + }); + it("renders period coverage summary for management profile intent", () => { const reply = composeFactualReply("period_coverage_profile", [ { @@ -3284,12 +3344,13 @@ describe("address query limited taxonomy and stage diagnostics", { timeout: 1500 expect(reply.toLowerCase()).toContain("shortlist"); } else { expect(result?.debug.balance_confirmed).toBe(true); - expect(reply).toContain("Итого подтвержденный долг"); - expect(reply).toMatch(/Блок\s+(?:\*\*1\*\*|1)\.\s+Статус результата/u); - expect(reply).toMatch(/\n\nБлок\s+(?:\*\*2\*\*|2)\.\s+Что учтено/u); - expect(reply).toMatch(/\n\nБлок\s+(?:\*\*3\*\*|3)\.\s+Сводка/u); - expect(reply).toMatch(/\n\nБлок\s+(?:\*\*4\*\*|4)\.\s+Категории обязательств/u); - expect(reply).toMatch(/\n\nБлок\s+(?:\*\*5\*\*|5)\.\s+Подтвержденные позиции к оплате/u); + expect(reply).toContain("Коротко: подтвержденный долг к оплате"); + expect(reply).toContain("Это подтвержденный срез обязательств к оплате"); + expect(reply).toMatch(/\n\nЧто учтено/u); + expect(reply).toMatch(/\n\nСводка/u); + expect(reply).toMatch(/\n\nКатегории обязательств/u); + expect(reply).toMatch(/\n\nПодтвержденные позиции к оплате/u); + expect(reply).not.toContain("Блок 1"); expect(reply).not.toContain("эвристический"); } }); diff --git a/llm_normalizer/data/eval_cases/assistant_saved_session_runtime_job-mmTvku3mtK.json b/llm_normalizer/data/eval_cases/assistant_saved_session_runtime_job-mmTvku3mtK.json new file mode 100644 index 0000000..cda9281 --- /dev/null +++ b/llm_normalizer/data/eval_cases/assistant_saved_session_runtime_job-mmTvku3mtK.json @@ -0,0 +1,114 @@ +{ + "suite_id": "assistant_saved_session_runtime_job-mmTvku3mtK", + "suite_version": "0.1.0", + "schema_version": "assistant_saved_session_runtime_v0_1", + "title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06", + "scenario_count": 1, + "case_ids": [ + "SAVED-001" + ], + "cases": [ + { + "case_id": "SAVED-001", + "scenario_tag": "saved_user_sessions_runtime", + "title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06", + "question_type": "followup", + "broadness_level": "medium", + "turns": [ + { + "user_message": "приветик - че как там дела" + }, + { + "user_message": "расскажи что можешь интересного" + }, + { + "user_message": "кайф - что там на складе по остаткам?" + }, + { + "user_message": "а исторические остатки на другие даты умеешь?" + }, + { + "user_message": "давай на июль 2017" + }, + { + "user_message": "март 2016" + }, + { + "user_message": "По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?" + }, + { + "user_message": "а кому продали?" + }, + { + "user_message": "у тебя написано кто контрагент: рабочая станция - это ошибка?" + }, + { + "user_message": "ндс можешь прикинуть на дату покупки рабочей станции?" + }, + { + "user_message": "а какой ндс мы должны сгрузить на март 2020?" + }, + { + "user_message": "прикинь какой ндс нам надо заплатить на февраль 2017" + }, + { + "user_message": "кто у нас самый доходный клиент за все время" + }, + { + "user_message": "кто нам должен денег на май 2017" + }, + { + "user_message": "а какой ндс мы должны примерно заплатить за этот период?" + }, + { + "user_message": "мы должны комуто денег на сегодня?" + }, + { + "user_message": "а нам?" + }, + { + "user_message": "какой у нас самый доходный год" + }, + { + "user_message": "а за 2017 мы скок заработали?" + }, + { + "user_message": "сколько вообще денег мы заработали за все время?" + }, + { + "user_message": "ты умеешь считать дельту по договорам?" + }, + { + "user_message": "по чепурнову покажи все доки" + }, + { + "user_message": "а по свк" + }, + { + "user_message": "а сейчас у нас есть что на складе?" + }, + { + "user_message": "что нам отгружал чепурнов? какой товар или услугу?" + }, + { + "user_message": "какие остатки на складе на сегодня" + }, + { + "user_message": "остатки на март 2016" + }, + { + "user_message": "хвосты покажи по счету 60 на август 2022" + }, + { + "user_message": "Есть ли остатки товара, которые закупались очень давно" + }, + { + "user_message": "Какие конкретно номенклатуры формируют остаток по складу на май 2020" + }, + { + "user_message": "а по Альтернативе Плюс сколько лет активности в базе 1С?" + } + ] + } + ] +} \ No newline at end of file