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

This commit is contained in:
2026-04-16 09:00:43 +03:00
parent a493e2fd69
commit d7c4eb781a
23 changed files with 329 additions and 8 deletions
@@ -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" ||
@@ -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",
@@ -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",
@@ -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) ||
@@ -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;
@@ -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" ||
@@ -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
@@ -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" ||
@@ -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) {
@@ -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
);
}
@@ -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"