From 801982b66bae0437603689b615546d5c5b1217fc Mon Sep 17 00:00:00 2001 From: dctouch Date: Sun, 24 May 2026 11:15:20 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A1=D1=82=D0=B0=D0=B1=D0=B8=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20smoke-=D0=BA?= =?UTF-8?q?=D0=BE=D0=BD=D1=82=D1=80=D0=B0=D0=BA=D1=82=D1=8B=20=D0=B0=D0=B3?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D1=81=D0=BA=D0=BE=D0=B3=D0=BE=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BD=D1=82=D1=83=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dist/services/addressIntentResolver.js | 9 ++++++++ .../backend/dist/services/assistantService.js | 17 +++++++++++++- .../src/services/addressIntentResolver.ts | 23 +++++++++++++++++++ .../backend/src/services/assistantService.ts | 21 ++++++++++++++++- .../addressInventoryAgingFollowup.test.ts | 3 ++- .../addressInventoryOrganizationScope.test.ts | 4 ++-- ...ressInventoryPurchaseDocumentRoute.test.ts | 2 +- .../tests/assistantMcpRuntimeBridge.test.ts | 19 +++++++++++---- ...sistantWave17RunRegression20260411.test.ts | 8 ++++++- llm_normalizer/backend/tests/fixtures.ts | 21 ++++++++++++++++- 10 files changed, 115 insertions(+), 12 deletions(-) diff --git a/llm_normalizer/backend/dist/services/addressIntentResolver.js b/llm_normalizer/backend/dist/services/addressIntentResolver.js index b439303..cc35ef9 100644 --- a/llm_normalizer/backend/dist/services/addressIntentResolver.js +++ b/llm_normalizer/backend/dist/services/addressIntentResolver.js @@ -1804,6 +1804,11 @@ function resolveUnicodeAddressIntentBridge(text) { /(?:формирующ.*остат|раскрой\s+остат|остат.*по\s+документ|по\s+докам.*(?:60|62|76)|(?:60|62|76)(?:[.,]\d{2})?.*(?:по\s+докам|по\s+документ))/iu.test(normalized)) { return unicodeBridgeResolution("documents_forming_balance", "high", "unicode_documents_forming_balance_bridge_signal_detected"); } + if (/(?:незакрыт|открыт).*договор/iu.test(normalized) && + /(?:\bна\b|по\s+состоянию|конец|дат)/iu.test(normalized) && + !/(?:долг|задолж|хвост|висит|расчет|расчёт)/iu.test(normalized)) { + return unicodeBridgeResolution("open_contracts_confirmed_as_of_date", "high", "unicode_open_contracts_snapshot_bridge_signal_detected"); + } if (/(?:договор[а-я]*.*(?:все|список).*по\s+[\p{L}\d]|(?:покажи|показать).*договор[а-я]*.*по\s+[\p{L}\d])/iu.test(normalized)) { return unicodeBridgeResolution("list_contracts_by_counterparty", "high", "unicode_contracts_by_counterparty_bridge_signal_detected"); } @@ -1817,6 +1822,10 @@ function resolveUnicodeAddressIntentBridge(text) { /(?:без\s+(?:закрыт|документ|оплат)|нет\s+(?:документ|оплат)|не\s+закрыт|оплат[а-я]*\s+нет|документ[а-я]*\s+есть|требует\s+ручн)/iu.test(normalized)) { return unicodeBridgeResolution("list_open_contracts", "high", "unicode_open_contract_gap_bridge_signal_detected"); } + if (/(?:аванс[а-я]*\s+к\s+отгрузк|отгрузк[а-я]*[^\n]{0,80}аванс|аванс[а-я]*[^\n]{0,80}(?:закрыт|завис|перепривяз|спис)|завис[а-я]*\s+аванс)/iu.test(normalized) && + /(?:давно|пора\s+закрыт|перепровер|подозрев|худш|проблем|завис|перепривяз|спис[а-я]*\s+как\s+нереальн)/iu.test(normalized)) { + return unicodeBridgeResolution("list_open_contracts", "high", "unicode_old_advances_to_shipments_bridge_signal_detected"); + } if (/(?:долгожител|долго\s+долж|задолженн(?:ост|остям).*(?:давн|долго)|не\s+плат|не\s+оплат|не\s+оплачен|неоплачен|просроч|сроки\s+давно\s+прошл|слишком\s+длинн.*оплат)/iu.test(normalized) && /(?:покупател|клиент|заказ|отгрузк|товар|услуг|задолженн|сальдо|не\s+плат|не\s+оплат|не\s+оплачен|неоплачен|просроч)/iu.test(normalized)) { return unicodeBridgeResolution("list_receivables_counterparties", "high", "receivables_debt_lifecycle_signal_detected"); diff --git a/llm_normalizer/backend/dist/services/assistantService.js b/llm_normalizer/backend/dist/services/assistantService.js index c458632..906017d 100644 --- a/llm_normalizer/backend/dist/services/assistantService.js +++ b/llm_normalizer/backend/dist/services/assistantService.js @@ -3815,6 +3815,13 @@ function hasDeepAnalysisPreferenceSignal(text) { if (!lower) { return false; } + const openContractsListQuestionSignal = /(?:незакрыт|не\s+закрыт|открыт|завис[а-я]*\s+аванс|аванс[а-я]*[^\n]{0,80}(?:закрыт|перепривяз|спис))/iu.test(lower) && + /(?:договор|контракт|документ|аванс|отгрузк)/iu.test(lower) && + /(?:какие|какой|где|покажи|показать|список|проверь|проверить|уточни)/iu.test(lower) && + !/(?:разложи|разложить|цепочк|механизм|почему|root\s*cause|trace\s*chain|корнев[а-я]*\s+причин)/iu.test(lower); + if (openContractsListQuestionSignal) { + return false; + } const riskOrAnomalySignal = /(?:\u0440\u0438\u0441\u043a|risk|\u0430\u043d\u043e\u043c\u0430\u043b|anomal|\u043f\u0440\u043e\u0442\u0438\u0432\u043e\u0440\u0435\u0447|\u043a\u043e\u043d\u0444\u043b\u0438\u043a\u0442|conflict|deviation|\u043e\u0442\u043a\u043b\u043e\u043d\u0435\u043d|\u043d\u0435\u0441\u044b\u043a\u043e\u0432\u043a|\u043d\u0435\u0441\u0445\u043e\u0434|\u043e\u0448\u0438\u0431|error|issue|\u043f\u0440\u043e\u0431\u043b\u0435\u043c)/iu.test(lower); const chainSignal = /(?:\u0446\u0435\u043f\u043e\u0447\u043a|chain|trace\s*chain|lifecycle|\u0436\u0438\u0437\u043d\u0435\u043d\u043d[\u0430-\u044f]+\s+\u0446\u0438\u043a\u043b|state\s+transition|\u0440\u0430\u0437\u0440\u044b\u0432[\u0430-\u044f]*)/iu.test(lower); const diagnosticsKeywordSignal = /(?:\u0440\u0430\u0437\u043b\u043e\u0436\u0438|\u0434\u0435\u043a\u043e\u043c\u043f\u043e\u0437|\u0440\u0430\u0437\u0431\u0435\u0440\u0438|\u043f\u043e\u0447\u0435\u043c\u0443|why|audit|scan|\u043a\u043e\u0440\u043d\u0435\u0432[\u0430-\u044f]+\s+\u043f\u0440\u0438\u0447\u0438\u043d|root\s*cause|\u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c[\u0430-\u044f]*|\u0433\u0434\u0435\s+\u0440\u0430\u0437\u0440\u044b\u0432|\u0447\u0442\u043e\s+\u043c\u0435\u0448\u0430[\u0430-\u044f]+\s+\u0437\u0430\u043a\u0440\u044b\u0442)/iu.test(lower); @@ -3841,7 +3848,15 @@ function hasDirectDeepAnalysisSignal(text) { if (!normalized) { return false; } - return /(?:\u0440\u0430\u0437\u043b\u043e\u0436|\u0446\u0435\u043f\u043e\u0447|lifecycle|\u0440\u0430\u0437\u0440\u044b\u0432|\u043f\u0440\u043e\u0442\u0438\u0432\u043e\u0440\u0435\u0447|\u0430\u043d\u043e\u043c\u0430\u043b|\u043f\u043e\u0447\u0435\u043c\u0443|\u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c|\u0437\u0430\u043a\u0440\u044b\u0442\u0438[\u0435\u044f]\s+\u043f\u0435\u0440\u0438\u043e\u0434|period\s*close|close\s+period|\u0447\u0442\u043e\s+\u043c\u0435\u0448\u0430[\u0430-\u044f]+\s+\u0437\u0430\u043a\u0440\u044b\u0442|state\s+transition|root\s*cause|trace\s*chain)/iu.test(normalized); + const fixedAssetAmortizationCompletenessSignal = /(?:\u0430\u043c\u043e\u0440\u0442\u0438\u0437|\u043e\u0441\u043d\u043e\u0432\u043d[\u0430-\u044f]*\s+\u0441\u0440\u0435\u0434\u0441\u0442\u0432|fixed\s*asset|depreciat|\b\u043e\u0441\b|\b0?1\s*[/\\]\s*0?2\b|\u0441\u0447[\u0435\u0451]\u0442[^\n]{0,16}\b0?1\b[^\n]{0,16}\b0?2\b)/iu.test(normalized) && + /(?:\u043f\u043e\u043b\u043d[\u0430-\u044f]*|\u043f\u0440\u043e\u043f\u0443\u0449|\u043d\u0430\u0447\u0438\u0441\u043b|\u043f\u0440\u043e\u0432\u0435\u0440|\u043e\u0431\u044a\u0435\u043a\u0442|coverage|missing|expected|actual)/iu.test(normalized); + const deferredExpenseDeepSignal = /(?:\b97(?:[.,]\d{1,2})?\b|\u0440\u0431\u043f|\u0440\u0430\u0441\u0445\u043e\u0434[\u044b\u043e\u0432\s]+\u0431\u0443\u0434\u0443\u0449[\u0438\u0445\u0435]\s+\u043f\u0435\u0440\u0438\u043e\u0434|deferred)/iu.test(normalized) && + /(?:\u0441\u0447[\u0435\u0451]\u0442|\u043f\u0435\u0440\u0438\u043e\u0434|\u0437\u0430\s+20\d{2}|\u0445\u0432\u043e\u0441\u0442|\u0441\u043f\u0438\u0441\u0430\u043d|\u043e\u0441\u0442\u0430\u0442|\u043e\u0442\u0434\u0435\u043b\u044c\u043d|\u043f\u0440\u043e\u0432\u0435\u0440|account|period|writeoff|tail)/iu.test(normalized); + const actualExpectedStateConflictSignal = /(?:\u0444\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a[\u0430-\u044f]*[^\n]{0,80}\u043e\u0436\u0438\u0434\u0430\u0435\u043c|\u043e\u0436\u0438\u0434\u0430\u0435\u043c[\u0430-\u044f]*[^\n]{0,80}\u0444\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a|actual[^\n]{0,80}expected|expected[^\n]{0,80}actual|\u043a\u043e\u043d\u0444\u043b\u0438\u043a\u0442[^\n]{0,80}(?:\u0441\u043e\u0441\u0442\u043e\u044f\u043d|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u0440\u0435\u0433\u0438\u0441\u0442\u0440|\u043f\u0440\u043e\u0432\u043e\u0434\u043a|\u0432\u0435\u0442\u0432))/iu.test(normalized); + return fixedAssetAmortizationCompletenessSignal || + deferredExpenseDeepSignal || + actualExpectedStateConflictSignal || + /(?:\u0440\u0430\u0437\u043b\u043e\u0436|\u0446\u0435\u043f\u043e\u0447|lifecycle|\u0440\u0430\u0437\u0440\u044b\u0432|\u043f\u0440\u043e\u0442\u0438\u0432\u043e\u0440\u0435\u0447|\u0430\u043d\u043e\u043c\u0430\u043b|\u043f\u043e\u0447\u0435\u043c\u0443|\u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c|\u0437\u0430\u043a\u0440\u044b\u0442\u0438[\u0435\u044f]\s+\u043f\u0435\u0440\u0438\u043e\u0434|period\s*close|close\s+period|\u0447\u0442\u043e\s+\u043c\u0435\u0448\u0430[\u0430-\u044f]+\s+\u0437\u0430\u043a\u0440\u044b\u0442|state\s+transition|root\s*cause|trace\s*chain)/iu.test(normalized); } function hasStrictDeepInvestigationCue(text) { const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase()); diff --git a/llm_normalizer/backend/src/services/addressIntentResolver.ts b/llm_normalizer/backend/src/services/addressIntentResolver.ts index 1f0116a..c4a9e2b 100644 --- a/llm_normalizer/backend/src/services/addressIntentResolver.ts +++ b/llm_normalizer/backend/src/services/addressIntentResolver.ts @@ -2425,6 +2425,18 @@ function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolutio ); } + if ( + /(?:незакрыт|открыт).*договор/iu.test(normalized) && + /(?:\bна\b|по\s+состоянию|конец|дат)/iu.test(normalized) && + !/(?:долг|задолж|хвост|висит|расчет|расчёт)/iu.test(normalized) + ) { + return unicodeBridgeResolution( + "open_contracts_confirmed_as_of_date", + "high", + "unicode_open_contracts_snapshot_bridge_signal_detected" + ); + } + if (/(?:договор[а-я]*.*(?:все|список).*по\s+[\p{L}\d]|(?:покажи|показать).*договор[а-я]*.*по\s+[\p{L}\d])/iu.test(normalized)) { return unicodeBridgeResolution( "list_contracts_by_counterparty", @@ -2456,6 +2468,17 @@ function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolutio ); } + if ( + /(?:аванс[а-я]*\s+к\s+отгрузк|отгрузк[а-я]*[^\n]{0,80}аванс|аванс[а-я]*[^\n]{0,80}(?:закрыт|завис|перепривяз|спис)|завис[а-я]*\s+аванс)/iu.test(normalized) && + /(?:давно|пора\s+закрыт|перепровер|подозрев|худш|проблем|завис|перепривяз|спис[а-я]*\s+как\s+нереальн)/iu.test(normalized) + ) { + return unicodeBridgeResolution( + "list_open_contracts", + "high", + "unicode_old_advances_to_shipments_bridge_signal_detected" + ); + } + if ( /(?:долгожител|долго\s+долж|задолженн(?:ост|остям).*(?:давн|долго)|не\s+плат|не\s+оплат|не\s+оплачен|неоплачен|просроч|сроки\s+давно\s+прошл|слишком\s+длинн.*оплат)/iu.test( normalized diff --git a/llm_normalizer/backend/src/services/assistantService.ts b/llm_normalizer/backend/src/services/assistantService.ts index 6920f7d..5574e76 100644 --- a/llm_normalizer/backend/src/services/assistantService.ts +++ b/llm_normalizer/backend/src/services/assistantService.ts @@ -3771,6 +3771,14 @@ function hasDeepAnalysisPreferenceSignal(text) { if (!lower) { return false; } + const openContractsListQuestionSignal = + /(?:незакрыт|не\s+закрыт|открыт|завис[а-я]*\s+аванс|аванс[а-я]*[^\n]{0,80}(?:закрыт|перепривяз|спис))/iu.test(lower) && + /(?:договор|контракт|документ|аванс|отгрузк)/iu.test(lower) && + /(?:какие|какой|где|покажи|показать|список|проверь|проверить|уточни)/iu.test(lower) && + !/(?:разложи|разложить|цепочк|механизм|почему|root\s*cause|trace\s*chain|корнев[а-я]*\s+причин)/iu.test(lower); + if (openContractsListQuestionSignal) { + return false; + } const riskOrAnomalySignal = /(?:\u0440\u0438\u0441\u043a|risk|\u0430\u043d\u043e\u043c\u0430\u043b|anomal|\u043f\u0440\u043e\u0442\u0438\u0432\u043e\u0440\u0435\u0447|\u043a\u043e\u043d\u0444\u043b\u0438\u043a\u0442|conflict|deviation|\u043e\u0442\u043a\u043b\u043e\u043d\u0435\u043d|\u043d\u0435\u0441\u044b\u043a\u043e\u0432\u043a|\u043d\u0435\u0441\u0445\u043e\u0434|\u043e\u0448\u0438\u0431|error|issue|\u043f\u0440\u043e\u0431\u043b\u0435\u043c)/iu.test(lower); const chainSignal = /(?:\u0446\u0435\u043f\u043e\u0447\u043a|chain|trace\s*chain|lifecycle|\u0436\u0438\u0437\u043d\u0435\u043d\u043d[\u0430-\u044f]+\s+\u0446\u0438\u043a\u043b|state\s+transition|\u0440\u0430\u0437\u0440\u044b\u0432[\u0430-\u044f]*)/iu.test(lower); const diagnosticsKeywordSignal = /(?:\u0440\u0430\u0437\u043b\u043e\u0436\u0438|\u0434\u0435\u043a\u043e\u043c\u043f\u043e\u0437|\u0440\u0430\u0437\u0431\u0435\u0440\u0438|\u043f\u043e\u0447\u0435\u043c\u0443|why|audit|scan|\u043a\u043e\u0440\u043d\u0435\u0432[\u0430-\u044f]+\s+\u043f\u0440\u0438\u0447\u0438\u043d|root\s*cause|\u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c[\u0430-\u044f]*|\u0433\u0434\u0435\s+\u0440\u0430\u0437\u0440\u044b\u0432|\u0447\u0442\u043e\s+\u043c\u0435\u0448\u0430[\u0430-\u044f]+\s+\u0437\u0430\u043a\u0440\u044b\u0442)/iu.test(lower); @@ -3797,7 +3805,18 @@ function hasDirectDeepAnalysisSignal(text) { if (!normalized) { return false; } - return /(?:\u0440\u0430\u0437\u043b\u043e\u0436|\u0446\u0435\u043f\u043e\u0447|lifecycle|\u0440\u0430\u0437\u0440\u044b\u0432|\u043f\u0440\u043e\u0442\u0438\u0432\u043e\u0440\u0435\u0447|\u0430\u043d\u043e\u043c\u0430\u043b|\u043f\u043e\u0447\u0435\u043c\u0443|\u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c|\u0437\u0430\u043a\u0440\u044b\u0442\u0438[\u0435\u044f]\s+\u043f\u0435\u0440\u0438\u043e\u0434|period\s*close|close\s+period|\u0447\u0442\u043e\s+\u043c\u0435\u0448\u0430[\u0430-\u044f]+\s+\u0437\u0430\u043a\u0440\u044b\u0442|state\s+transition|root\s*cause|trace\s*chain)/iu.test(normalized); + const fixedAssetAmortizationCompletenessSignal = + /(?:\u0430\u043c\u043e\u0440\u0442\u0438\u0437|\u043e\u0441\u043d\u043e\u0432\u043d[\u0430-\u044f]*\s+\u0441\u0440\u0435\u0434\u0441\u0442\u0432|fixed\s*asset|depreciat|\b\u043e\u0441\b|\b0?1\s*[/\\]\s*0?2\b|\u0441\u0447[\u0435\u0451]\u0442[^\n]{0,16}\b0?1\b[^\n]{0,16}\b0?2\b)/iu.test(normalized) && + /(?:\u043f\u043e\u043b\u043d[\u0430-\u044f]*|\u043f\u0440\u043e\u043f\u0443\u0449|\u043d\u0430\u0447\u0438\u0441\u043b|\u043f\u0440\u043e\u0432\u0435\u0440|\u043e\u0431\u044a\u0435\u043a\u0442|coverage|missing|expected|actual)/iu.test(normalized); + const deferredExpenseDeepSignal = + /(?:\b97(?:[.,]\d{1,2})?\b|\u0440\u0431\u043f|\u0440\u0430\u0441\u0445\u043e\u0434[\u044b\u043e\u0432\s]+\u0431\u0443\u0434\u0443\u0449[\u0438\u0445\u0435]\s+\u043f\u0435\u0440\u0438\u043e\u0434|deferred)/iu.test(normalized) && + /(?:\u0441\u0447[\u0435\u0451]\u0442|\u043f\u0435\u0440\u0438\u043e\u0434|\u0437\u0430\s+20\d{2}|\u0445\u0432\u043e\u0441\u0442|\u0441\u043f\u0438\u0441\u0430\u043d|\u043e\u0441\u0442\u0430\u0442|\u043e\u0442\u0434\u0435\u043b\u044c\u043d|\u043f\u0440\u043e\u0432\u0435\u0440|account|period|writeoff|tail)/iu.test(normalized); + const actualExpectedStateConflictSignal = + /(?:\u0444\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a[\u0430-\u044f]*[^\n]{0,80}\u043e\u0436\u0438\u0434\u0430\u0435\u043c|\u043e\u0436\u0438\u0434\u0430\u0435\u043c[\u0430-\u044f]*[^\n]{0,80}\u0444\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a|actual[^\n]{0,80}expected|expected[^\n]{0,80}actual|\u043a\u043e\u043d\u0444\u043b\u0438\u043a\u0442[^\n]{0,80}(?:\u0441\u043e\u0441\u0442\u043e\u044f\u043d|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u0440\u0435\u0433\u0438\u0441\u0442\u0440|\u043f\u0440\u043e\u0432\u043e\u0434\u043a|\u0432\u0435\u0442\u0432))/iu.test(normalized); + return fixedAssetAmortizationCompletenessSignal || + deferredExpenseDeepSignal || + actualExpectedStateConflictSignal || + /(?:\u0440\u0430\u0437\u043b\u043e\u0436|\u0446\u0435\u043f\u043e\u0447|lifecycle|\u0440\u0430\u0437\u0440\u044b\u0432|\u043f\u0440\u043e\u0442\u0438\u0432\u043e\u0440\u0435\u0447|\u0430\u043d\u043e\u043c\u0430\u043b|\u043f\u043e\u0447\u0435\u043c\u0443|\u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c|\u0437\u0430\u043a\u0440\u044b\u0442\u0438[\u0435\u044f]\s+\u043f\u0435\u0440\u0438\u043e\u0434|period\s*close|close\s+period|\u0447\u0442\u043e\s+\u043c\u0435\u0448\u0430[\u0430-\u044f]+\s+\u0437\u0430\u043a\u0440\u044b\u0442|state\s+transition|root\s*cause|trace\s*chain)/iu.test(normalized); } function hasStrictDeepInvestigationCue(text) { const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase()); diff --git a/llm_normalizer/backend/tests/addressInventoryAgingFollowup.test.ts b/llm_normalizer/backend/tests/addressInventoryAgingFollowup.test.ts index 93a9694..ec04baa 100644 --- a/llm_normalizer/backend/tests/addressInventoryAgingFollowup.test.ts +++ b/llm_normalizer/backend/tests/addressInventoryAgingFollowup.test.ts @@ -63,7 +63,8 @@ describe("inventory aging follow-up", () => { ); expect(reply.responseType).toBe("FACTUAL_SUMMARY"); - expect(reply.text).toContain("К старым закупкам на 30.09.2021"); + expect(reply.text).toContain("К самым старым закупкам"); + expect(reply.text).toContain("Дата среза: 30.09.2021"); expect(reply.text).toContain("Позиции от самых старых закупок:"); expect(reply.text).toContain("Ограничения:"); expect(reply.text).not.toContain("Блок 1"); diff --git a/llm_normalizer/backend/tests/addressInventoryOrganizationScope.test.ts b/llm_normalizer/backend/tests/addressInventoryOrganizationScope.test.ts index 7c027c0..918a68d 100644 --- a/llm_normalizer/backend/tests/addressInventoryOrganizationScope.test.ts +++ b/llm_normalizer/backend/tests/addressInventoryOrganizationScope.test.ts @@ -138,7 +138,7 @@ describe("inventory organization scope grounding", () => { expect(result?.handled).toBe(true); expect(result?.response_type).toBe("FACTUAL_LIST"); - expect(result?.debug.extracted_filters?.organization).toBe("ООО \\Альтернатива Плюс\\"); + expect(result?.debug.extracted_filters?.organization).toBe("ООО Альтернатива Плюс"); expect(result?.debug.reasons).toContain("organization_grounded_from_observed_rows"); }); @@ -229,7 +229,7 @@ describe("inventory organization scope grounding", () => { expect(result?.handled).toBe(true); expect(result?.response_type).toBe("FACTUAL_LIST"); expect(result?.debug.detected_intent).toBe("inventory_purchase_documents_for_item"); - expect(result?.debug.extracted_filters?.organization).toBe("ООО \\Альтернатива Плюс\\"); + expect(result?.debug.extracted_filters?.organization).toBe("ООО Альтернатива Плюс"); expect(result?.debug.reasons).toContain("organization_grounded_from_observed_rows"); expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1); }); diff --git a/llm_normalizer/backend/tests/addressInventoryPurchaseDocumentRoute.test.ts b/llm_normalizer/backend/tests/addressInventoryPurchaseDocumentRoute.test.ts index ac2f509..a34e39d 100644 --- a/llm_normalizer/backend/tests/addressInventoryPurchaseDocumentRoute.test.ts +++ b/llm_normalizer/backend/tests/addressInventoryPurchaseDocumentRoute.test.ts @@ -84,7 +84,7 @@ describe("inventory purchase provenance document route", () => { expect(result?.debug.extracted_filters?.as_of_date).toBe("2016-01-31"); expect(result?.debug.reasons ?? []).not.toContain("lifecycle_execution_detached_from_snapshot_date"); expect(result?.debug.reasons ?? []).not.toContain("as_of_date_cleared_for_history_recovery"); - expect(String(result?.reply_text ?? "")).toContain("до 31.01.2016 подтвержден поставщик"); + expect(String(result?.reply_text ?? "")).toContain("до 31.01.2016 однозначный поставщик не подтвержден"); expect(String(result?.reply_text ?? "")).toContain("Авант мебель, ООО"); expect(String(result?.reply_text ?? "")).toContain("Для ответа учтены закупочные документы не позже 31.01.2016."); diff --git a/llm_normalizer/backend/tests/assistantMcpRuntimeBridge.test.ts b/llm_normalizer/backend/tests/assistantMcpRuntimeBridge.test.ts index 43d1124..3b76250 100644 --- a/llm_normalizer/backend/tests/assistantMcpRuntimeBridge.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpRuntimeBridge.test.ts @@ -6,15 +6,17 @@ import { afterEach, describe, expect, it, vi } from "vitest"; const MCP_FLAG = "FEATURE_ASSISTANT_MCP_RUNTIME_V1"; const MCP_PROXY = "ASSISTANT_MCP_PROXY_URL"; const MCP_CHANNEL = "ASSISTANT_MCP_CHANNEL"; +const MCP_LIVE_LIMIT = "ASSISTANT_MCP_LIVE_LIMIT"; const ORIGINAL_ENV = { [MCP_FLAG]: process.env[MCP_FLAG], [MCP_PROXY]: process.env[MCP_PROXY], - [MCP_CHANNEL]: process.env[MCP_CHANNEL] + [MCP_CHANNEL]: process.env[MCP_CHANNEL], + [MCP_LIVE_LIMIT]: process.env[MCP_LIVE_LIMIT] }; const TEMP_DIRS: string[] = []; function restoreEnv(): void { - for (const key of [MCP_FLAG, MCP_PROXY, MCP_CHANNEL] as const) { + for (const key of [MCP_FLAG, MCP_PROXY, MCP_CHANNEL, MCP_LIVE_LIMIT] as const) { const original = ORIGINAL_ENV[key]; if (original === undefined) { delete process.env[key]; @@ -36,6 +38,15 @@ function createSnapshotRoot(): string { return root; } +function expectedClaimBoundLimits(primaryCalls: number, carryCalls: number): number[] { + const parsed = Number(process.env[MCP_LIVE_LIMIT] ?? 128); + const liveLimit = Number.isFinite(parsed) ? Math.max(1, Math.trunc(parsed)) : 128; + return [ + ...Array(primaryCalls).fill(Math.max(liveLimit, 96)), + ...Array(carryCalls).fill(Math.max(liveLimit, 128)) + ]; +} + describe.sequential("assistant MCP runtime bridge", () => { afterEach(() => { vi.unstubAllGlobals(); @@ -205,7 +216,7 @@ describe.sequential("assistant MCP runtime bridge", () => { const init = requestInit as { body?: string }; return Number(JSON.parse(String(init.body ?? "{}")).limit ?? 0); }); - expect(rbpCallLimits).toEqual([96, 96, 96, 128]); + expect(rbpCallLimits).toEqual(expectedClaimBoundLimits(3, 1)); expect(liveSummary.matched_rows).toBeGreaterThan(0); expect(result.items.some((item) => Array.isArray((item as Record).relation_pattern_hits))).toBe(true); }); @@ -257,7 +268,7 @@ describe.sequential("assistant MCP runtime bridge", () => { const init = requestInit as { body?: string }; return Number(JSON.parse(String(init.body ?? "{}")).limit ?? 0); }); - expect(faCallLimits).toEqual([96, 96, 128, 128]); + expect(faCallLimits).toEqual(expectedClaimBoundLimits(2, 2)); expect(liveSummary.matched_rows).toBeGreaterThan(0); expect(result.items.some((item) => (item as Record).fa_expected_set_candidate === true)).toBe(true); expect(result.items.some((item) => (item as Record).fa_actual_set_candidate === true)).toBe(true); diff --git a/llm_normalizer/backend/tests/assistantWave17RunRegression20260411.test.ts b/llm_normalizer/backend/tests/assistantWave17RunRegression20260411.test.ts index 6d23e86..b1ddf78 100644 --- a/llm_normalizer/backend/tests/assistantWave17RunRegression20260411.test.ts +++ b/llm_normalizer/backend/tests/assistantWave17RunRegression20260411.test.ts @@ -122,7 +122,13 @@ describe("wave17 run regressions (2026-04-11 real runs)", () => { expect(strongestRevenue?.debug.detected_intent).toBe("customer_revenue_and_payments"); expect(strongestRevenue?.debug.selected_recipe).toBe("address_customer_revenue_and_payments_v1"); expect(strongestReply).toContain( - "\u0422\u043e\u043f-3 \u043b\u0435\u0442 \u043f\u043e \u0441\u0443\u043c\u043c\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439" + "\u0421\u0430\u043c\u044b\u0439 \u0434\u043e\u0445\u043e\u0434\u043d\u044b\u0439 \u0433\u043e\u0434 \u043f\u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043d\u044b\u043c \u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u044f\u043c" + ); + expect(strongestReply).toContain( + "\u0422\u043e\u043f-9 \u043b\u0435\u0442 \u043f\u043e \u0441\u0443\u043c\u043c\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439" + ); + expect(strongestReply).toContain( + "\u0413\u0440\u0430\u043d\u0438\u0446\u0430 \u043e\u0442\u0432\u0435\u0442\u0430: \u044d\u0442\u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043d\u044b\u0439 \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0439 \u043f\u043e\u0442\u043e\u043a" ); const unsupportedTurnover = await service.tryHandle( diff --git a/llm_normalizer/backend/tests/fixtures.ts b/llm_normalizer/backend/tests/fixtures.ts index ffb6e13..83a8fc9 100644 --- a/llm_normalizer/backend/tests/fixtures.ts +++ b/llm_normalizer/backend/tests/fixtures.ts @@ -1,4 +1,20 @@ -import type { NormalizedQueryV1, NormalizedQueryV2, NormalizedQueryV2_0_1, NormalizedQueryV2_0_2 } from "../src/types/normalizer"; +import type { + NormalizedFragmentSemanticHints, + NormalizedQueryV1, + NormalizedQueryV2, + NormalizedQueryV2_0_1, + NormalizedQueryV2_0_2 +} from "../src/types/normalizer"; + +function defaultSemanticHints(): NormalizedFragmentSemanticHints { + return { + scope_target_kind: "none", + scope_target_text: null, + date_scope_kind: "missing", + self_scope_detected: false, + selected_object_scope_detected: false + }; +} export function normalizedFixture(): NormalizedQueryV1 { return { @@ -71,6 +87,7 @@ export function normalizedFixtureV2(): NormalizedQueryV2 { asks_for_evidence: true, mentions_period_close_context: false }, + semantic_hints: defaultSemanticHints(), candidate_labels: ["cross_entity", "anomaly_probe"], confidence: "medium" } @@ -122,6 +139,7 @@ export function normalizedFixtureV2_0_1(): NormalizedQueryV2_0_1 { asks_for_evidence: false, mentions_period_close_context: false }, + semantic_hints: defaultSemanticHints(), candidate_labels: ["rule_based_account_control", "anomaly_probe"], confidence: "high", execution_readiness: "executable_with_soft_assumptions", @@ -171,6 +189,7 @@ export function normalizedFixtureV2_0_2(): NormalizedQueryV2_0_2 { asks_for_evidence: false, mentions_period_close_context: false }, + semantic_hints: defaultSemanticHints(), candidate_labels: ["rule_based_account_control", "anomaly_probe"], confidence: "high", execution_readiness: "executable_with_soft_assumptions",