diff --git a/llm_normalizer/backend/dist/services/addressIntentResolver.js b/llm_normalizer/backend/dist/services/addressIntentResolver.js index 9461936..69ca944 100644 --- a/llm_normalizer/backend/dist/services/addressIntentResolver.js +++ b/llm_normalizer/backend/dist/services/addressIntentResolver.js @@ -1343,14 +1343,16 @@ function hasInventorySaleTraceSignal(text) { return /(?:продаж|покупател|buyer|sale trace|purchase[\s-]?to[\s-]?sale|purchase -> warehouse -> sale|закупка.*продаж)/iu.test(text); } function hasSelectedObjectInventoryCue(text) { - return /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+нему|по\s+ней|по\s+нему\s+же|по\s+ней\s+же|selected\s+object)/iu.test(text); + 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 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) && - /(?:по\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)); + hasPurchaseDocumentsCue); } function hasSelectedObjectInventorySaleTraceSignal(text) { return hasSelectedObjectInventoryCue(text) && (0, inventoryLifecycleCueHelpers_1.hasInventorySaleCue)(text); diff --git a/llm_normalizer/backend/dist/services/addressQueryClassifier.js b/llm_normalizer/backend/dist/services/addressQueryClassifier.js index 45b562d..8e135e9 100644 --- a/llm_normalizer/backend/dist/services/addressQueryClassifier.js +++ b/llm_normalizer/backend/dist/services/addressQueryClassifier.js @@ -281,6 +281,8 @@ function hasSelectedObjectInventoryFollowupSignal(text) { return ((0, inventoryLifecycleCueHelpers_1.hasInventorySupplierCue)(text) || (0, inventoryLifecycleCueHelpers_1.hasInventorySaleCue)(text) || /(?:кто\s+(?:поставил|продал)|по\s+каким\s+документам\s+.*купили)/iu.test(text) || + /(?:к[оа]му|куда)[\s\S]{0,80}(?:поставил|поставили|поставлен|поставлена|поставлено|отгрузил|отгрузили|отгружен|отгружена|отгружено)/iu.test(text) || + /(?:док(?:и|умент[а-яё]*)[\s\S]{0,80}(?:по\s+(?:ним|ней|нему|этой\s+позиции|этому\s+товару)|операци)|(?:по\s+(?:ним|ней|нему|этой\s+позиции|этому\s+товару))[\s\S]{0,80}док(?:и|умент[а-яё]*))/iu.test(text) || (/\bкогда\b/iu.test(text) && (0, inventoryLifecycleCueHelpers_1.hasInventoryPurchaseStem)(text))); } function hasDocsOrBankSignal(text) { diff --git a/llm_normalizer/backend/dist/services/address_runtime/decomposeStage.js b/llm_normalizer/backend/dist/services/address_runtime/decomposeStage.js index e84cfa4..c0502ef 100644 --- a/llm_normalizer/backend/dist/services/address_runtime/decomposeStage.js +++ b/llm_normalizer/backend/dist/services/address_runtime/decomposeStage.js @@ -395,6 +395,9 @@ function shouldRestoreInventoryRootFrame(userMessage, intent, extractedFilters, if (!canReenterInventoryRoot) { return false; } + if (intent !== "unknown" && !isInventoryIntent(intent) && !hasInventoryRootRestatementCue) { + return false; + } if (hasSelectedObjectInventorySignal(normalized) || hasInventorySupplierFollowupCue(normalized) || hasInventoryPurchaseDocumentsFollowupCue(normalized) || @@ -441,7 +444,9 @@ function hasInventorySupplierFollowupCue(text) { return (0, inventoryLifecycleCueHelpers_1.hasInventorySupplierCue)(String(text ?? "")); } function hasInventoryPurchaseDocumentsFollowupCue(text) { - return /(?:по\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(String(text ?? "")); + const value = String(text ?? ""); + return (/(?:по\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) || + /(?:(?:покажи|показать|выведи|дай)?[\s\S]{0,30}док(?:и|умент[а-яё]*)[\s\S]{0,80}(?:по\s+(?:ним|ней|нему|этой\s+позиции|этому\s+товару)|операци)|(?:по\s+(?:ним|ней|нему|этой\s+позиции|этому\s+товару))[\s\S]{0,80}док(?:и|умент[а-яё]*))/iu.test(value)); } function hasInventoryPurchaseDateFollowupCue(text) { const value = String(text ?? ""); @@ -946,6 +951,8 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo if (detectedIntent.intent === "unknown" || detectedIntent.intent === "list_documents_by_counterparty" || detectedIntent.intent === "list_documents_by_contract" || + detectedIntent.intent === "bank_operations_by_counterparty" || + detectedIntent.intent === "bank_operations_by_contract" || detectedIntent.intent === "inventory_on_hand_as_of_date" || detectedIntent.intent === previousIntent) { return { diff --git a/llm_normalizer/backend/dist/services/assistantAddressOrchestrationRuntimeAdapter.js b/llm_normalizer/backend/dist/services/assistantAddressOrchestrationRuntimeAdapter.js index cee61e0..b455fae 100644 --- a/llm_normalizer/backend/dist/services/assistantAddressOrchestrationRuntimeAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantAddressOrchestrationRuntimeAdapter.js @@ -3,10 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.buildAssistantAddressOrchestrationRuntime = buildAssistantAddressOrchestrationRuntime; const assistantRoutePolicyRuntimeAdapter_1 = require("./assistantRoutePolicyRuntimeAdapter"); function hasSelectedObjectInventorySignal(text) { - return /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(String(text ?? "")); + return /(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ним|selected\s+object)/iu.test(String(text ?? "")); } function hasSelectedObjectInventoryActionCue(text) { - return /(?:кому[\s\S]{0,80}продал[аи]?|кому[\s\S]{0,80}реализова[нлт][а-я]*|кому\s+был\s+продан|куда[\s\S]{0,80}продал[аи]?|куда[\s\S]{0,80}реализова[нлт][а-я]*|кто[\s\S]{0,40}купил|кто\s+это\s+поставил|кто\s+поставил|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+мы\s+купили|где\s+куплено|по\s+каким\s+документам|какими\s+документами|покажи\s+документы|документы\s+закупки|buyer|sale\s+trace|supplier|vendor|purchase\s+documents|purchase[\s-]?to[\s-]?sale|old\s+purchase|aged\s+stock)/iu.test(String(text ?? "")); + return /(?:кому[\s\S]{0,80}(?:продал[аи]?|реализова[нлт][а-я]*|поставил[аи]?|поставлен[а-я]*|отгрузил[аи]?|отгружен[а-я]*)|кому\s+был\s+продан|куда[\s\S]{0,80}(?:продал[аи]?|реализова[нлт][а-я]*|поставил[аи]?|поставлен[а-я]*|отгрузил[аи]?|отгружен[а-я]*)|кто[\s\S]{0,40}купил|кто\s+это\s+поставил|кто\s+поставил|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+мы\s+купили|где\s+куплено|по\s+каким\s+документам|какими\s+документами|покажи\s+документы|документы[\s\S]{0,80}(?:по\s+(?:ним|ней|нему|этой\s+позиции|этому\s+товару)|операци)|документы\s+закупки|buyer|sale\s+trace|supplier|vendor|purchase\s+documents|purchase[\s-]?to[\s-]?sale|old\s+purchase|aged\s+stock)/iu.test(String(text ?? "")); } function isGenericCanonicalDriftIntent(intent) { return (intent === "open_items_by_counterparty_or_contract" || diff --git a/llm_normalizer/backend/dist/services/inventoryLifecycleCueHelpers.js b/llm_normalizer/backend/dist/services/inventoryLifecycleCueHelpers.js index 6e532da..ee53209 100644 --- a/llm_normalizer/backend/dist/services/inventoryLifecycleCueHelpers.js +++ b/llm_normalizer/backend/dist/services/inventoryLifecycleCueHelpers.js @@ -28,7 +28,7 @@ function hasInventorySaleCue(text) { return true; } const hasDirectionCue = /(?:кому|каму|куда)/iu.test(value); - const hasSaleVerb = /(?:продал(?:и|а|о|ы)?|продан(?:а|о|ы)?|продано|реализовал(?:и|а|о|ы)?|реализован(?:а|о|ы)?|реализовано|впарил(?:и|а|о|ы)?|отгрузил(?:и|а|о|ы)?|ушло|ушел|ушла)/iu.test(value); + const hasSaleVerb = /(?:продал(?:и|а|о|ы)?|продан(?:а|о|ы)?|продано|реализовал(?:и|а|о|ы)?|реализован(?:а|о|ы)?|реализовано|впарил(?:и|а|о|ы)?|отгрузил(?:и|а|о|ы)?|отгружен(?:а|о|ы)?|поставил(?:и|а|о|ы)?|поставлен(?:а|о|ы)?|ушло|ушел|ушла)/iu.test(value); if (hasDirectionCue && hasSaleVerb) { return true; } diff --git a/llm_normalizer/backend/src/services/addressIntentResolver.ts b/llm_normalizer/backend/src/services/addressIntentResolver.ts index 1ac376a..40a73b7 100644 --- a/llm_normalizer/backend/src/services/addressIntentResolver.ts +++ b/llm_normalizer/backend/src/services/addressIntentResolver.ts @@ -1611,8 +1611,8 @@ function hasInventorySaleTraceSignal(text: string): boolean { } function hasSelectedObjectInventoryCue(text: string): boolean { - return /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+нему|по\s+ней|по\s+нему\s+же|по\s+ней\s+же|selected\s+object)/iu.test( - text + return /(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+нему|по\s+ней|по\s+ним|по\s+нему\s+же|по\s+ней\s+же|selected\s+object)/iu.test( + String(text ?? "") ); } @@ -1621,11 +1621,16 @@ function hasSelectedObjectInventoryProvenanceSignal(text: string): boolean { } function hasSelectedObjectInventoryPurchaseDocumentsSignal(text: string): boolean { - return ( - hasSelectedObjectInventoryCue(text) && + 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 ); } diff --git a/llm_normalizer/backend/src/services/addressQueryClassifier.ts b/llm_normalizer/backend/src/services/addressQueryClassifier.ts index 694635b..c0fc145 100644 --- a/llm_normalizer/backend/src/services/addressQueryClassifier.ts +++ b/llm_normalizer/backend/src/services/addressQueryClassifier.ts @@ -292,6 +292,8 @@ function hasSelectedObjectInventoryFollowupSignal(text: string): boolean { hasInventorySupplierCue(text) || hasInventorySaleCue(text) || /(?:кто\s+(?:поставил|продал)|по\s+каким\s+документам\s+.*купили)/iu.test(text) || + /(?:к[оа]му|куда)[\s\S]{0,80}(?:поставил|поставили|поставлен|поставлена|поставлено|отгрузил|отгрузили|отгружен|отгружена|отгружено)/iu.test(text) || + /(?:док(?:и|умент[а-яё]*)[\s\S]{0,80}(?:по\s+(?:ним|ней|нему|этой\s+позиции|этому\s+товару)|операци)|(?:по\s+(?:ним|ней|нему|этой\s+позиции|этому\s+товару))[\s\S]{0,80}док(?:и|умент[а-яё]*))/iu.test(text) || (/\bкогда\b/iu.test(text) && hasInventoryPurchaseStem(text)) ); } diff --git a/llm_normalizer/backend/src/services/address_runtime/decomposeStage.ts b/llm_normalizer/backend/src/services/address_runtime/decomposeStage.ts index 2f07ade..0474ed0 100644 --- a/llm_normalizer/backend/src/services/address_runtime/decomposeStage.ts +++ b/llm_normalizer/backend/src/services/address_runtime/decomposeStage.ts @@ -506,6 +506,9 @@ function shouldRestoreInventoryRootFrame( if (!canReenterInventoryRoot) { return false; } + if (intent !== "unknown" && !isInventoryIntent(intent) && !hasInventoryRootRestatementCue) { + return false; + } if ( hasSelectedObjectInventorySignal(normalized) || hasInventorySupplierFollowupCue(normalized) || @@ -562,8 +565,14 @@ export function hasInventorySupplierFollowupCue(text: string): boolean { } export function hasInventoryPurchaseDocumentsFollowupCue(text: string): boolean { - return /(?:по\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( - String(text ?? "") + const value = String(text ?? ""); + return ( + /(?:по\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 + ) || + /(?:(?:покажи|показать|выведи|дай)?[\s\S]{0,30}док(?:и|умент[а-яё]*)[\s\S]{0,80}(?:по\s+(?:ним|ней|нему|этой\s+позиции|этому\s+товару)|операци)|(?:по\s+(?:ним|ней|нему|этой\s+позиции|этому\s+товару))[\s\S]{0,80}док(?:и|умент[а-яё]*))/iu.test( + value + ) ); } @@ -1177,6 +1186,8 @@ function deriveIntentWithFollowupContext( detectedIntent.intent === "unknown" || detectedIntent.intent === "list_documents_by_counterparty" || detectedIntent.intent === "list_documents_by_contract" || + detectedIntent.intent === "bank_operations_by_counterparty" || + detectedIntent.intent === "bank_operations_by_contract" || detectedIntent.intent === "inventory_on_hand_as_of_date" || detectedIntent.intent === previousIntent ) { diff --git a/llm_normalizer/backend/src/services/assistantAddressOrchestrationRuntimeAdapter.ts b/llm_normalizer/backend/src/services/assistantAddressOrchestrationRuntimeAdapter.ts index fa21c67..66d9d96 100644 --- a/llm_normalizer/backend/src/services/assistantAddressOrchestrationRuntimeAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantAddressOrchestrationRuntimeAdapter.ts @@ -61,13 +61,13 @@ export interface BuildAssistantAddressOrchestrationRuntimeOutput { } function hasSelectedObjectInventorySignal(text: string | null): boolean { - return /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test( + return /(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ним|selected\s+object)/iu.test( String(text ?? "") ); } function hasSelectedObjectInventoryActionCue(text: string | null): boolean { - return /(?:кому[\s\S]{0,80}продал[аи]?|кому[\s\S]{0,80}реализова[нлт][а-я]*|кому\s+был\s+продан|куда[\s\S]{0,80}продал[аи]?|куда[\s\S]{0,80}реализова[нлт][а-я]*|кто[\s\S]{0,40}купил|кто\s+это\s+поставил|кто\s+поставил|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+мы\s+купили|где\s+куплено|по\s+каким\s+документам|какими\s+документами|покажи\s+документы|документы\s+закупки|buyer|sale\s+trace|supplier|vendor|purchase\s+documents|purchase[\s-]?to[\s-]?sale|old\s+purchase|aged\s+stock)/iu.test( + return /(?:кому[\s\S]{0,80}(?:продал[аи]?|реализова[нлт][а-я]*|поставил[аи]?|поставлен[а-я]*|отгрузил[аи]?|отгружен[а-я]*)|кому\s+был\s+продан|куда[\s\S]{0,80}(?:продал[аи]?|реализова[нлт][а-я]*|поставил[аи]?|поставлен[а-я]*|отгрузил[аи]?|отгружен[а-я]*)|кто[\s\S]{0,40}купил|кто\s+это\s+поставил|кто\s+поставил|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+мы\s+купили|где\s+куплено|по\s+каким\s+документам|какими\s+документами|покажи\s+документы|документы[\s\S]{0,80}(?:по\s+(?:ним|ней|нему|этой\s+позиции|этому\s+товару)|операци)|документы\s+закупки|buyer|sale\s+trace|supplier|vendor|purchase\s+documents|purchase[\s-]?to[\s-]?sale|old\s+purchase|aged\s+stock)/iu.test( String(text ?? "") ); } diff --git a/llm_normalizer/backend/src/services/inventoryLifecycleCueHelpers.ts b/llm_normalizer/backend/src/services/inventoryLifecycleCueHelpers.ts index 6432a1b..b17fc17 100644 --- a/llm_normalizer/backend/src/services/inventoryLifecycleCueHelpers.ts +++ b/llm_normalizer/backend/src/services/inventoryLifecycleCueHelpers.ts @@ -31,7 +31,7 @@ export function hasInventorySaleCue(text: string): boolean { } const hasDirectionCue = /(?:кому|каму|куда)/iu.test(value); const hasSaleVerb = - /(?:продал(?:и|а|о|ы)?|продан(?:а|о|ы)?|продано|реализовал(?:и|а|о|ы)?|реализован(?:а|о|ы)?|реализовано|впарил(?:и|а|о|ы)?|отгрузил(?:и|а|о|ы)?|ушло|ушел|ушла)/iu.test( + /(?:продал(?:и|а|о|ы)?|продан(?:а|о|ы)?|продано|реализовал(?:и|а|о|ы)?|реализован(?:а|о|ы)?|реализовано|впарил(?:и|а|о|ы)?|отгрузил(?:и|а|о|ы)?|отгружен(?:а|о|ы)?|поставил(?:и|а|о|ы)?|поставлен(?:а|о|ы)?|ушло|ушел|ушла)/iu.test( value ); if (hasDirectionCue && hasSaleVerb) { diff --git a/llm_normalizer/backend/tests/addressInventoryRootFrameRegression.test.ts b/llm_normalizer/backend/tests/addressInventoryRootFrameRegression.test.ts index 22771f7..a111084 100644 --- a/llm_normalizer/backend/tests/addressInventoryRootFrameRegression.test.ts +++ b/llm_normalizer/backend/tests/addressInventoryRootFrameRegression.test.ts @@ -62,4 +62,53 @@ describe("inventory root frame regressions", () => { result?.intent.reasons.includes("inventory_selected_object_provenance_signal_detected") ).toBe(true); }); + + it("does not restore inventory root when the follow-up is an explicit receivables root query", () => { + const result = runAddressDecomposeStage("покажи кто нам должен денег на май 2017", { + previous_intent: "inventory_sale_trace_for_item", + previous_filters: { + item: "Четки Пост (84*117)", + as_of_date: "2020-05-31", + period_from: "2020-05-01", + period_to: "2020-05-31" + }, + previous_anchor_type: "item", + previous_anchor_value: "Четки Пост (84*117)", + root_intent: "inventory_on_hand_as_of_date", + root_filters: { + period_from: "2020-05-01", + period_to: "2020-05-31", + as_of_date: "2020-05-31" + }, + root_anchor_type: "unknown", + root_anchor_value: null, + current_frame_kind: "inventory_drilldown" + }); + + expect(result).not.toBeNull(); + expect(result?.intent.intent).toBe("receivables_confirmed_as_of_date"); + expect(result?.intent.reasons).not.toContain("intent_restored_to_inventory_root_frame"); + expect(result?.filters.extracted_filters.as_of_date).toBe("2017-05-31"); + }); + + it("keeps selected-object loose documents wording in the inventory document contour", () => { + const result = runAddressDecomposeStage( + 'По выбранному объекту "Четки Пост (84*117)": меня четки интересуют покажи документы все по ним и все операции', + { + previous_intent: "inventory_on_hand_as_of_date", + previous_filters: { + as_of_date: "2020-05-31", + period_from: "2020-05-01", + period_to: "2020-05-31" + }, + previous_anchor_type: "unknown", + previous_anchor_value: null + } + ); + + expect(result).not.toBeNull(); + expect(result?.intent.intent).toBe("inventory_purchase_documents_for_item"); + expect(result?.intent.intent).not.toBe("bank_operations_by_counterparty"); + expect(result?.filters.extracted_filters.item).toBe("Четки Пост (84*117)"); + }); }); diff --git a/llm_normalizer/backend/tests/addressInventorySaleTraceSelectedObjectRegression.test.ts b/llm_normalizer/backend/tests/addressInventorySaleTraceSelectedObjectRegression.test.ts index 60e70b8..9810e42 100644 --- a/llm_normalizer/backend/tests/addressInventorySaleTraceSelectedObjectRegression.test.ts +++ b/llm_normalizer/backend/tests/addressInventorySaleTraceSelectedObjectRegression.test.ts @@ -75,6 +75,40 @@ describe("inventory sale trace selected-object regressions", () => { expect(query).not.toContain('Номенклатура.Наименование = "Кромка"'); }); + it("routes selected-object delivery wording 'кому мы это поставили' into sale trace", async () => { + executeAddressMcpQueryMock.mockResolvedValueOnce({ + fetched_rows: 1, + matched_rows: 1, + raw_rows: [saleRow], + rows: [], + error: null + }); + + const service = new AddressQueryService(); + const result = await service.tryHandle( + 'По выбранному объекту "Кромка с клеем 33 дуб ниагара 137 м": кому мы это поставили', + { + followupContext: { + previous_intent: "inventory_on_hand_as_of_date", + previous_filters: { + as_of_date: "2021-03-31", + period_from: "2021-03-01", + period_to: "2021-03-31", + organization: "ООО \\Альтернатива Плюс\\" + }, + previous_anchor_type: "unknown", + previous_anchor_value: null + } + } + ); + + expect(result?.handled).toBe(true); + expect(result?.response_type).toBe("FACTUAL_LIST"); + expect(result?.debug.detected_intent).toBe("inventory_sale_trace_for_item"); + expect(result?.debug.extracted_filters?.item).toBe("Кромка с клеем 33 дуб ниагара 137 м"); + expect(String(result?.reply_text ?? "")).toContain("ООО \\Покупатель\\"); + }); + it("keeps the full selected item for canonical selected-object buyer wording", async () => { executeAddressMcpQueryMock.mockResolvedValueOnce({ fetched_rows: 1, diff --git a/llm_normalizer/backend/tests/assistantAddressOrchestrationRuntimeAdapter.test.ts b/llm_normalizer/backend/tests/assistantAddressOrchestrationRuntimeAdapter.test.ts index 555d63d..4f19ad4 100644 --- a/llm_normalizer/backend/tests/assistantAddressOrchestrationRuntimeAdapter.test.ts +++ b/llm_normalizer/backend/tests/assistantAddressOrchestrationRuntimeAdapter.test.ts @@ -314,4 +314,67 @@ describe("assistant address orchestration runtime adapter", () => { }) ); }); + + it("prefers raw selected-object delivery wording over generic canonical drift intent", async () => { + const resolveAddressFollowupCarryoverContext = vi.fn(() => ({ + followupContext: { + previous_intent: "inventory_on_hand_as_of_date", + previous_filters: { + as_of_date: "2021-03-31", + period_from: "2021-03-01", + period_to: "2021-03-31" + } + } + })); + const resolveAssistantOrchestrationDecision = vi.fn(() => ({ + runAddressLane: true, + livingMode: "address_data", + livingReason: "address_lane_triggered", + toolGateDecision: "run_address_lane", + toolGateReason: "address_mode_classifier_detected", + orchestrationContract: { schema_version: "assistant_orchestration_contract_v1" } + })); + const buildAddressLlmPredecomposeContractV1 = vi.fn(({ sourceMessage, canonicalMessage }: { sourceMessage: string; canonicalMessage: string }) => ({ + schema_version: "address_llm_predecompose_contract_v1", + source_message: sourceMessage, + canonical_message: canonicalMessage, + mode: "address_query", + intent: "unknown" + })); + + const rawMessage = 'По выбранному объекту "Кромка с клеем 33 дуб ниагара 137 м": кому мы это поставили'; + + const output = await buildAssistantAddressOrchestrationRuntime( + buildInput({ + userMessage: rawMessage, + runAddressLlmPreDecompose: vi.fn(async () => ({ + attempted: true, + applied: true, + effectiveMessage: "Покажи взаиморасчеты с контрагентом по выбранной позиции", + reason: "normalized_fragment_applied", + predecomposeContract: { + mode: "address_query", + intent: "bank_operations_by_counterparty", + semantics: { + selected_object_scope_detected: true + } + } + })), + buildAddressLlmPredecomposeContractV1, + resolveAddressFollowupCarryoverContext, + resolveAssistantOrchestrationDecision + }) + ); + + expect(output.addressInputMessage).toBe(rawMessage); + expect(output.addressPreDecompose.applied).toBe(false); + expect(output.addressPreDecompose.reason).toBe("followup_raw_message_preferred_over_llm_rewrite"); + expect(resolveAddressFollowupCarryoverContext).toHaveBeenCalledTimes(2); + expect(resolveAssistantOrchestrationDecision).toHaveBeenCalledWith( + expect.objectContaining({ + rawUserMessage: rawMessage, + effectiveAddressUserMessage: rawMessage + }) + ); + }); });