From 5a9b30bcdb6739a57c2db1deafe650e47d1d3658 Mon Sep 17 00:00:00 2001 From: dctouch Date: Fri, 24 Apr 2026 11:47:18 +0300 Subject: [PATCH] =?UTF-8?q?Post-F:=20=D1=80=D0=B0=D1=81=D1=88=D0=B8=D1=80?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20inventory=20trace=20=D0=B8=20aging=20routi?= =?UTF-8?q?ng?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dist/services/addressIntentResolver.js | 2 +- .../services/addressInventoryIntentSignals.js | 37 ++++++++++++ .../src/services/addressIntentResolver.ts | 2 +- .../services/addressInventoryIntentSignals.ts | 56 +++++++++++++++++++ 4 files changed, 95 insertions(+), 2 deletions(-) diff --git a/llm_normalizer/backend/dist/services/addressIntentResolver.js b/llm_normalizer/backend/dist/services/addressIntentResolver.js index 788ce00..90ae436 100644 --- a/llm_normalizer/backend/dist/services/addressIntentResolver.js +++ b/llm_normalizer/backend/dist/services/addressIntentResolver.js @@ -1589,7 +1589,7 @@ function resolveAddressIntent(userMessage) { : ["payables_snapshot_bridge_signal_detected"] }; } - const hasDirectInventoryAgingBridge = /(?:\u043e\u0447\u0435\u043d\u044c\s+\u0434\u0430\u0432\u043d\u043e|\u0434\u0430\u0432\u043d\u043e\s+\u043a\u0443\u043f\u043b|\u0434\u0430\u0432\u043d\u043e\s+\u043f\u0440\u0438\u043e\u0431\u0440\u0435\u0442|\u0441\u0442\u0430\u0440(?:\u044b\u0435|\u044b\u043c|\u044b\u0445)?\s+\u0437\u0430\u043a\u0443\u043f|\u0441\u0442\u0430\u0440(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0442\u043e\u0432\u0430\u0440|old\s+stock|old\s+purchase|very\s+old\s+stock|aging\s+by\s+purchase\s+date)/iu.test(bridgeText); + const hasDirectInventoryAgingBridge = /(?:\u043e\u0447\u0435\u043d\u044c\s+\u0434\u0430\u0432\u043d\u043e|\u0434\u0430\u0432\u043d\u043e\s+\u043a\u0443\u043f\u043b|\u0434\u0430\u0432\u043d\u043e\s+\u043f\u0440\u0438\u043e\u0431\u0440\u0435\u0442|\u0441\u0442\u0430\u0440(?:\u044b\u0435|\u044b\u043c|\u044b\u0445)?\s+\u0437\u0430\u043a\u0443\u043f|\u0441\u0442\u0430\u0440(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0442\u043e\u0432\u0430\u0440|\u0437\u0430\u043a\u0443\u043f\u043b\u0435\u043d(?:\u043d\u044b\u0435|\u043d\u044b\u043c|\u043d\u044b\u0445|\u0430\u044f|\u044b\u0439)?\s+\u0437\u0430\u0434\u043e\u043b\u0433\u043e\s+\u0434\u043e|\u0437\u0430\u0434\u043e\u043b\u0433\u043e\s+\u0434\u043e|old\s+stock|old\s+purchase|very\s+old\s+stock|aging\s+by\s+purchase\s+date)/iu.test(bridgeText); if (hasDirectInventoryAgingBridge || hasInventoryAgingSignal(text) || hasInventoryAgingSignal(repairedText)) { return { intent: "inventory_aging_by_purchase_date", diff --git a/llm_normalizer/backend/dist/services/addressInventoryIntentSignals.js b/llm_normalizer/backend/dist/services/addressInventoryIntentSignals.js index 55625ae..ea9578d 100644 --- a/llm_normalizer/backend/dist/services/addressInventoryIntentSignals.js +++ b/llm_normalizer/backend/dist/services/addressInventoryIntentSignals.js @@ -106,11 +106,28 @@ function hasInventoryPurchaseDocumentsSignalV2(text) { return hasItemCue && hasPurchaseDocCue; } function hasInventorySaleTraceSignalV2(text) { + const value = String(text ?? ""); + const hasPlainItemCue = /(?:товар|номенклатур|позици|продукци|sku|item|product)/iu.test(value); + const hasPlainTraceCue = /(?:кому\s+(?:в\s+итоге\s+)?(?:мы\s+)?(?:продали|реализовали|впарили)|кому\s+(?:был[аио]?|были)?\s*реализован|кто\s+купил|покупател|buyer|sale\s+trace|trace\s+of\s+sale)/iu.test(value); + if (hasPlainItemCue && hasPlainTraceCue) { + return true; + } const hasItemCue = /(?:товар|номенклатур|sku|item|product|позици(?:СЏ|СЋ|Рё)|продукци(?:СЏ|СЋ|Рё))/iu.test(text); const hasTraceCue = /(?:РєРѕРјСѓ\s+(?:РІ\s+итоге\s+)?(?:РјС‹\s+)?продали|РєРѕРјСѓ\s+был\s+продан|РєСѓРґР°\s+(?:РІ\s+итоге\s+)?(?:РјС‹\s+)?продали(?:\s+(?:это|его|товар|позицию))?|РєСѓРґР°\s+(?:была\s+)?реализована\s+(?:позиция|номенклатура|продукция)|кто\s+РєСѓРїРёР»|buyer|sale\s+trace|trace\s+of\s+sale|через\s+какие\s+документы\s+РїСЂРѕС€[её]Р»\s+путь\s+товара|закупк.*склад.*продаж|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*warehouse\s*->\s*sale|purchase\s*->\s*stock\s*->\s*sale)/iu.test(text); return hasItemCue && hasTraceCue; } function hasInventorySupplierStockOverlapSignal(text) { + const value = String(text ?? ""); + const hasPlainDirectSingleItemSupplierQuestion = /(?:от\s+(?:какого|кого)\s+поставщик[а-яё]*\s+куплен\s+(?:товар|номенклатур|позици)|от\s+кого\s+куплен\s+(?:товар|номенклатур|позици))/iu.test(value); + if (hasPlainDirectSingleItemSupplierQuestion) { + return false; + } + const hasPlainSupplierCue = /(?:поставщик|supplier|vendor)/iu.test(value); + const hasPlainStockCue = /(?:склад|остат|леж[аи][т]?|висят|сейчас\s+еще|сейчас\s+ещё|на\s+дату|current\s+stock|stock\s+overlap)/iu.test(value); + const hasPlainUnresolvedSupplierLink = /(?:без\s+понятн[а-яё]*\s+привязк[а-яё]*\s+к\s+поставщик|без\s+привязк[а-яё]*\s+к\s+поставщик|unresolved\s+supplier\s+link)/iu.test(value); + if ((hasPlainSupplierCue && hasPlainStockCue) || (hasPlainUnresolvedSupplierLink && hasPlainStockCue)) { + return true; + } const hasDirectSingleItemSupplierQuestion = /(?:РѕС‚\s+какого\s+поставщика\s+куплен\s+(?:товар|номенклатур(?:Р°|Сѓ|С‹)|позици(?:СЏ|СЋ|Рё))|РѕС‚\s+РєРѕРіРѕ\s+куплен\s+(?:товар|номенклатур(?:Р°|Сѓ|С‹)|позици(?:СЏ|СЋ|Рё)))/iu.test(text); if (hasDirectSingleItemSupplierQuestion) { return false; @@ -120,16 +137,36 @@ function hasInventorySupplierStockOverlapSignal(text) { return hasSupplierCue && hasStockCue; } function hasInventoryAgingSignal(text) { + const value = String(text ?? ""); + const hasPlainResidueCue = /(?:остат|склад|stock\s+residue|stock\s+balance)/iu.test(value); + const hasPlainAgingCue = /(?:стар(?:ые|ым|ых)\s+закупк|очень\s+давно|давно\s+(?:куплен|приобретен|приобретён)|закуплен[а-яё]*\s+задолго\s+до|задолго\s+до(?:\s+\d{4}-\d{2}-\d{2})?|возраст\s+(?:остатк|закупк)|aging\s+by\s+purchase\s+date|old\s+purchase|old\s+purchases|old\s+stock|very\s+old\s+stock|very\s+old\s+purchase)/iu.test(value); + if (hasPlainAgingCue || (hasPlainResidueCue && /(?:давно|задолго\s+до|стар(?:ые|ым|ых)\s+закупк)/iu.test(value))) { + return true; + } const hasResidueCue = /(?:остат(?:РѕРє|РєРё)|РІ\s+остатке|среди\s+текущих\s+остатков|РЅР°\s+складе|stock\s+residue|stock\s+balance)/iu.test(text); const hasAgingCue = /(?:стар(?:ые|ым|ых)\s+закупк|стары(?:Рј|С…)\s+закупк(?:ам|Рё|ах)|относит(?:СЃСЏ|СЃСЏ\s+ли)?\s+.*\s+Рє\s+старым\s+закупк|закупал(?:РёСЃСЊ|СЃСЏ)\s+очень\s+давно|очень\s+давно|давно\s+куплен|давно\s+приобретен|куплен\s+задолго\s+РґРѕ(?:\s+даты)?|закуплен(?:С‹|Р°)?\s+давно|приобретен\s+давно|задолго\s+РґРѕ(?:\s+даты)?|возраст\s+остатк|возраст\s+закупк|aged?\s+stock|old\s+purchase|old\s+purchases|old\s+stock|bought\s+long\s+ago|purchased\s+long\s+ago|aging\s+by\s+purchase\s+date|very\s+old\s+stock|very\s+old\s+purchase|old\s+procurement|older\s+purchases|aged\s+items|old\s+goods)/iu.test(text); return hasAgingCue || (hasResidueCue && /(?:давно\s+куплен|давно\s+приобретен|задолго\s+РґРѕ)/iu.test(text)); } function hasInventoryPurchaseToSaleChainSignal(text) { + const value = String(text ?? ""); + const hasPlainItemCue = /(?:товар|номенклатур|позици|sku|item|product)/iu.test(value); + const hasPlainChainCue = /(?:закупк[а-яё]*\s*->\s*склад\s*->\s*продаж|через\s+какие\s+документы\s+прош[её]л\s+путь|цепочк[а-яё]*\s+движен|документально\s+подтвержденн[а-яё]*\s+цепочк|supplier\s*->\s*item\s*->\s*(?:buyer|customer)|supplier\s+to\s+buyer|supplier\s+to\s+item\s+to\s+buyer|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*(?:warehouse|stock)\s*->\s*sale)/iu.test(value) || value.includes("->"); + if (hasPlainItemCue && hasPlainChainCue) { + return true; + } const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text); const hasChainCue = /(?:закупк.*склад.*продаж|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*(?:warehouse|stock)\s*->\s*sale|закупка\s*->\s*склад\s*->\s*продажа|цепочк[аи]\s+движен|документально\s+подтвержденн\w+\s+цепочк|supplier\s*->\s*item\s*->\s*(?:buyer|customer)|supplier\s+to\s+buyer|supplier\s+to\s+item\s+to\s+buyer)/iu.test(text) || text.includes("->"); return hasItemCue && hasChainCue; } function hasInventorySupplierToBuyerChainSignal(text) { + const value = String(text ?? ""); + const hasPlainSupplierCue = /(?:поставщик|supplier|vendor)/iu.test(value); + const hasPlainBuyerCue = /(?:покупател|buyer|customer|client)/iu.test(value); + const hasPlainItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(value); + const hasPlainChainCue = /(?:документально\s+подтвержденн[а-яё]*\s+цепочк|поставщик\s*->\s*товар\s*->\s*покупател|supplier\s*->\s*item\s*->\s*(?:buyer|customer)|supplier\s*->\s*buyer|supplier\s+to\s+buyer|supplier\s+to\s+item\s+to\s+buyer)/iu.test(value) || value.includes("->"); + if (hasPlainSupplierCue && hasPlainBuyerCue && hasPlainItemCue && hasPlainChainCue) { + return true; + } const hasSupplierCue = /(?:поставщик|supplier|vendor)/iu.test(text); const hasBuyerCue = /(?:покупател|buyer|customer|client)/iu.test(text); const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text); diff --git a/llm_normalizer/backend/src/services/addressIntentResolver.ts b/llm_normalizer/backend/src/services/addressIntentResolver.ts index 8cea930..7280b31 100644 --- a/llm_normalizer/backend/src/services/addressIntentResolver.ts +++ b/llm_normalizer/backend/src/services/addressIntentResolver.ts @@ -2003,7 +2003,7 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti } const hasDirectInventoryAgingBridge = - /(?:\u043e\u0447\u0435\u043d\u044c\s+\u0434\u0430\u0432\u043d\u043e|\u0434\u0430\u0432\u043d\u043e\s+\u043a\u0443\u043f\u043b|\u0434\u0430\u0432\u043d\u043e\s+\u043f\u0440\u0438\u043e\u0431\u0440\u0435\u0442|\u0441\u0442\u0430\u0440(?:\u044b\u0435|\u044b\u043c|\u044b\u0445)?\s+\u0437\u0430\u043a\u0443\u043f|\u0441\u0442\u0430\u0440(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0442\u043e\u0432\u0430\u0440|old\s+stock|old\s+purchase|very\s+old\s+stock|aging\s+by\s+purchase\s+date)/iu.test( + /(?:\u043e\u0447\u0435\u043d\u044c\s+\u0434\u0430\u0432\u043d\u043e|\u0434\u0430\u0432\u043d\u043e\s+\u043a\u0443\u043f\u043b|\u0434\u0430\u0432\u043d\u043e\s+\u043f\u0440\u0438\u043e\u0431\u0440\u0435\u0442|\u0441\u0442\u0430\u0440(?:\u044b\u0435|\u044b\u043c|\u044b\u0445)?\s+\u0437\u0430\u043a\u0443\u043f|\u0441\u0442\u0430\u0440(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0442\u043e\u0432\u0430\u0440|\u0437\u0430\u043a\u0443\u043f\u043b\u0435\u043d(?:\u043d\u044b\u0435|\u043d\u044b\u043c|\u043d\u044b\u0445|\u0430\u044f|\u044b\u0439)?\s+\u0437\u0430\u0434\u043e\u043b\u0433\u043e\s+\u0434\u043e|\u0437\u0430\u0434\u043e\u043b\u0433\u043e\s+\u0434\u043e|old\s+stock|old\s+purchase|very\s+old\s+stock|aging\s+by\s+purchase\s+date)/iu.test( bridgeText ); if (hasDirectInventoryAgingBridge || hasInventoryAgingSignal(text) || hasInventoryAgingSignal(repairedText)) { diff --git a/llm_normalizer/backend/src/services/addressInventoryIntentSignals.ts b/llm_normalizer/backend/src/services/addressInventoryIntentSignals.ts index 0fb4184..7fa97eb 100644 --- a/llm_normalizer/backend/src/services/addressInventoryIntentSignals.ts +++ b/llm_normalizer/backend/src/services/addressInventoryIntentSignals.ts @@ -174,6 +174,15 @@ function hasInventoryPurchaseDocumentsSignalV2(text: string): boolean { } function hasInventorySaleTraceSignalV2(text: string): boolean { + const value = String(text ?? ""); + const hasPlainItemCue = /(?:товар|номенклатур|позици|продукци|sku|item|product)/iu.test(value); + const hasPlainTraceCue = + /(?:кому\s+(?:в\s+итоге\s+)?(?:мы\s+)?(?:продали|реализовали|впарили)|кому\s+(?:был[аио]?|были)?\s*реализован|кто\s+купил|покупател|buyer|sale\s+trace|trace\s+of\s+sale)/iu.test( + value + ); + if (hasPlainItemCue && hasPlainTraceCue) { + return true; + } const hasItemCue = /(?:товар|номенклатур|sku|item|product|позици(?:СЏ|СЋ|Рё)|продукци(?:СЏ|СЋ|Рё))/iu.test(text); const hasTraceCue = /(?:РєРѕРјСѓ\s+(?:РІ\s+итоге\s+)?(?:РјС‹\s+)?продали|РєРѕРјСѓ\s+был\s+продан|РєСѓРґР°\s+(?:РІ\s+итоге\s+)?(?:РјС‹\s+)?продали(?:\s+(?:это|его|товар|позицию))?|РєСѓРґР°\s+(?:была\s+)?реализована\s+(?:позиция|номенклатура|продукция)|кто\s+РєСѓРїРёР»|buyer|sale\s+trace|trace\s+of\s+sale|через\s+какие\s+документы\s+РїСЂРѕС€[её]Р»\s+путь\s+товара|закупк.*склад.*продаж|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*warehouse\s*->\s*sale|purchase\s*->\s*stock\s*->\s*sale)/iu.test( @@ -183,6 +192,24 @@ function hasInventorySaleTraceSignalV2(text: string): boolean { } function hasInventorySupplierStockOverlapSignal(text: string): boolean { + const value = String(text ?? ""); + const hasPlainDirectSingleItemSupplierQuestion = + /(?:от\s+(?:какого|кого)\s+поставщик[а-яё]*\s+куплен\s+(?:товар|номенклатур|позици)|от\s+кого\s+куплен\s+(?:товар|номенклатур|позици))/iu.test( + value + ); + if (hasPlainDirectSingleItemSupplierQuestion) { + return false; + } + const hasPlainSupplierCue = /(?:поставщик|supplier|vendor)/iu.test(value); + const hasPlainStockCue = + /(?:склад|остат|леж[аи][т]?|висят|сейчас\s+еще|сейчас\s+ещё|на\s+дату|current\s+stock|stock\s+overlap)/iu.test(value); + const hasPlainUnresolvedSupplierLink = + /(?:без\s+понятн[а-яё]*\s+привязк[а-яё]*\s+к\s+поставщик|без\s+привязк[а-яё]*\s+к\s+поставщик|unresolved\s+supplier\s+link)/iu.test( + value + ); + if ((hasPlainSupplierCue && hasPlainStockCue) || (hasPlainUnresolvedSupplierLink && hasPlainStockCue)) { + return true; + } const hasDirectSingleItemSupplierQuestion = /(?:РѕС‚\s+какого\s+поставщика\s+куплен\s+(?:товар|номенклатур(?:Р°|Сѓ|С‹)|позици(?:СЏ|СЋ|Рё))|РѕС‚\s+РєРѕРіРѕ\s+куплен\s+(?:товар|номенклатур(?:Р°|Сѓ|С‹)|позици(?:СЏ|СЋ|Рё)))/iu.test( text @@ -198,6 +225,15 @@ function hasInventorySupplierStockOverlapSignal(text: string): boolean { } function hasInventoryAgingSignal(text: string): boolean { + const value = String(text ?? ""); + const hasPlainResidueCue = /(?:остат|склад|stock\s+residue|stock\s+balance)/iu.test(value); + const hasPlainAgingCue = + /(?:стар(?:ые|ым|ых)\s+закупк|очень\s+давно|давно\s+(?:куплен|приобретен|приобретён)|закуплен[а-яё]*\s+задолго\s+до|задолго\s+до(?:\s+\d{4}-\d{2}-\d{2})?|возраст\s+(?:остатк|закупк)|aging\s+by\s+purchase\s+date|old\s+purchase|old\s+purchases|old\s+stock|very\s+old\s+stock|very\s+old\s+purchase)/iu.test( + value + ); + if (hasPlainAgingCue || (hasPlainResidueCue && /(?:давно|задолго\s+до|стар(?:ые|ым|ых)\s+закупк)/iu.test(value))) { + return true; + } const hasResidueCue = /(?:остат(?:РѕРє|РєРё)|РІ\s+остатке|среди\s+текущих\s+остатков|РЅР°\s+складе|stock\s+residue|stock\s+balance)/iu.test(text); const hasAgingCue = @@ -208,6 +244,15 @@ function hasInventoryAgingSignal(text: string): boolean { } function hasInventoryPurchaseToSaleChainSignal(text: string): boolean { + const value = String(text ?? ""); + const hasPlainItemCue = /(?:товар|номенклатур|позици|sku|item|product)/iu.test(value); + const hasPlainChainCue = + /(?:закупк[а-яё]*\s*->\s*склад\s*->\s*продаж|через\s+какие\s+документы\s+прош[её]л\s+путь|цепочк[а-яё]*\s+движен|документально\s+подтвержденн[а-яё]*\s+цепочк|supplier\s*->\s*item\s*->\s*(?:buyer|customer)|supplier\s+to\s+buyer|supplier\s+to\s+item\s+to\s+buyer|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*(?:warehouse|stock)\s*->\s*sale)/iu.test( + value + ) || value.includes("->"); + if (hasPlainItemCue && hasPlainChainCue) { + return true; + } const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text); const hasChainCue = /(?:закупк.*склад.*продаж|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*(?:warehouse|stock)\s*->\s*sale|закупка\s*->\s*склад\s*->\s*продажа|цепочк[аи]\s+движен|документально\s+подтвержденн\w+\s+цепочк|supplier\s*->\s*item\s*->\s*(?:buyer|customer)|supplier\s+to\s+buyer|supplier\s+to\s+item\s+to\s+buyer)/iu.test( @@ -217,6 +262,17 @@ function hasInventoryPurchaseToSaleChainSignal(text: string): boolean { } function hasInventorySupplierToBuyerChainSignal(text: string): boolean { + const value = String(text ?? ""); + const hasPlainSupplierCue = /(?:поставщик|supplier|vendor)/iu.test(value); + const hasPlainBuyerCue = /(?:покупател|buyer|customer|client)/iu.test(value); + const hasPlainItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(value); + const hasPlainChainCue = + /(?:документально\s+подтвержденн[а-яё]*\s+цепочк|поставщик\s*->\s*товар\s*->\s*покупател|supplier\s*->\s*item\s*->\s*(?:buyer|customer)|supplier\s*->\s*buyer|supplier\s+to\s+buyer|supplier\s+to\s+item\s+to\s+buyer)/iu.test( + value + ) || value.includes("->"); + if (hasPlainSupplierCue && hasPlainBuyerCue && hasPlainItemCue && hasPlainChainCue) { + return true; + } const hasSupplierCue = /(?:поставщик|supplier|vendor)/iu.test(text); const hasBuyerCue = /(?:покупател|buyer|customer|client)/iu.test(text); const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text);