АРЧ АП11 - Добавить item-profitability intent для selected-object follow-up в inventory contour

This commit is contained in:
dctouch 2026-04-16 09:00:43 +03:00
parent a493e2fd69
commit d7c4eb781a
23 changed files with 329 additions and 8 deletions

View File

@ -808,6 +808,7 @@ function isInventoryTraceIntent(intent) {
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_profitability_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date");
}
@ -816,6 +817,7 @@ function isInventoryItemAnchoredIntent(intent) {
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_aging_by_purchase_date" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_profitability_for_item" ||
intent === "inventory_purchase_to_sale_chain");
}
function usesRecipeDefaultLimit(intent) {
@ -824,6 +826,7 @@ function usesRecipeDefaultLimit(intent) {
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_profitability_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date");
}
@ -1197,6 +1200,7 @@ function requiredFiltersByIntent(intent) {
}
if (intent === "inventory_purchase_provenance_for_item" ||
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_profitability_for_item" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_purchase_to_sale_chain") {
return ["item"];
@ -1234,6 +1238,7 @@ function usesAsOfPrimaryWindow(intent) {
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_profitability_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date" ||
intent === "open_items_by_counterparty_or_contract" ||

View File

@ -1357,6 +1357,9 @@ function hasSelectedObjectInventoryPurchaseDocumentsSignal(text) {
function hasSelectedObjectInventorySaleTraceSignal(text) {
return hasSelectedObjectInventoryCue(text) && (0, inventoryLifecycleCueHelpers_1.hasInventorySaleCue)(text);
}
function hasSelectedObjectInventoryProfitabilitySignal(text) {
return hasSelectedObjectInventoryCue(text) && (0, inventoryLifecycleCueHelpers_1.hasInventoryProfitabilityCue)(text);
}
function hasInventoryProvenanceSignalV2(text) {
const hasItemCue = /(?:товар|номенклатур|sku|item|product|остат(?:ок|ки)|склад)/iu.test(text);
const hasSupplierCue = (0, inventoryLifecycleCueHelpers_1.hasInventorySupplierCue)(text) || /кем\s+поставлен/iu.test(text);
@ -1593,6 +1596,13 @@ function resolveAddressIntent(userMessage) {
reasons: ["inventory_purchase_documents_signal_detected"]
};
}
if (hasSelectedObjectInventoryProfitabilitySignal(text)) {
return {
intent: "inventory_profitability_for_item",
confidence: "medium",
reasons: ["inventory_selected_object_profitability_signal_detected"]
};
}
if (hasSelectedObjectInventorySaleTraceSignal(text)) {
return {
intent: "inventory_sale_trace_for_item",

View File

@ -27,6 +27,7 @@ const DISPLAY_ENTITY_TYPE_BY_INTENT = {
inventory_purchase_documents_for_item: "item",
inventory_supplier_stock_overlap_as_of_date: "item",
inventory_sale_trace_for_item: "item",
inventory_profitability_for_item: "item",
inventory_purchase_to_sale_chain: "item",
inventory_aging_by_purchase_date: "item"
};
@ -51,6 +52,7 @@ const RESULT_SET_TYPE_BY_INTENT = {
inventory_purchase_documents_for_item: "inventory_trace",
inventory_supplier_stock_overlap_as_of_date: "inventory_trace",
inventory_sale_trace_for_item: "inventory_trace",
inventory_profitability_for_item: "inventory_trace",
inventory_purchase_to_sale_chain: "inventory_trace",
inventory_aging_by_purchase_date: "inventory_trace",
period_coverage_profile: "profile_summary",

View File

@ -279,6 +279,7 @@ function hasSelectedObjectInventoryFollowupSignal(text) {
return false;
}
return ((0, inventoryLifecycleCueHelpers_1.hasInventorySupplierCue)(text) ||
(0, inventoryLifecycleCueHelpers_1.hasInventoryProfitabilityCue)(text) ||
(0, inventoryLifecycleCueHelpers_1.hasInventorySaleCue)(text) ||
/(?:кто\s+(?:поставил|продал)|по\s+каким\s+документам\s+.*купили)/iu.test(text) ||
/(?:к[оа]му|куда)[\s\S]{0,80}(?:поставил|поставили|поставлен|поставлена|поставлено|отгрузил|отгрузили|отгружен|отгружена|отгружено)/iu.test(text) ||

View File

@ -1239,6 +1239,7 @@ function isOrganizationScopedInventoryIntent(intent) {
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_profitability_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date");
}
@ -1925,6 +1926,7 @@ function normalizeMissingAnchorLabel(anchor) {
}
function buildLimitedScopeLine(filters) {
const organization = toNonEmptyFilterValue(filters.organization);
const item = toNonEmptyFilterValue(filters.item);
const asOfDate = toNonEmptyFilterValue(filters.as_of_date);
const periodFrom = toNonEmptyFilterValue(filters.period_from);
const periodTo = toNonEmptyFilterValue(filters.period_to);
@ -1932,6 +1934,9 @@ function buildLimitedScopeLine(filters) {
if (organization) {
scopeParts.push(`организация ${organization}`);
}
if (item) {
scopeParts.push(`товар ${item}`);
}
if (asOfDate) {
scopeParts.push(`срез на ${asOfDate}`);
}
@ -1951,6 +1956,7 @@ function buildLimitedVariantSeedFingerprint(filters) {
"contract",
"account",
"document_ref",
"item",
"as_of_date",
"period_from",
"period_to"
@ -2001,6 +2007,9 @@ function buildLimitedOffers(input) {
else if (input.intent === "inventory_sale_trace_for_item") {
offers.push("показать подтвержденные движения выбытия товара со счета 41.01");
}
else if (input.intent === "inventory_profitability_for_item") {
offers.push("показать выручку, прибыль или маржу по выбранному товару за период продаж");
}
else if (input.intent === "inventory_purchase_to_sale_chain") {
offers.push("показать документальную цепочку по товару: поступление на 41.01 и последующее выбытие");
}
@ -2060,6 +2069,7 @@ function buildLimitedIntentSignalLine(input) {
list_receivables_counterparties: "Сигнал запроса: нужен ранжированный список должников.",
list_payables_counterparties: "Сигнал запроса: нужен ранжированный список кредиторов.",
inventory_on_hand_as_of_date: "Сигнал запроса: нужен подтвержденный срез товаров на складе на дату.",
inventory_profitability_for_item: "Сигнал запроса: нужен расчет выручки/прибыли/маржи по выбранной номенклатуре.",
receivables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез дебиторской задолженности на дату.",
payables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез обязательств к оплате на дату.",
vat_payable_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез НДС к уплате на дату.",
@ -2083,6 +2093,7 @@ function hasAggregateLimitedSignal(input) {
input.intent === "contract_usage_overview" ||
input.intent === "supplier_payouts_profile" ||
input.intent === "customer_revenue_and_payments" ||
input.intent === "inventory_profitability_for_item" ||
input.intent === "contract_usage_and_value") {
return true;
}

View File

@ -2,6 +2,7 @@
Object.defineProperty(exports, "__esModule", { value: true });
exports.hasInventorySupplierFollowupCue = hasInventorySupplierFollowupCue;
exports.hasInventoryPurchaseDocumentsFollowupCue = hasInventoryPurchaseDocumentsFollowupCue;
exports.hasInventoryProfitabilityFollowupCue = hasInventoryProfitabilityFollowupCue;
exports.hasInventoryPurchaseDateFollowupCue = hasInventoryPurchaseDateFollowupCue;
exports.hasBareInventoryPurchaseDateFollowupCue = hasBareInventoryPurchaseDateFollowupCue;
exports.hasInventorySaleFollowupCue = hasInventorySaleFollowupCue;
@ -261,6 +262,7 @@ function isInventoryIntent(intent) {
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_profitability_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date");
}
@ -271,6 +273,7 @@ function isInventoryDrilldownFrameIntent(intent) {
return (intent === "inventory_purchase_provenance_for_item" ||
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_profitability_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date");
}
@ -448,6 +451,9 @@ 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(value) ||
/(?:(?:покажи|показать|выведи|дай)?[\s\S]{0,30}док(?:и|умент[а-яё]*)[\s\S]{0,80}(?:по\s+(?:ним|ней|нему|этой\s+позиции|этому\s+товару)|операци)|(?:по\s+(?:ним|ней|нему|этой\s+позиции|этому\s+товару))[\s\S]{0,80}док(?:и|умент[а-яё]*))/iu.test(value));
}
function hasInventoryProfitabilityFollowupCue(text) {
return (0, inventoryLifecycleCueHelpers_1.hasInventoryProfitabilityCue)(String(text ?? ""));
}
function hasInventoryPurchaseDateFollowupCue(text) {
const value = String(text ?? "");
return /(?:когда\s+(?:примерно\s+)?(?:мы\s+)?купили|когда\s+был\s+куплен|когда\s+куплен|когда\s+это\s+купили|когда\s+эту\s+позицию\s+купили|когда\s+ее\s+купили|дата\s+закупки|purchase\s+date)/iu.test(value) || (/когда/iu.test(value) && (0, inventoryLifecycleCueHelpers_1.hasInventoryPurchaseStem)(value));
@ -622,6 +628,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_profitability_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date" ||
intent === "payables_confirmed_as_of_date" ||
@ -650,6 +657,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
if ((intent === "inventory_purchase_provenance_for_item" ||
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_profitability_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date")) {
const inheritedItem = previousItem ?? previousAnchorItem;
@ -874,6 +882,7 @@ function resolveMissingRequiredFilters(intent, filters) {
account_balance_snapshot: ["account", "as_of_date"],
documents_forming_balance: ["account", "as_of_date"],
inventory_on_hand_as_of_date: ["as_of_date"],
inventory_profitability_for_item: ["item"],
open_contracts_confirmed_as_of_date: ["as_of_date"],
payables_confirmed_as_of_date: ["as_of_date"],
receivables_confirmed_as_of_date: ["as_of_date"],
@ -975,6 +984,23 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
};
}
}
if (inventorySelectedObjectFollowup && hasInventoryProfitabilityFollowupCue(normalizedMessage)) {
if (detectedIntent.intent === "unknown" ||
detectedIntent.intent === "customer_revenue_and_payments" ||
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 === "inventory_sale_trace_for_item" ||
detectedIntent.intent === previousIntent) {
return {
intent: "inventory_profitability_for_item",
confidence: "low",
reasons: [...detectedIntent.reasons, "intent_adjusted_to_inventory_followup_context"]
};
}
}
if (inventorySelectedObjectFollowup && hasInventoryPurchaseDateFollowupCue(normalizedMessage)) {
if (detectedIntent.intent === "unknown" ||
detectedIntent.intent === "inventory_purchase_provenance_for_item" ||

View File

@ -199,6 +199,7 @@ function resolvePrimaryAnchor(intent, filters) {
if ((intent === "inventory_purchase_provenance_for_item" ||
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_profitability_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date") &&
item) {

View File

@ -2,14 +2,17 @@
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildAssistantAddressOrchestrationRuntime = buildAssistantAddressOrchestrationRuntime;
const assistantRoutePolicyRuntimeAdapter_1 = require("./assistantRoutePolicyRuntimeAdapter");
const inventoryLifecycleCueHelpers_1 = require("./inventoryLifecycleCueHelpers");
function hasSelectedObjectInventorySignal(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+продан|куда[\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 ?? ""));
const value = 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(value) || (0, inventoryLifecycleCueHelpers_1.hasInventoryProfitabilityCue)(value));
}
function isGenericCanonicalDriftIntent(intent) {
return (intent === "open_items_by_counterparty_or_contract" ||
intent === "customer_revenue_and_payments" ||
intent === "list_documents_by_counterparty" ||
intent === "list_documents_by_contract" ||
intent === "bank_operations_by_counterparty" ||

View File

@ -2505,6 +2505,7 @@ function isInventoryDrilldownFrameIntent(intent) {
return intent === "inventory_purchase_provenance_for_item" ||
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_profitability_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date";
}
@ -2781,6 +2782,7 @@ function isInventorySelectedObjectIntent(intent) {
return intent === "inventory_purchase_provenance_for_item" ||
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_profitability_for_item" ||
intent === "inventory_purchase_to_sale_chain";
}
function hasShortInventoryObjectFollowupSignal(userMessage) {
@ -3103,6 +3105,7 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
sourceIntentHint === "inventory_purchase_provenance_for_item" ||
sourceIntentHint === "inventory_purchase_documents_for_item" ||
sourceIntentHint === "inventory_sale_trace_for_item" ||
sourceIntentHint === "inventory_profitability_for_item" ||
sourceIntentHint === "inventory_purchase_to_sale_chain" ||
sourceIntentHint === "inventory_aging_by_purchase_date" ||
hasSelectedObjectInventorySignalPrimary ||
@ -3534,6 +3537,7 @@ function resolveRequiredAnchorTypeForIntent(intent) {
if (intent === "inventory_purchase_provenance_for_item" ||
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_profitability_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date") {
return "item";
@ -3586,7 +3590,9 @@ function hasSelectedObjectInventoryFollowupSignalForPredecompose(text) {
return /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(String(text ?? ""));
}
function isInventorySelectedObjectFollowupIntent(intent) {
return intent === "inventory_purchase_provenance_for_item" || intent === "inventory_purchase_documents_for_item";
return intent === "inventory_purchase_provenance_for_item" ||
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_profitability_for_item";
}
function hasSameDateAccountFollowupSignalForPredecompose(text) {
const source = String(text ?? "");
@ -4269,6 +4275,7 @@ const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([
"inventory_purchase_documents_for_item",
"inventory_supplier_stock_overlap_as_of_date",
"inventory_sale_trace_for_item",
"inventory_profitability_for_item",
"inventory_purchase_to_sale_chain",
"inventory_aging_by_purchase_date",
"contract_usage_overview",
@ -4281,6 +4288,7 @@ const ADDRESS_INTENTS_ALLOW_STRICT_DEEP_INVESTIGATION_BYPASS = new Set([
"inventory_purchase_provenance_for_item",
"inventory_purchase_documents_for_item",
"inventory_sale_trace_for_item",
"inventory_profitability_for_item",
"inventory_purchase_to_sale_chain"
]);
function shouldBypassStrictDeepInvestigationCueForAddressIntent(intent) {

View File

@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.hasInventoryPurchaseStem = hasInventoryPurchaseStem;
exports.hasInventorySupplierCue = hasInventorySupplierCue;
exports.hasInventorySaleCue = hasInventorySaleCue;
exports.hasInventoryProfitabilityCue = hasInventoryProfitabilityCue;
function toText(value) {
return String(value ?? "");
}
@ -34,3 +35,15 @@ function hasInventorySaleCue(text) {
}
return /(?:^|[\s,.;:!?])(продано|продали|продан(?:а|о|ы)?|реализовано|реализовали|реализован(?:а|о|ы)?)(?=$|[\s,.;:!?])/iu.test(value);
}
function hasInventoryProfitabilityCue(text) {
const value = toText(text);
const hasExplicitEconomicsMetric = /(?:прибыл|марж|рентабел|наценк|выручк|доход|profit(?:ability)?|margin|revenue|unit\s+economics)/iu.test(value);
if (hasExplicitEconomicsMetric) {
return true;
}
if (/(?:заработ(?:ал|али|аем|ок|ан)|прин[её]с(?:ли)?)/iu.test(value) &&
/(?:денег|деньг|с\s+продаж[а-яё]*|по\s+продаж[а-яё]*|от\s+продаж[а-яё]*|продаж[а-яё]*|реализац|sale|sales)/iu.test(value)) {
return true;
}
return /(?:сколько|скока|скок)[\s\S]{0,60}(?:заработ|прин[её]с|денег[\s\S]{0,20}(?:с\s+продаж|по\s+продаж|от\s+продаж|продаж|реализац))/iu.test(value);
}

View File

@ -916,6 +916,7 @@ function isInventoryTraceIntent(intent: AddressIntent): boolean {
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_profitability_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date"
);
@ -927,6 +928,7 @@ function isInventoryItemAnchoredIntent(intent: AddressIntent): boolean {
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_aging_by_purchase_date" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_profitability_for_item" ||
intent === "inventory_purchase_to_sale_chain"
);
}
@ -938,6 +940,7 @@ function usesRecipeDefaultLimit(intent: AddressIntent): boolean {
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_profitability_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date"
);
@ -1373,6 +1376,7 @@ function requiredFiltersByIntent(intent: AddressIntent): Array<keyof AddressFilt
if (
intent === "inventory_purchase_provenance_for_item" ||
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_profitability_for_item" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_purchase_to_sale_chain"
) {
@ -1415,6 +1419,7 @@ function usesAsOfPrimaryWindow(intent: AddressIntent): boolean {
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_profitability_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date" ||
intent === "open_items_by_counterparty_or_contract" ||

View File

@ -1,5 +1,10 @@
import type { AddressIntentResolution } from "../types/addressQuery";
import { hasInventoryPurchaseStem, hasInventorySaleCue, hasInventorySupplierCue } from "./inventoryLifecycleCueHelpers";
import {
hasInventoryProfitabilityCue,
hasInventoryPurchaseStem,
hasInventorySaleCue,
hasInventorySupplierCue
} from "./inventoryLifecycleCueHelpers";
const RECEIVABLES_STRONG = [
"кто должен нам",
@ -1638,6 +1643,10 @@ function hasSelectedObjectInventorySaleTraceSignal(text: string): boolean {
return hasSelectedObjectInventoryCue(text) && hasInventorySaleCue(text);
}
function hasSelectedObjectInventoryProfitabilitySignal(text: string): boolean {
return hasSelectedObjectInventoryCue(text) && hasInventoryProfitabilityCue(text);
}
function hasInventoryProvenanceSignalV2(text: string): boolean {
const hasItemCue = /(?:товар|номенклатур|sku|item|product|остат(?:ок|ки)|склад)/iu.test(text);
const hasSupplierCue = hasInventorySupplierCue(text) || /кем\s+поставлен/iu.test(text);
@ -1946,6 +1955,14 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
};
}
if (hasSelectedObjectInventoryProfitabilitySignal(text)) {
return {
intent: "inventory_profitability_for_item",
confidence: "medium",
reasons: ["inventory_selected_object_profitability_signal_detected"]
};
}
if (hasSelectedObjectInventorySaleTraceSignal(text)) {
return {
intent: "inventory_sale_trace_for_item",

View File

@ -35,6 +35,7 @@ const DISPLAY_ENTITY_TYPE_BY_INTENT: Partial<Record<AddressIntent, AddressFocusO
inventory_purchase_documents_for_item: "item",
inventory_supplier_stock_overlap_as_of_date: "item",
inventory_sale_trace_for_item: "item",
inventory_profitability_for_item: "item",
inventory_purchase_to_sale_chain: "item",
inventory_aging_by_purchase_date: "item"
};
@ -60,6 +61,7 @@ const RESULT_SET_TYPE_BY_INTENT: Partial<Record<AddressIntent, AddressResultSetT
inventory_purchase_documents_for_item: "inventory_trace",
inventory_supplier_stock_overlap_as_of_date: "inventory_trace",
inventory_sale_trace_for_item: "inventory_trace",
inventory_profitability_for_item: "inventory_trace",
inventory_purchase_to_sale_chain: "inventory_trace",
inventory_aging_by_purchase_date: "inventory_trace",
period_coverage_profile: "profile_summary",

View File

@ -1,6 +1,11 @@
import type { AddressModeDetection } from "../types/addressQuery";
import { hasInventoryPurchaseStem, hasInventorySaleCue, hasInventorySupplierCue } from "./inventoryLifecycleCueHelpers";
import {
hasInventoryProfitabilityCue,
hasInventoryPurchaseStem,
hasInventorySaleCue,
hasInventorySupplierCue
} from "./inventoryLifecycleCueHelpers";
const ADDRESS_ACTION_TOKENS = [
"show",
@ -290,6 +295,7 @@ function hasSelectedObjectInventoryFollowupSignal(text: string): boolean {
}
return (
hasInventorySupplierCue(text) ||
hasInventoryProfitabilityCue(text) ||
hasInventorySaleCue(text) ||
/(?:кто\s+(?:поставил|продал)|по\s+каким\s+документам\s+.*купили)/iu.test(text) ||
/(?:к[оа]му|куда)[\s\S]{0,80}(?:поставил|поставили|поставлен|поставлена|поставлено|отгрузил|отгрузили|отгружен|отгружена|отгружено)/iu.test(text) ||

View File

@ -1533,6 +1533,7 @@ function isOrganizationScopedInventoryIntent(intent: AddressIntent): boolean {
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_profitability_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date"
);
@ -2418,6 +2419,7 @@ function normalizeMissingAnchorLabel(anchor: string): string {
function buildLimitedScopeLine(filters: AddressFilterSet): string | null {
const organization = toNonEmptyFilterValue(filters.organization);
const item = toNonEmptyFilterValue(filters.item);
const asOfDate = toNonEmptyFilterValue(filters.as_of_date);
const periodFrom = toNonEmptyFilterValue(filters.period_from);
const periodTo = toNonEmptyFilterValue(filters.period_to);
@ -2425,6 +2427,9 @@ function buildLimitedScopeLine(filters: AddressFilterSet): string | null {
if (organization) {
scopeParts.push(`организация ${organization}`);
}
if (item) {
scopeParts.push(`товар ${item}`);
}
if (asOfDate) {
scopeParts.push(`срез на ${asOfDate}`);
} else if (periodFrom || periodTo) {
@ -2444,6 +2449,7 @@ function buildLimitedVariantSeedFingerprint(filters: AddressFilterSet): string {
"contract",
"account",
"document_ref",
"item",
"as_of_date",
"period_from",
"period_to"
@ -2503,6 +2509,8 @@ function buildLimitedOffers(input: {
offers.push("показать документы поступления по товару на 41.01");
} else if (input.intent === "inventory_sale_trace_for_item") {
offers.push("показать подтвержденные движения выбытия товара со счета 41.01");
} else if (input.intent === "inventory_profitability_for_item") {
offers.push("показать выручку, прибыль или маржу по выбранному товару за период продаж");
} else if (input.intent === "inventory_purchase_to_sale_chain") {
offers.push("показать документальную цепочку по товару: поступление на 41.01 и последующее выбытие");
} else if (input.intent === "open_contracts_confirmed_as_of_date") {
@ -2565,6 +2573,7 @@ function buildLimitedIntentSignalLine(input: {
list_receivables_counterparties: "Сигнал запроса: нужен ранжированный список должников.",
list_payables_counterparties: "Сигнал запроса: нужен ранжированный список кредиторов.",
inventory_on_hand_as_of_date: "Сигнал запроса: нужен подтвержденный срез товаров на складе на дату.",
inventory_profitability_for_item: "Сигнал запроса: нужен расчет выручки/прибыли/маржи по выбранной номенклатуре.",
receivables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез дебиторской задолженности на дату.",
payables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез обязательств к оплате на дату.",
vat_payable_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез НДС к уплате на дату.",
@ -2596,6 +2605,7 @@ function hasAggregateLimitedSignal(input: {
input.intent === "contract_usage_overview" ||
input.intent === "supplier_payouts_profile" ||
input.intent === "customer_revenue_and_payments" ||
input.intent === "inventory_profitability_for_item" ||
input.intent === "contract_usage_and_value"
) {
return true;

View File

@ -15,7 +15,12 @@ import {
isInventoryItemAnchorDegradation,
isLowQualityInventoryItemAnchorValue
} from "../addressFilterExtractor";
import { hasInventoryPurchaseStem, hasInventorySaleCue, hasInventorySupplierCue } from "../inventoryLifecycleCueHelpers";
import {
hasInventoryProfitabilityCue,
hasInventoryPurchaseStem,
hasInventorySaleCue,
hasInventorySupplierCue
} from "../inventoryLifecycleCueHelpers";
import { applyAddressLlmSemanticHintsToExtraction } from "./semanticHintOverlay";
import type { AddressLlmSemanticHints } from "../../types/addressQuery";
@ -352,6 +357,7 @@ function isInventoryIntent(intent: AddressIntent | undefined): boolean {
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_profitability_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date"
);
@ -366,6 +372,7 @@ function isInventoryDrilldownFrameIntent(intent: AddressIntent | undefined): boo
intent === "inventory_purchase_provenance_for_item" ||
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_profitability_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date"
);
@ -576,6 +583,10 @@ export function hasInventoryPurchaseDocumentsFollowupCue(text: string): boolean
);
}
export function hasInventoryProfitabilityFollowupCue(text: string): boolean {
return hasInventoryProfitabilityCue(String(text ?? ""));
}
export function hasInventoryPurchaseDateFollowupCue(text: string): boolean {
const value = String(text ?? "");
return /(?:когда\s+(?:примерно\s+)?(?:мы\s+)?купили|когда\s+был\s+куплен|когда\s+куплен|когда\s+это\s+купили|когда\s+эту\s+позицию\s+купили|когда\s+ее\s+купили|дата\s+закупки|purchase\s+date)/iu.test(
@ -796,6 +807,7 @@ function mergeFollowupFilters(
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_profitability_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date" ||
intent === "payables_confirmed_as_of_date" ||
@ -829,6 +841,7 @@ function mergeFollowupFilters(
(intent === "inventory_purchase_provenance_for_item" ||
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_profitability_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date")
) {
@ -1092,6 +1105,7 @@ function resolveMissingRequiredFilters(intent: AddressIntent, filters: AddressFi
account_balance_snapshot: ["account", "as_of_date"],
documents_forming_balance: ["account", "as_of_date"],
inventory_on_hand_as_of_date: ["as_of_date"],
inventory_profitability_for_item: ["item"],
open_contracts_confirmed_as_of_date: ["as_of_date"],
payables_confirmed_as_of_date: ["as_of_date"],
receivables_confirmed_as_of_date: ["as_of_date"],
@ -1215,6 +1229,26 @@ function deriveIntentWithFollowupContext(
}
}
if (inventorySelectedObjectFollowup && hasInventoryProfitabilityFollowupCue(normalizedMessage)) {
if (
detectedIntent.intent === "unknown" ||
detectedIntent.intent === "customer_revenue_and_payments" ||
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 === "inventory_sale_trace_for_item" ||
detectedIntent.intent === previousIntent
) {
return {
intent: "inventory_profitability_for_item",
confidence: "low",
reasons: [...detectedIntent.reasons, "intent_adjusted_to_inventory_followup_context"]
};
}
}
if (inventorySelectedObjectFollowup && hasInventoryPurchaseDateFollowupCue(normalizedMessage)) {
if (
detectedIntent.intent === "unknown" ||

View File

@ -248,6 +248,7 @@ export function resolvePrimaryAnchor(intent: AddressIntent, filters: AddressFilt
(intent === "inventory_purchase_provenance_for_item" ||
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_profitability_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date") &&
item

View File

@ -1,5 +1,7 @@
import { runAssistantRoutePolicyRuntime } from "./assistantRoutePolicyRuntimeAdapter";
import { hasInventoryProfitabilityCue } from "./inventoryLifecycleCueHelpers";
export interface BuildAssistantAddressOrchestrationRuntimeInput {
userMessage: string;
sessionItems: unknown[];
@ -67,14 +69,18 @@ function hasSelectedObjectInventorySignal(text: string | null): boolean {
}
function hasSelectedObjectInventoryActionCue(text: string | null): boolean {
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 ?? "")
const value = 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(
value
) || hasInventoryProfitabilityCue(value)
);
}
function isGenericCanonicalDriftIntent(intent: string | null): boolean {
return (
intent === "open_items_by_counterparty_or_contract" ||
intent === "customer_revenue_and_payments" ||
intent === "list_documents_by_counterparty" ||
intent === "list_documents_by_contract" ||
intent === "bank_operations_by_counterparty" ||

View File

@ -2463,6 +2463,7 @@ function isInventoryDrilldownFrameIntent(intent) {
return intent === "inventory_purchase_provenance_for_item" ||
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_profitability_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date";
}
@ -2739,6 +2740,7 @@ function isInventorySelectedObjectIntent(intent) {
return intent === "inventory_purchase_provenance_for_item" ||
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_profitability_for_item" ||
intent === "inventory_purchase_to_sale_chain";
}
function hasShortInventoryObjectFollowupSignal(userMessage) {
@ -3061,6 +3063,7 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
sourceIntentHint === "inventory_purchase_provenance_for_item" ||
sourceIntentHint === "inventory_purchase_documents_for_item" ||
sourceIntentHint === "inventory_sale_trace_for_item" ||
sourceIntentHint === "inventory_profitability_for_item" ||
sourceIntentHint === "inventory_purchase_to_sale_chain" ||
sourceIntentHint === "inventory_aging_by_purchase_date" ||
hasSelectedObjectInventorySignalPrimary ||
@ -3492,6 +3495,7 @@ function resolveRequiredAnchorTypeForIntent(intent) {
if (intent === "inventory_purchase_provenance_for_item" ||
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_profitability_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date") {
return "item";
@ -3544,7 +3548,9 @@ function hasSelectedObjectInventoryFollowupSignalForPredecompose(text) {
return /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(String(text ?? ""));
}
function isInventorySelectedObjectFollowupIntent(intent) {
return intent === "inventory_purchase_provenance_for_item" || intent === "inventory_purchase_documents_for_item";
return intent === "inventory_purchase_provenance_for_item" ||
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_profitability_for_item";
}
function hasSameDateAccountFollowupSignalForPredecompose(text) {
const source = String(text ?? "");
@ -4228,6 +4234,7 @@ const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([
"inventory_purchase_documents_for_item",
"inventory_supplier_stock_overlap_as_of_date",
"inventory_sale_trace_for_item",
"inventory_profitability_for_item",
"inventory_purchase_to_sale_chain",
"inventory_aging_by_purchase_date",
"contract_usage_overview",
@ -4240,6 +4247,7 @@ const ADDRESS_INTENTS_ALLOW_STRICT_DEEP_INVESTIGATION_BYPASS = new Set([
"inventory_purchase_provenance_for_item",
"inventory_purchase_documents_for_item",
"inventory_sale_trace_for_item",
"inventory_profitability_for_item",
"inventory_purchase_to_sale_chain"
]);
function shouldBypassStrictDeepInvestigationCueForAddressIntent(intent) {

View File

@ -41,3 +41,23 @@ export function hasInventorySaleCue(text: string): boolean {
value
);
}
export function hasInventoryProfitabilityCue(text: string): boolean {
const value = toText(text);
const hasExplicitEconomicsMetric =
/(?:прибыл|марж|рентабел|наценк|выручк|доход|profit(?:ability)?|margin|revenue|unit\s+economics)/iu.test(value);
if (hasExplicitEconomicsMetric) {
return true;
}
if (
/(?:заработ(?:ал|али|аем|ок|ан)|прин[её]с(?:ли)?)/iu.test(value) &&
/(?:денег|деньг|с\s+продаж[а-яё]*|по\s+продаж[а-яё]*|от\s+продаж[а-яё]*|продаж[а-яё]*|реализац|sale|sales)/iu.test(
value
)
) {
return true;
}
return /(?:сколько|скока|скок)[\s\S]{0,60}(?:заработ|прин[её]с|денег[\s\S]{0,20}(?:с\s+продаж|по\s+продаж|от\s+продаж|продаж|реализац))/iu.test(
value
);
}

View File

@ -24,6 +24,7 @@ export type AddressIntent =
| "inventory_purchase_documents_for_item"
| "inventory_supplier_stock_overlap_as_of_date"
| "inventory_sale_trace_for_item"
| "inventory_profitability_for_item"
| "inventory_purchase_to_sale_chain"
| "inventory_aging_by_purchase_date"
| "account_balance_snapshot"

View File

@ -0,0 +1,67 @@
import { afterEach, describe, expect, it, vi } from "vitest";
const { executeAddressMcpQueryMock } = vi.hoisted(() => ({
executeAddressMcpQueryMock: vi.fn()
}));
vi.mock("../src/services/addressMcpClient", async () => {
const actual = await vi.importActual<typeof import("../src/services/addressMcpClient")>(
"../src/services/addressMcpClient"
);
return {
...actual,
executeAddressMcpQuery: executeAddressMcpQueryMock
};
});
import { AddressQueryService } from "../src/services/addressQueryService";
import { runAddressDecomposeStage } from "../src/services/address_runtime/decomposeStage";
afterEach(() => {
executeAddressMcpQueryMock.mockReset();
vi.restoreAllMocks();
});
describe("inventory profitability selected-object regressions", () => {
const followupContext = {
previous_intent: "inventory_on_hand_as_of_date" as const,
previous_filters: {
organization: "ООО \\Альтернатива Плюс\\",
as_of_date: "2020-05-31",
period_from: "2020-05-01",
period_to: "2020-05-31"
},
previous_anchor_type: "unknown" as const,
previous_anchor_value: null
};
const selectedObjectProfitabilityMessage =
'По выбранному объекту "Четки Пост (84*117)": а сколько денег мы заработали с продажжи этих четок';
it("routes selected-object profitability wording into an item profitability intent", () => {
const result = runAddressDecomposeStage(selectedObjectProfitabilityMessage, followupContext);
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("inventory_profitability_for_item");
expect(result?.intent.intent).not.toBe("customer_revenue_and_payments");
expect(result?.filters.extracted_filters.item).toBe("Четки Пост (84*117)");
expect(result?.filters.extracted_filters.period_from).toBe("2020-05-01");
expect(result?.filters.extracted_filters.period_to).toBe("2020-05-31");
});
it("returns a truthful recipe visibility gap until item profitability gets a dedicated recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle(selectedObjectProfitabilityMessage, {
followupContext
});
expect(result?.handled).toBe(true);
expect(result?.response_type).toBe("LIMITED_WITH_REASON");
expect(result?.debug.detected_intent).toBe("inventory_profitability_for_item");
expect(result?.debug.selected_recipe).toBeNull();
expect(result?.debug.limited_reason_category).toBe("recipe_visibility_gap");
expect(result?.debug.extracted_filters?.item).toBe("Четки Пост (84*117)");
expect(String(result?.reply_text ?? "")).toContain("Четки Пост (84*117)");
expect(executeAddressMcpQueryMock).not.toHaveBeenCalled();
});
});

View File

@ -377,4 +377,68 @@ describe("assistant address orchestration runtime adapter", () => {
})
);
});
it("prefers raw selected-object profitability wording over customer revenue canonical drift", async () => {
const resolveAddressFollowupCarryoverContext = vi.fn(() => ({
followupContext: {
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"
}
}
}));
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 =
'По выбранному объекту "Четки Пост (84*117)": а сколько денег мы заработали с продажжи этих четок';
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: "customer_revenue_and_payments",
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
})
);
});
});