diff --git a/llm_normalizer/backend/dist/services/addressInventoryIntentSignals.js b/llm_normalizer/backend/dist/services/addressInventoryIntentSignals.js index 6e3a320..55625ae 100644 --- a/llm_normalizer/backend/dist/services/addressInventoryIntentSignals.js +++ b/llm_normalizer/backend/dist/services/addressInventoryIntentSignals.js @@ -50,12 +50,21 @@ function hasInventoryOnHandSignal(text) { (hasRequestCue || hasBalanceLexeme || hasColloquialStockSnapshotCue || hasStockStateCue); } function hasSelectedObjectInventoryCue(text) { + const value = String(text ?? ""); + if (/(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+нему|по\s+ней|по\s+ним|по\s+нему\s+же|по\s+ней\s+же|selected\s+object)/iu.test(value)) { + return true; + } return /(?:РїРѕ\s+выбранному\s+объекту|РїРѕ\s+выбранной\s+позиции|РїРѕ\s+этой\s+позиции|РїРѕ\s+этому\s+товару|РїРѕ\s+нему|РїРѕ\s+ней|РїРѕ\s+РЅРёРј|РїРѕ\s+нему\s+Р¶Рµ|РїРѕ\s+ней\s+Р¶Рµ|selected\s+object)/iu.test(String(text ?? "")); } function hasSelectedObjectInventoryProvenanceSignal(text) { return hasSelectedObjectInventoryCue(text) && (0, inventoryLifecycleCueHelpers_1.hasInventorySupplierCue)(text); } function hasSelectedObjectInventoryPurchaseDocumentsSignal(text) { + const value = String(text ?? ""); + if (hasSelectedObjectInventoryCue(value) && + /(?:по\s+каким\s+документам\s+(?:это|его|этот\s+товар|эту\s+позицию)?\s*купили|по\s+каким\s+документам\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)?\s*купили|какими\s+документами\s+(?:был\s+)?куплен|покажи\s+документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(value)) { + return true; + } const hasPurchaseDocumentsCue = /(?:РїРѕ\s+каким\s+документам\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|РїРѕ\s+каким\s+документам\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|какими\s+документами\s+(?:был\s+)?куплен|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(text) || /(?:(?:РїРѕ\s+каким|какими)\s+РґРѕРє[Р°-СЏС‘]*[\s\S]{0,80}(?:РєСѓРїРёР»|куплен)|РґРѕРє(?:Рё|умент[Р°-СЏС‘]*)[\s\S]{0,80}(?:РїРѕ\s+(?:РЅРёРј|ней|нему|этой\s+позиции|этому\s+товару)|операци)|(?:РїРѕ\s+(?:РЅРёРј|ней|нему|этой\s+позиции|этому\s+товару))[\s\S]{0,80}РґРѕРє(?:Рё|умент[Р°-СЏС‘]*))/iu.test(text); return hasSelectedObjectInventoryCue(text) && hasPurchaseDocumentsCue; @@ -67,6 +76,13 @@ function hasSelectedObjectInventoryProfitabilitySignal(text) { return hasSelectedObjectInventoryCue(text) && (0, inventoryLifecycleCueHelpers_1.hasInventoryProfitabilityCue)(text); } function hasInventoryProvenanceSignalV2(text) { + const value = String(text ?? ""); + const hasPlainItemCue = /(?:товар|номенклатур|sku|item|product|остат|склад)/iu.test(value); + const hasPlainSupplierCue = (0, inventoryLifecycleCueHelpers_1.hasInventorySupplierCue)(value) || /(?:кем\s+поставлен|кто\s+(?:это|его|этот\s+товар|эту\s+позицию)?\s*поставил)/iu.test(value); + const hasPlainPurchaseCue = /(?:куплен(?:ы|а|о)?|закупк|происхожд|откуда|где\s+(?:мы\s+)?купили|поставлен(?:ы|а)?|purchase\s+provenance|purchase\s+date)/iu.test(value) || (0, inventoryLifecycleCueHelpers_1.hasInventoryPurchaseStem)(value); + if (hasPlainItemCue && hasPlainSupplierCue && hasPlainPurchaseCue) { + return true; + } const hasItemCue = /(?:товар|номенклатур|sku|item|product|остат(?:РѕРє|РєРё)|склад)/iu.test(text); const hasSupplierCue = (0, inventoryLifecycleCueHelpers_1.hasInventorySupplierCue)(text) || /кем\s+поставлен/iu.test(text); const hasPurchaseCue = /(?:куплен(?:С‹|Р°|Рѕ)?|закупк|происхождени|откуда|РіРґРµ\s+(?:РјС‹\s+)?купили(?:\s+(?:это|его|товар|позицию))?|РіРґРµ\s+куплено|РєРѕРіРґР°\s+был\s+куплен|РєРѕРіРґР°\s+куплен|дата\s+закупк|кто\s+(?:нам\s+)?поставил|кем\s+поставлен|поставлен(?:С‹|Р°)?|purchase\s+provenance|purchase\s+date)/iu.test(text) || (0, inventoryLifecycleCueHelpers_1.hasInventoryPurchaseStem)(text); @@ -79,6 +95,12 @@ function hasInventoryPurchaseDateSignal(text) { return hasItemCue && hasPurchaseDateCue; } function hasInventoryPurchaseDocumentsSignalV2(text) { + const value = String(text ?? ""); + const hasPlainItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(value); + const hasPlainPurchaseDocCue = /(?:по\s+каким\s+документам\s+(?:был\s+)?куплен|по\s+каким\s+документам\s+(?:это|его|этот\s+товар|эту\s+позицию)?\s*купили|какими\s+документами\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)?\s*купили|документ[а-яё]*\s+закупк|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(value); + if (hasPlainItemCue && hasPlainPurchaseDocCue) { + return true; + } const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text); const hasPurchaseDocCue = /(?:РїРѕ\s+каким\s+документам\s+был\s+куплен|РїРѕ\s+каким\s+документам\s+куплен|какими\s+документами\s+был\s+куплен|документ(?:ам|С‹)\s+закупк|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(text); return hasItemCue && hasPurchaseDocCue; diff --git a/llm_normalizer/backend/dist/services/addressRecipeCatalog.js b/llm_normalizer/backend/dist/services/addressRecipeCatalog.js index 06bcb1b..0d6239e 100644 --- a/llm_normalizer/backend/dist/services/addressRecipeCatalog.js +++ b/llm_normalizer/backend/dist/services/addressRecipeCatalog.js @@ -1446,7 +1446,7 @@ function buildAddressRecipePlan(recipe, filters) { : recipe.query_template === "inventory_purchase_provenance_profile" ? buildInventoryPurchaseDocumentQuery(filters, resolvedLimit) : recipe.query_template === "inventory_purchase_documents_profile" - ? buildInventoryPurchaseDocumentQuery(filters, resolvedLimit) + ? buildInventoryMovementQuery(filters, resolvedLimit, "dt") : recipe.query_template === "inventory_supplier_stock_overlap_profile" ? buildInventoryMovementQuery(filters, resolvedLimit, "dt") : recipe.query_template === "inventory_sale_trace_profile" diff --git a/llm_normalizer/backend/dist/services/address_runtime/decomposeStage.js b/llm_normalizer/backend/dist/services/address_runtime/decomposeStage.js index 5c4a345..6f6956c 100644 --- a/llm_normalizer/backend/dist/services/address_runtime/decomposeStage.js +++ b/llm_normalizer/backend/dist/services/address_runtime/decomposeStage.js @@ -1168,6 +1168,7 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo followupContext.current_frame_kind === "inventory_drilldown"; const inventorySelectedObjectFollowup = inventoryLineageActive && (hasSelectedObjectInventorySignal(normalizedMessage) || (previousIsInventoryFamily && hasFollowupSignal)); + const hasExplicitInventoryItemReference = /(?:товар|номенклатур|позици|склад|остат|sku|item|product|товар|номенклатур|позици|склад|остат)/iu.test(normalizedMessage) || hasSelectedObjectInlineSnapshotMetadata(normalizedMessage); const inventoryPurchaseDateVatBridge = inventorySelectedObjectFollowup && hasInventoryPurchaseDateVatBridgeCue(normalizedMessage); if (inventoryPurchaseDateVatBridge && (detectedIntent.intent === "unknown" || @@ -1191,6 +1192,16 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo reasons: [...detectedIntent.reasons, "intent_adjusted_to_vat_followup_context"] }; } + if (!inventoryLineageActive && + hasAnyPartyAnchor && + !hasExplicitInventoryItemReference && + detectedIntent.intent === "inventory_purchase_documents_for_item") { + return { + intent: hasPreviousContract && !hasPreviousCounterparty ? "list_documents_by_contract" : "list_documents_by_counterparty", + confidence: "low", + reasons: [...detectedIntent.reasons, "intent_adjusted_from_non_inventory_followup_context"] + }; + } const allowOpenItemsFollowupFallback = detectedIntent.intent === "unknown" && !isVatFollowup; if (allowOpenItemsFollowupFallback && !inventorySelectedObjectFollowup && diff --git a/llm_normalizer/backend/src/services/addressInventoryIntentSignals.ts b/llm_normalizer/backend/src/services/addressInventoryIntentSignals.ts index b6e04e6..0fb4184 100644 --- a/llm_normalizer/backend/src/services/addressInventoryIntentSignals.ts +++ b/llm_normalizer/backend/src/services/addressInventoryIntentSignals.ts @@ -79,6 +79,14 @@ function hasInventoryOnHandSignal(text: string): boolean { } function hasSelectedObjectInventoryCue(text: string): boolean { + const value = String(text ?? ""); + if ( + /(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+нему|по\s+ней|по\s+ним|по\s+нему\s+же|по\s+ней\s+же|selected\s+object)/iu.test( + value + ) + ) { + return true; + } return /(?:РїРѕ\s+выбранному\s+объекту|РїРѕ\s+выбранной\s+позиции|РїРѕ\s+этой\s+позиции|РїРѕ\s+этому\s+товару|РїРѕ\s+нему|РїРѕ\s+ней|РїРѕ\s+РЅРёРј|РїРѕ\s+нему\s+Р¶Рµ|РїРѕ\s+ней\s+Р¶Рµ|selected\s+object)/iu.test( String(text ?? "") ); @@ -89,6 +97,15 @@ function hasSelectedObjectInventoryProvenanceSignal(text: string): boolean { } function hasSelectedObjectInventoryPurchaseDocumentsSignal(text: string): boolean { + const value = String(text ?? ""); + if ( + hasSelectedObjectInventoryCue(value) && + /(?:по\s+каким\s+документам\s+(?:это|его|этот\s+товар|эту\s+позицию)?\s*купили|по\s+каким\s+документам\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)?\s*купили|какими\s+документами\s+(?:был\s+)?куплен|покажи\s+документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test( + value + ) + ) { + return true; + } const hasPurchaseDocumentsCue = /(?:РїРѕ\s+каким\s+документам\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|РїРѕ\s+каким\s+документам\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|какими\s+документами\s+(?:был\s+)?куплен|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test( text @@ -108,6 +125,17 @@ function hasSelectedObjectInventoryProfitabilitySignal(text: string): boolean { } function hasInventoryProvenanceSignalV2(text: string): boolean { + const value = String(text ?? ""); + const hasPlainItemCue = /(?:товар|номенклатур|sku|item|product|остат|склад)/iu.test(value); + const hasPlainSupplierCue = + hasInventorySupplierCue(value) || /(?:кем\s+поставлен|кто\s+(?:это|его|этот\s+товар|эту\s+позицию)?\s*поставил)/iu.test(value); + const hasPlainPurchaseCue = + /(?:куплен(?:ы|а|о)?|закупк|происхожд|откуда|где\s+(?:мы\s+)?купили|поставлен(?:ы|а)?|purchase\s+provenance|purchase\s+date)/iu.test( + value + ) || hasInventoryPurchaseStem(value); + if (hasPlainItemCue && hasPlainSupplierCue && hasPlainPurchaseCue) { + return true; + } const hasItemCue = /(?:товар|номенклатур|sku|item|product|остат(?:РѕРє|РєРё)|склад)/iu.test(text); const hasSupplierCue = hasInventorySupplierCue(text) || /кем\s+поставлен/iu.test(text); const hasPurchaseCue = @@ -129,6 +157,15 @@ function hasInventoryPurchaseDateSignal(text: string): boolean { } function hasInventoryPurchaseDocumentsSignalV2(text: string): boolean { + const value = String(text ?? ""); + const hasPlainItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(value); + const hasPlainPurchaseDocCue = + /(?:по\s+каким\s+документам\s+(?:был\s+)?куплен|по\s+каким\s+документам\s+(?:это|его|этот\s+товар|эту\s+позицию)?\s*купили|какими\s+документами\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)?\s*купили|документ[а-яё]*\s+закупк|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test( + value + ); + if (hasPlainItemCue && hasPlainPurchaseDocCue) { + return true; + } const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text); const hasPurchaseDocCue = /(?:РїРѕ\s+каким\s+документам\s+был\s+куплен|РїРѕ\s+каким\s+документам\s+куплен|какими\s+документами\s+был\s+куплен|документ(?:ам|С‹)\s+закупк|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test( text diff --git a/llm_normalizer/backend/src/services/addressRecipeCatalog.ts b/llm_normalizer/backend/src/services/addressRecipeCatalog.ts index f101791..75ca9ae 100644 --- a/llm_normalizer/backend/src/services/addressRecipeCatalog.ts +++ b/llm_normalizer/backend/src/services/addressRecipeCatalog.ts @@ -1,4 +1,4 @@ -import type { +import type { AddressFilterSet, AddressIntent, AddressRecipeDefinition, @@ -1610,7 +1610,7 @@ export function buildAddressRecipePlan( : recipe.query_template === "inventory_purchase_provenance_profile" ? buildInventoryPurchaseDocumentQuery(filters, resolvedLimit) : recipe.query_template === "inventory_purchase_documents_profile" - ? buildInventoryPurchaseDocumentQuery(filters, resolvedLimit) + ? buildInventoryMovementQuery(filters, resolvedLimit, "dt") : recipe.query_template === "inventory_supplier_stock_overlap_profile" ? buildInventoryMovementQuery(filters, resolvedLimit, "dt") : recipe.query_template === "inventory_sale_trace_profile" diff --git a/llm_normalizer/backend/src/services/address_runtime/decomposeStage.ts b/llm_normalizer/backend/src/services/address_runtime/decomposeStage.ts index f3085f0..e011840 100644 --- a/llm_normalizer/backend/src/services/address_runtime/decomposeStage.ts +++ b/llm_normalizer/backend/src/services/address_runtime/decomposeStage.ts @@ -1461,6 +1461,10 @@ function deriveIntentWithFollowupContext( const inventorySelectedObjectFollowup = inventoryLineageActive && (hasSelectedObjectInventorySignal(normalizedMessage) || (previousIsInventoryFamily && hasFollowupSignal)); + const hasExplicitInventoryItemReference = + /(?:товар|номенклатур|позици|склад|остат|sku|item|product|товар|номенклатур|позици|склад|остат)/iu.test( + normalizedMessage + ) || hasSelectedObjectInlineSnapshotMetadata(normalizedMessage); const inventoryPurchaseDateVatBridge = inventorySelectedObjectFollowup && hasInventoryPurchaseDateVatBridgeCue(normalizedMessage); @@ -1490,6 +1494,19 @@ function deriveIntentWithFollowupContext( }; } + if ( + !inventoryLineageActive && + hasAnyPartyAnchor && + !hasExplicitInventoryItemReference && + detectedIntent.intent === "inventory_purchase_documents_for_item" + ) { + return { + intent: hasPreviousContract && !hasPreviousCounterparty ? "list_documents_by_contract" : "list_documents_by_counterparty", + confidence: "low", + reasons: [...detectedIntent.reasons, "intent_adjusted_from_non_inventory_followup_context"] + }; + } + const allowOpenItemsFollowupFallback = detectedIntent.intent === "unknown" && !isVatFollowup; if ( allowOpenItemsFollowupFallback &&