Стабилизировать маржинальность номенклатуры
This commit is contained in:
@@ -12,6 +12,7 @@ const COMPUTE_EXACT_INTENTS = new Set([
|
||||
"inventory_purchase_documents_for_item",
|
||||
"inventory_supplier_stock_overlap_as_of_date",
|
||||
"inventory_sale_trace_for_item",
|
||||
"inventory_margin_ranking_for_nomenclature",
|
||||
"inventory_profitability_for_item",
|
||||
"inventory_purchase_to_sale_chain",
|
||||
"inventory_aging_by_purchase_date",
|
||||
@@ -68,6 +69,7 @@ function defaultCapabilityId(intent) {
|
||||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date") {
|
||||
@@ -151,12 +153,15 @@ function resolveCapabilityEnabled(intent) {
|
||||
if (intent === "inventory_purchase_provenance_for_item" ||
|
||||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain") {
|
||||
if (intent === "inventory_profitability_for_item") {
|
||||
if (intent === "inventory_profitability_for_item" || intent === "inventory_margin_ranking_for_nomenclature") {
|
||||
return {
|
||||
enabled: true,
|
||||
reason: "inventory_profitability_route_enabled"
|
||||
reason: intent === "inventory_margin_ranking_for_nomenclature"
|
||||
? "inventory_margin_ranking_route_enabled"
|
||||
: "inventory_profitability_route_enabled"
|
||||
};
|
||||
}
|
||||
if (intent === "inventory_purchase_to_sale_chain") {
|
||||
@@ -249,6 +254,7 @@ function resolveShadowRouteIntent(intent, requestedResultMode) {
|
||||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date") {
|
||||
|
||||
@@ -101,6 +101,7 @@ function isConfirmedBalanceIntent(intent) {
|
||||
intent === "inventory_purchase_provenance_for_item" ||
|
||||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "open_contracts_confirmed_as_of_date" ||
|
||||
|
||||
@@ -971,6 +971,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_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date");
|
||||
@@ -989,6 +990,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_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date");
|
||||
@@ -1407,6 +1409,9 @@ function requiredFiltersByIntent(intent) {
|
||||
intent === "inventory_purchase_to_sale_chain") {
|
||||
return ["item"];
|
||||
}
|
||||
if (intent === "inventory_margin_ranking_for_nomenclature") {
|
||||
return ["period_from", "period_to"];
|
||||
}
|
||||
if (intent === "payables_confirmed_as_of_date") {
|
||||
return ["as_of_date"];
|
||||
}
|
||||
|
||||
@@ -1664,6 +1664,17 @@ function hasBidirectionalValueFlowComparisonSignal(text) {
|
||||
const hasNetAmountCue = /(?:сколько|сумм|итог|нетто|сальдо|минус|net|total|sum)/iu.test(normalized);
|
||||
return hasIncomingCue && hasOutgoingCue && hasComparisonCue && (hasValueFlowCue || hasNetAmountCue);
|
||||
}
|
||||
function hasNomenclatureMarginRankingSignal(text) {
|
||||
const normalized = String(text ?? "").trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const hasNomenclatureCue = /(?:номенклатур|товар|позици|ассортимент|sku|item|product|goods)/iu.test(normalized);
|
||||
const hasRealizationCue = /(?:реализован|реализац|продан|продаж|отгруж|41(?:[.,]0?1)?|90(?:[.,]\d{1,2})?|sales?|sold)/iu.test(normalized);
|
||||
const hasMarginCue = /(?:прибыл|марж|рентаб|наценк|себестоим|выручк|profit|margin|profitability|gross\s+spread|cogs)/iu.test(normalized);
|
||||
const hasRankingCue = /(?:высок|низк|топ|сам(?:ая|ый|ое|ые)|больш|меньш|ранж|рейтинг|high|low|top|rank|best|worst)/iu.test(normalized);
|
||||
return hasNomenclatureCue && hasRealizationCue && hasMarginCue && hasRankingCue;
|
||||
}
|
||||
function hasVatPeriodInspectionBridgeSignal(text) {
|
||||
const normalized = String(text ?? "").trim().toLowerCase();
|
||||
if (!/(?:ндс|vat)/iu.test(normalized)) {
|
||||
@@ -1736,6 +1747,9 @@ function resolveUnicodeAddressIntentBridge(text) {
|
||||
if (hasSelectedObjectProfitabilityCue) {
|
||||
return unicodeBridgeResolution("inventory_profitability_for_item", "high", "unicode_selected_object_profitability_bridge_signal_detected");
|
||||
}
|
||||
if (hasNomenclatureMarginRankingSignal(normalized)) {
|
||||
return unicodeBridgeResolution("inventory_margin_ranking_for_nomenclature", "high", "unicode_nomenclature_margin_ranking_bridge_signal_detected");
|
||||
}
|
||||
const hasOpenItemsAccountCue = /(?:хвост|долг|незакрыт|вис)/iu.test(normalized) &&
|
||||
/(?:сч(?:е|ё)т(?:а|у|ом|е|ов)?\s*(?:№|#)?\s*(?:60|62|76)(?:[.,]\d{1,2})?|\b(?:60|62|76)(?:[.,]\d{1,2})?\b\s*сч(?:е|ё)т)/iu.test(normalized);
|
||||
if (hasOpenItemsAccountCue) {
|
||||
|
||||
@@ -1647,6 +1647,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_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date");
|
||||
@@ -1680,6 +1681,7 @@ function shouldDeferInventoryOrganizationClarification(intent, filters, semantic
|
||||
return (intent === "inventory_purchase_provenance_for_item" ||
|
||||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date");
|
||||
@@ -1981,6 +1983,7 @@ function canAutoBroadenPeriodWindow(intent, filters) {
|
||||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date");
|
||||
@@ -1990,6 +1993,7 @@ function shouldBoostAutoBroadenedLimit(intent) {
|
||||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date");
|
||||
@@ -2520,6 +2524,9 @@ async function tryComposeLlmLimitedReply(input) {
|
||||
if (process.env.VITEST === "true" || process.env.NODE_ENV === "test") {
|
||||
return null;
|
||||
}
|
||||
if (input.intent === "inventory_margin_ranking_for_nomenclature" && input.category === "missing_anchor") {
|
||||
return null;
|
||||
}
|
||||
if (!shouldUseLlmLimitedReply(input.category)) {
|
||||
return null;
|
||||
}
|
||||
@@ -2580,6 +2587,13 @@ function composeLimitedReply(input) {
|
||||
.map((item) => normalizeMissingAnchorLabel(String(item ?? "").trim()))
|
||||
.filter((item) => item.length > 0)));
|
||||
const missingAnchorPhrase = missingAnchorLabels.length > 0 ? missingAnchorLabels.join(", ") : "контрагент, договор, счет или период";
|
||||
if (input.intent === "inventory_margin_ranking_for_nomenclature" && input.category === "missing_anchor") {
|
||||
return [
|
||||
"Для рейтинга прибыльности номенклатуры нужен период.",
|
||||
"Могу посчитать по номенклатуре: выручку без НДС, себестоимость реализации, валовую прибыль и маржинальность.",
|
||||
"Уточните период: месяц, квартал, год или весь доступный период."
|
||||
].join("\n\n");
|
||||
}
|
||||
const heading = input.category === "empty_match"
|
||||
? pickDeterministicVariant(headingSeed, [
|
||||
"По текущим условиям в доступном срезе данных совпадений не нашлось.",
|
||||
|
||||
+54
-40
@@ -966,6 +966,17 @@ const BASE_RECIPES = [
|
||||
account_scope_mode: "strict",
|
||||
query_template: "inventory_trading_margin_proxy_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_inventory_margin_ranking_for_nomenclature_v1",
|
||||
intent: "inventory_margin_ranking_for_nomenclature",
|
||||
purpose: "Rank realized nomenclature by bounded gross margin proxy for an explicit period using 41.01 purchase and sale document rows",
|
||||
required_filters: ["period_from", "period_to"],
|
||||
optional_filters: ["organization", "warehouse", "limit", "sort"],
|
||||
default_limit: 800,
|
||||
account_scope: ["41.01"],
|
||||
account_scope_mode: "strict",
|
||||
query_template: "inventory_margin_ranking_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_inventory_purchase_to_sale_chain_v1",
|
||||
intent: "inventory_purchase_to_sale_chain",
|
||||
@@ -1714,6 +1725,7 @@ function maxLimitForIntent(intent) {
|
||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_trading_margin_proxy_for_organization" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date" ||
|
||||
@@ -1905,33 +1917,17 @@ function buildAddressRecipePlan(recipe, filters) {
|
||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_trading_margin_proxy_profile"
|
||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
||||
: recipe.query_template === "inventory_margin_ranking_profile"
|
||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
||||
: recipe.query_template === "inventory_quality_events_profile"
|
||||
? buildInventoryQualityEventsQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "contracts_by_counterparty_profile"
|
||||
? CONTRACTS_BY_COUNTERPARTY_QUERY_TEMPLATE.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
: recipe.query_template === "open_contracts_confirmed_as_of_balance_profile"
|
||||
? (() => {
|
||||
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||
? toDateTimeExpr(filters.as_of_date, true)
|
||||
: null) ??
|
||||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_to, true)
|
||||
: null) ??
|
||||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_from, true)
|
||||
: null) ??
|
||||
"ТЕКУЩАЯДАТА()";
|
||||
return OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "62", "76"]))
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
})()
|
||||
: recipe.query_template === "payables_confirmed_as_of_balance_profile"
|
||||
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
||||
: recipe.query_template === "inventory_quality_events_profile"
|
||||
? buildInventoryQualityEventsQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "contracts_by_counterparty_profile"
|
||||
? CONTRACTS_BY_COUNTERPARTY_QUERY_TEMPLATE.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
: recipe.query_template === "open_contracts_confirmed_as_of_balance_profile"
|
||||
? (() => {
|
||||
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||
? toDateTimeExpr(filters.as_of_date, true)
|
||||
@@ -1946,10 +1942,10 @@ function buildAddressRecipePlan(recipe, filters) {
|
||||
return OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"]))
|
||||
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "62", "76"]))
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
})()
|
||||
: recipe.query_template === "receivables_confirmed_as_of_balance_profile"
|
||||
: recipe.query_template === "payables_confirmed_as_of_balance_profile"
|
||||
? (() => {
|
||||
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||
? toDateTimeExpr(filters.as_of_date, true)
|
||||
@@ -1964,20 +1960,38 @@ function buildAddressRecipePlan(recipe, filters) {
|
||||
return OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["62", "76"]))
|
||||
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"]))
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
})()
|
||||
: MOVEMENTS_QUERY_TEMPLATE
|
||||
.replace("__LIMIT__", String(resolvedLimit))
|
||||
.replace("__WHERE_CLAUSE__", (() => {
|
||||
const extraConditions = [];
|
||||
const accountCondition = buildMovementAccountCondition(filters);
|
||||
if (accountCondition) {
|
||||
extraConditions.push(accountCondition);
|
||||
}
|
||||
return buildWhereClause(filters, "Движения.Период", extraConditions);
|
||||
})())
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
: recipe.query_template === "receivables_confirmed_as_of_balance_profile"
|
||||
? (() => {
|
||||
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||
? toDateTimeExpr(filters.as_of_date, true)
|
||||
: null) ??
|
||||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_to, true)
|
||||
: null) ??
|
||||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_from, true)
|
||||
: null) ??
|
||||
"ТЕКУЩАЯДАТА()";
|
||||
return OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["62", "76"]))
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
})()
|
||||
: MOVEMENTS_QUERY_TEMPLATE
|
||||
.replace("__LIMIT__", String(resolvedLimit))
|
||||
.replace("__WHERE_CLAUSE__", (() => {
|
||||
const extraConditions = [];
|
||||
const accountCondition = buildMovementAccountCondition(filters);
|
||||
if (accountCondition) {
|
||||
extraConditions.push(accountCondition);
|
||||
}
|
||||
return buildWhereClause(filters, "Движения.Период", extraConditions);
|
||||
})())
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
return {
|
||||
recipe,
|
||||
query,
|
||||
|
||||
@@ -8,6 +8,7 @@ exports.hasBareInventoryPurchaseDateFollowupCue = hasBareInventoryPurchaseDateFo
|
||||
exports.hasInventorySaleFollowupCue = hasInventorySaleFollowupCue;
|
||||
exports.hasInventoryPurchaseToSaleChainFollowupCue = hasInventoryPurchaseToSaleChainFollowupCue;
|
||||
exports.hasInventoryPurchaseDateVatBridgeCue = hasInventoryPurchaseDateVatBridgeCue;
|
||||
exports.hasInventoryMarginRankingFollowupCue = hasInventoryMarginRankingFollowupCue;
|
||||
exports.hasAddressFollowupContextSignal = hasAddressFollowupContextSignal;
|
||||
exports.runAddressDecomposeStage = runAddressDecomposeStage;
|
||||
const addressQueryClassifier_1 = require("../addressQueryClassifier");
|
||||
@@ -321,6 +322,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_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date");
|
||||
@@ -332,6 +334,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_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date");
|
||||
@@ -340,6 +343,7 @@ function isInventoryLifecycleHistoryIntent(intent) {
|
||||
return (intent === "inventory_purchase_provenance_for_item" ||
|
||||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain");
|
||||
}
|
||||
@@ -625,11 +629,29 @@ function hasInventoryPurchaseDateVatBridgeCue(text) {
|
||||
return (/(?:ндс|vat)/iu.test(normalized) &&
|
||||
/(?:на\s+дат[ауеы]\s+покупк|на\s+дат[ауеы]\s+закупк|по\s+дат[еу]\s+покупк|по\s+дат[еу]\s+закупк|дата\s+покупк|дата\s+закупк|purchase\s+date)/iu.test(normalized));
|
||||
}
|
||||
function hasInventoryMarginRankingFollowupCue(text) {
|
||||
const normalized = textWithRepairedVariant(String(text ?? ""))
|
||||
.toLowerCase()
|
||||
.replace(/ё/g, "е");
|
||||
if (!normalized.trim()) {
|
||||
return false;
|
||||
}
|
||||
const wantsFoundRows = /(?:покажи|показать|выведи|дай|раскрой|show|list|покажи|показать|выведи|дай|раскрой)/iu.test(normalized) &&
|
||||
/(?:найденн|строк|реализац|себестоимостн|баз|найденн|строк|реализац|себестоимостн|баз)/iu.test(normalized) &&
|
||||
/(?:себестоимостн|реализац|марж|прибыл|номенклатур|себестоимостн|реализац|марж|прибыл|номенклат)/iu.test(normalized);
|
||||
const account41Not01 = /\b41(?:[.,]\d{1,2})?\b/iu.test(normalized) &&
|
||||
/\b01(?:[.,]\d{1,2})?\b/iu.test(normalized) &&
|
||||
/(?:\bне\b|вместо|а\s+не|not|instead|РЅРµ|вместо|Р°\s+РЅРµ)/iu.test(normalized);
|
||||
return wantsFoundRows || account41Not01;
|
||||
}
|
||||
function hasAddressFollowupContextSignal(text) {
|
||||
const normalized = String(text ?? "").trim();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
if (hasInventoryMarginRankingFollowupCue(normalized)) {
|
||||
return true;
|
||||
}
|
||||
if (/(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
@@ -867,6 +889,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_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date" ||
|
||||
@@ -919,6 +942,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_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date")) {
|
||||
@@ -1122,6 +1146,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_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date" ||
|
||||
@@ -1134,6 +1159,8 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||
const currentContractExplicit = toNonEmptyString(merged.contract);
|
||||
const currentItemExplicit = toNonEmptyString(merged.item);
|
||||
const currentAccountExplicit = toNonEmptyString(merged.account);
|
||||
const currentAccountRefinesMarginDomain = intent === "inventory_margin_ranking_for_nomenclature" &&
|
||||
hasInventoryMarginRankingFollowupCue(userMessage);
|
||||
const shouldSuppressGenericPeriodCarryover = (Boolean(currentCounterpartyExplicit) &&
|
||||
!isLowQualityCounterpartyAnchor(currentCounterpartyExplicit) &&
|
||||
currentCounterpartyExplicit !== previousCounterparty) ||
|
||||
@@ -1141,7 +1168,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||
!isLowQualityContractAnchor(currentContractExplicit) &&
|
||||
currentContractExplicit !== previousContract) ||
|
||||
(Boolean(currentItemExplicit) && currentItemExplicit !== previousItem) ||
|
||||
(Boolean(currentAccountExplicit) && currentAccountExplicit !== previousAccount);
|
||||
(Boolean(currentAccountExplicit) && currentAccountExplicit !== previousAccount && !currentAccountRefinesMarginDomain);
|
||||
const vatRelativeMonthFollowup = relativeMonthFromFollowupYear &&
|
||||
(intent === "vat_payable_confirmed_as_of_date" ||
|
||||
intent === "vat_payable_forecast" ||
|
||||
@@ -1189,6 +1216,19 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||
}
|
||||
reasons.push("period_from_followup_context");
|
||||
}
|
||||
if (intent === "inventory_margin_ranking_for_nomenclature" &&
|
||||
previousHasPeriod &&
|
||||
hasInventoryMarginRankingFollowupCue(userMessage) &&
|
||||
!hasExplicitPeriodInMessage &&
|
||||
!hasExplicitCurrentDateInMessage) {
|
||||
if (previousPeriodFrom && merged.period_from !== previousPeriodFrom) {
|
||||
merged.period_from = previousPeriodFrom;
|
||||
}
|
||||
if (previousPeriodTo && merged.period_to !== previousPeriodTo) {
|
||||
merged.period_to = previousPeriodTo;
|
||||
}
|
||||
reasons.push("period_from_followup_context");
|
||||
}
|
||||
if (!currentHasPeriod &&
|
||||
previousHasPeriod &&
|
||||
hasFollowupSignal &&
|
||||
@@ -1251,6 +1291,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_margin_ranking_for_nomenclature: ["period_from", "period_to"],
|
||||
inventory_profitability_for_item: ["item"],
|
||||
open_contracts_confirmed_as_of_date: ["as_of_date"],
|
||||
payables_confirmed_as_of_date: ["as_of_date"],
|
||||
@@ -1319,6 +1360,22 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
||||
const hasExplicitInventoryItemReference = /(?:товар|номенклатур|позици|склад|остат|sku|item|product|товар|номенклатур|позици|склад|остат)/iu.test(normalizedMessage) || hasSelectedObjectInlineSnapshotMetadata(normalizedMessage);
|
||||
const staleInventoryLineageCanYieldToCounterparty = previousCounterpartyLaneActive && !hasExplicitInventoryItemReference;
|
||||
const inventoryPurchaseDateVatBridge = inventorySelectedObjectFollowup && hasInventoryPurchaseDateVatBridgeCue(normalizedMessage);
|
||||
const marginRankingLineageActive = sourceIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
fallbackIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
followupContext.root_intent === "inventory_margin_ranking_for_nomenclature";
|
||||
if (marginRankingLineageActive &&
|
||||
hasInventoryMarginRankingFollowupCue(normalizedMessage) &&
|
||||
(detectedIntent.intent === "unknown" ||
|
||||
detectedIntent.intent === "account_balance_snapshot" ||
|
||||
detectedIntent.intent === "documents_forming_balance" ||
|
||||
detectedIntent.intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
detectedIntent.intent === sourceIntent)) {
|
||||
return {
|
||||
intent: "inventory_margin_ranking_for_nomenclature",
|
||||
confidence: "low",
|
||||
reasons: [...detectedIntent.reasons, "intent_adjusted_to_inventory_margin_ranking_followup_context"]
|
||||
};
|
||||
}
|
||||
if (inventoryPurchaseDateVatBridge &&
|
||||
(detectedIntent.intent === "unknown" ||
|
||||
detectedIntent.intent === sourceIntent ||
|
||||
|
||||
+119
@@ -81,6 +81,48 @@ function inventoryProfitabilityPeriodLabel(options, deps) {
|
||||
const asOfDate = typeof options.asOfDate === "string" && options.asOfDate.trim().length > 0 ? options.asOfDate : null;
|
||||
return asOfDate ? `до ${deps.formatDateRu(asOfDate)}` : "по доступной выборке";
|
||||
}
|
||||
function inventoryRowItemLabel(row, deps) {
|
||||
return deps.summarizeInventoryTraceRows([row]).item;
|
||||
}
|
||||
function buildInventoryMarginRankingEntries(rows, deps) {
|
||||
const byItem = new Map();
|
||||
for (const row of rows) {
|
||||
const item = inventoryRowItemLabel(row, deps);
|
||||
if (!item) {
|
||||
continue;
|
||||
}
|
||||
const key = item.trim().toLocaleLowerCase("ru");
|
||||
const current = byItem.get(key) ?? { item, saleRows: [], purchaseRows: [] };
|
||||
if (deps.isInventorySaleMovement(row)) {
|
||||
current.saleRows.push(row);
|
||||
}
|
||||
if (deps.isInventoryPurchaseMovement(row)) {
|
||||
current.purchaseRows.push(row);
|
||||
}
|
||||
byItem.set(key, current);
|
||||
}
|
||||
return Array.from(byItem.values())
|
||||
.map((entry) => {
|
||||
const revenue = sumInventoryRowAmount(entry.saleRows);
|
||||
const costProxy = sumInventoryRowAmount(entry.purchaseRows);
|
||||
const spread = revenue - costProxy;
|
||||
return {
|
||||
item: entry.item,
|
||||
revenue,
|
||||
costProxy,
|
||||
spread,
|
||||
marginPct: revenue > 0 ? (spread / revenue) * 100 : null,
|
||||
saleQuantity: sumInventoryRowQuantity(entry.saleRows),
|
||||
purchaseQuantity: sumInventoryRowQuantity(entry.purchaseRows),
|
||||
saleDocuments: entry.saleRows.length,
|
||||
purchaseDocuments: entry.purchaseRows.length
|
||||
};
|
||||
})
|
||||
.filter((entry) => entry.revenue > 0 || entry.costProxy > 0);
|
||||
}
|
||||
function formatInventoryMarginRankingLine(entry, index, deps) {
|
||||
return `${index + 1}. ${entry.item} — выручка ${deps.formatMoneyRub(entry.revenue)}, себестоимостная база ${deps.formatMoneyRub(entry.costProxy)}, валовая разница ${deps.formatMoneyRub(entry.spread)}, маржа ${formatInventoryPercent(entry.marginPct, deps.formatNumberWithDots)}.`;
|
||||
}
|
||||
function composeInventoryReply(intent, rows, options, deps) {
|
||||
if (intent === "inventory_on_hand_as_of_date") {
|
||||
const asOfDate = deps.resolvePayablesAsOfDate(options);
|
||||
@@ -401,6 +443,83 @@ function composeInventoryReply(intent, rows, options, deps) {
|
||||
? (0, replyContracts_1.buildFactualListReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(summary.counterparties.length > 0 ? "strong" : "medium", true))
|
||||
: (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)("medium", false));
|
||||
}
|
||||
if (intent === "inventory_margin_ranking_for_nomenclature") {
|
||||
const entries = buildInventoryMarginRankingEntries(rows, deps);
|
||||
const confirmedEntries = entries.filter((entry) => entry.revenue > 0 && entry.costProxy > 0);
|
||||
const highMargin = [...confirmedEntries]
|
||||
.sort((left, right) => right.spread - left.spread || (right.marginPct ?? -Infinity) - (left.marginPct ?? -Infinity))
|
||||
.slice(0, 5);
|
||||
const lowMargin = [...confirmedEntries]
|
||||
.sort((left, right) => left.spread - right.spread || (left.marginPct ?? Infinity) - (right.marginPct ?? Infinity))
|
||||
.slice(0, 5);
|
||||
const salesWithoutCost = entries.filter((entry) => entry.revenue > 0 && entry.costProxy <= 0);
|
||||
const purchasesWithoutSales = entries.filter((entry) => entry.costProxy > 0 && entry.revenue <= 0);
|
||||
const periodLabel = inventoryProfitabilityPeriodLabel(options, deps);
|
||||
const totalRevenue = entries.reduce((sum, entry) => sum + entry.revenue, 0);
|
||||
const totalCostProxy = entries.reduce((sum, entry) => sum + entry.costProxy, 0);
|
||||
const totalSpread = totalRevenue - totalCostProxy;
|
||||
if (confirmedEntries.length === 0) {
|
||||
const lines = [`За период ${periodLabel} рейтинг прибыльности номенклатуры построить нельзя.`];
|
||||
const findings = [];
|
||||
if (salesWithoutCost.length > 0) {
|
||||
const salesCount = deps.formatNumberWithDots(salesWithoutCost.length);
|
||||
const salesItemPhrase = salesWithoutCost.length === 1 ? "1 номенклатурной позиции" : `${salesCount} номенклатурным позициям`;
|
||||
findings.push(`Есть реализация по ${salesItemPhrase}.`);
|
||||
findings.push(salesWithoutCost.length === 1
|
||||
? "Подтвержденной себестоимости реализации по этой позиции не найдено."
|
||||
: "Подтвержденной себестоимости реализации по этим позициям не найдено.");
|
||||
findings.push("Поэтому валовую прибыль и маржинальность честно посчитать нельзя.");
|
||||
}
|
||||
if (purchasesWithoutSales.length > 0) {
|
||||
const purchaseCount = deps.formatNumberWithDots(purchasesWithoutSales.length);
|
||||
const purchaseItemPhrase = purchasesWithoutSales.length === 1 ? "1 позиции" : `${purchaseCount} позициям`;
|
||||
findings.push(purchasesWithoutSales.length === 1
|
||||
? `Есть себестоимостная база по ${purchaseItemPhrase}, но реализации по ней в периоде не найдено.`
|
||||
: `Есть себестоимостная база по ${purchaseItemPhrase}, но реализации по ним в периоде не найдено.`);
|
||||
}
|
||||
if (entries.length === 0) {
|
||||
findings.push("В доступной выборке нет достаточных строк реализации и себестоимости по номенклатуре.");
|
||||
}
|
||||
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Что нашлось:", findings);
|
||||
lines.push(`Вывод: за период ${periodLabel} нет достаточной базы для рейтинга «высокая / низкая прибыль» по номенклатуре.`);
|
||||
const nextActions = [];
|
||||
if (salesWithoutCost.length > 0) {
|
||||
nextActions.push("показать найденные реализации за этот период;");
|
||||
}
|
||||
if (purchasesWithoutSales.length > 0) {
|
||||
nextActions.push("показать найденные строки себестоимостной базы за этот период;");
|
||||
}
|
||||
nextActions.push("расширить период до квартала или года;", "попробовать строгий расчет по проводкам 90.01 / 90.02;", "построить управленческий proxy по закупочным документам, если такой способ допустим для вашей проверки.");
|
||||
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Что можно сделать дальше:", nextActions);
|
||||
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Граница ответа:", [
|
||||
"Прибыльность номенклатуры считаю только когда есть реализация и подтвержденная себестоимость реализации.",
|
||||
"Это не чистая прибыль компании и не замена закрытию месяца."
|
||||
]);
|
||||
return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(entries.length > 0 ? "medium" : "weak", false));
|
||||
}
|
||||
const directAnswerLine = confirmedEntries.length > 0
|
||||
? `За период ${periodLabel} собран рейтинг реализованной номенклатуры по валовой маржинальности: выручка ${deps.formatMoneyRub(totalRevenue)}, себестоимостная база ${deps.formatMoneyRub(totalCostProxy)}, расчетная валовая разница ${deps.formatMoneyRub(totalSpread)}.`
|
||||
: `За период ${periodLabel} не удалось подтвердить рейтинг прибыльности номенклатуры: нужны одновременно строки реализации и закупочного/себестоимостного следа по товарам.`;
|
||||
const lines = [directAnswerLine];
|
||||
if (highMargin.length > 0) {
|
||||
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Высокая валовая маржинальность:", highMargin.map((entry, index) => formatInventoryMarginRankingLine(entry, index, deps)));
|
||||
}
|
||||
if (lowMargin.length > 0) {
|
||||
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Низкая или отрицательная валовая маржинальность:", lowMargin.map((entry, index) => formatInventoryMarginRankingLine(entry, index, deps)));
|
||||
}
|
||||
const boundaryLines = [
|
||||
"Это управленческий расчет валовой маржинальности по реализации и доступной себестоимостной базе, не чистая прибыль компании.",
|
||||
"Для строгого бухгалтерского расчета нужны проводки 90.01 / 90.02 и закрытие себестоимости; этот ответ не подменяет закрытие месяца."
|
||||
];
|
||||
if (salesWithoutCost.length > 0) {
|
||||
boundaryLines.push(`По ${deps.formatNumberWithDots(salesWithoutCost.length)} позициям есть продажи, но нет подтвержденной себестоимости реализации — их нельзя честно ранжировать по прибыли.`);
|
||||
}
|
||||
if (purchasesWithoutSales.length > 0) {
|
||||
boundaryLines.push(`По ${deps.formatNumberWithDots(purchasesWithoutSales.length)} позициям есть себестоимостная база без реализации в этом периоде.`);
|
||||
}
|
||||
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Граница ответа:", boundaryLines);
|
||||
return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(confirmedEntries.length > 0 ? "strong" : entries.length > 0 ? "medium" : "weak", confirmedEntries.length > 0));
|
||||
}
|
||||
if (intent === "inventory_profitability_for_item") {
|
||||
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
|
||||
const saleRows = rows.filter((row) => deps.isInventorySaleMovement(row));
|
||||
|
||||
+18
@@ -61,6 +61,7 @@ function isInventorySelectedObjectOrRootIntent(intent) {
|
||||
intent === "inventory_purchase_provenance_for_item" ||
|
||||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date");
|
||||
@@ -74,6 +75,15 @@ function isGenericCanonicalDriftIntent(intent) {
|
||||
intent === "bank_operations_by_contract" ||
|
||||
intent === "documents_forming_balance");
|
||||
}
|
||||
function hasInventoryMarginRankingAccountCorrectionCue(text) {
|
||||
const value = String(text ?? "").toLowerCase();
|
||||
if (!value.trim()) {
|
||||
return false;
|
||||
}
|
||||
return (/\b41(?:[.,]\d{1,2})?\b/iu.test(value) &&
|
||||
/\b01(?:[.,]\d{1,2})?\b/iu.test(value) &&
|
||||
/(?:\u0430\s+\u043d\u0435|\u043d\u0435|\u0432\u043c\u0435\u0441\u0442\u043e|not|instead)/iu.test(value));
|
||||
}
|
||||
function hasSameDateFollowupSignal(text) {
|
||||
return /(?:эту\s+же\s+дат(?:у|е|ой)|ту\s+же\s+дат(?:у|е|ой)|same\s+date)/iu.test(String(text ?? ""));
|
||||
}
|
||||
@@ -117,6 +127,10 @@ function shouldPreferRawFollowupMessage(userMessage, addressInputMessage, carryo
|
||||
const hasInventoryItemCarryover = previousAnchorType === "item" && isInventorySelectedObjectOrRootIntent(previousIntent);
|
||||
const hasInventoryFrameCarryover = isInventorySelectedObjectOrRootIntent(previousIntent) ||
|
||||
isInventorySelectedObjectOrRootIntent(rootIntent);
|
||||
const hasInventoryMarginRankingCarryover = previousIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
rootIntent === "inventory_margin_ranking_for_nomenclature";
|
||||
const hasInventoryMarginRankingAccountCorrection = hasInventoryMarginRankingCarryover &&
|
||||
[rawMessage, canonicalMessage].some((message) => hasInventoryMarginRankingAccountCorrectionCue(message));
|
||||
const hasDocumentCarryover = previousIntent === "list_documents_by_counterparty" || previousIntent === "list_documents_by_contract";
|
||||
if (mode === "unsupported" && intent === "unknown") {
|
||||
return true;
|
||||
@@ -132,6 +146,10 @@ function shouldPreferRawFollowupMessage(userMessage, addressInputMessage, carryo
|
||||
(intent === "account_balance_snapshot" || intent === "documents_forming_balance" || intent === "unknown")) {
|
||||
return true;
|
||||
}
|
||||
if (hasInventoryMarginRankingAccountCorrection &&
|
||||
(intent === "account_balance_snapshot" || intent === "documents_forming_balance" || intent === "unknown")) {
|
||||
return true;
|
||||
}
|
||||
return ((hasSelectedObjectInventorySignal(rawMessage) || hasInventoryItemCarryover) &&
|
||||
(hasSelectedObjectInventoryActionCue(rawMessage) || hasShortInventoryPurchaseFollowupCue(rawMessage)) &&
|
||||
(isGenericCanonicalDriftIntent(intent) || intent === "unknown"));
|
||||
|
||||
+7
@@ -8,6 +8,13 @@ function formatMissingAnchors(anchors) {
|
||||
return anchors.join(", ");
|
||||
}
|
||||
function buildClarificationReply(binding) {
|
||||
if (binding.capability_contract_id === "inventory_inventory_margin_ranking_for_nomenclature") {
|
||||
return [
|
||||
"Для рейтинга прибыльности номенклатуры нужен период.",
|
||||
"Могу посчитать по номенклатуре: выручку без НДС, себестоимость реализации, валовую прибыль и маржинальность.",
|
||||
"Уточните период: месяц, квартал, год или весь доступный период."
|
||||
].join("\n\n");
|
||||
}
|
||||
return [
|
||||
"Нужно уточнение, чтобы не подставить неподтвержденный объект в расчет.",
|
||||
`Не хватает: ${formatMissingAnchors(binding.missing_anchors)}.`,
|
||||
|
||||
+7
@@ -110,6 +110,13 @@ function anchorSatisfied(requiredAnchor, providedAnchors, debug) {
|
||||
if (providedAnchors.includes(requiredAnchor)) {
|
||||
return true;
|
||||
}
|
||||
if (requiredAnchor === "period") {
|
||||
return ((hasValue(filters?.period_from) && hasValue(filters?.period_to)) ||
|
||||
hasValue(filters?.as_of_date) ||
|
||||
providedAnchors.includes("period_from") ||
|
||||
providedAnchors.includes("period_to") ||
|
||||
providedAnchors.includes("as_of_date"));
|
||||
}
|
||||
if (requiredAnchor === "item") {
|
||||
return (providedAnchors.includes("selected_object") ||
|
||||
providedAnchors.includes("anchor_item") ||
|
||||
|
||||
+2
-1
@@ -141,6 +141,7 @@ function isDetectedIntentAlignedWithTurnMeaning(detectedIntent, turnMeaning) {
|
||||
if (normalizedIntent === "inventory_purchase_provenance_for_item" ||
|
||||
normalizedIntent === "inventory_purchase_documents_for_item" ||
|
||||
normalizedIntent === "inventory_sale_trace_for_item" ||
|
||||
normalizedIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
normalizedIntent === "inventory_profitability_for_item" ||
|
||||
normalizedIntent === "inventory_purchase_to_sale_chain") {
|
||||
return askedDomain === "inventory";
|
||||
@@ -183,7 +184,7 @@ function isExplicitMetadataDiscoveryTurn(entryPoint) {
|
||||
reasonCodes.some((reason) => toNonEmptyString(reason) === "mcp_discovery_metadata_signal_detected"));
|
||||
}
|
||||
function isInventoryExactAddressIntent(intent) {
|
||||
return /^(?:inventory_purchase_provenance_for_item|inventory_purchase_documents_for_item|inventory_sale_trace_for_item|inventory_profitability_for_item|inventory_purchase_to_sale_chain|inventory_aging_by_purchase_date|inventory_on_hand_as_of_date)$/u.test(String(intent ?? ""));
|
||||
return /^(?:inventory_purchase_provenance_for_item|inventory_purchase_documents_for_item|inventory_sale_trace_for_item|inventory_margin_ranking_for_nomenclature|inventory_profitability_for_item|inventory_purchase_to_sale_chain|inventory_aging_by_purchase_date|inventory_on_hand_as_of_date)$/u.test(String(intent ?? ""));
|
||||
}
|
||||
function hasMetadataDiscoveryPriority(input, entryPoint) {
|
||||
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||
|
||||
@@ -27,6 +27,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_margin_ranking_for_nomenclature",
|
||||
"inventory_profitability_for_item",
|
||||
"inventory_purchase_to_sale_chain",
|
||||
"inventory_aging_by_purchase_date",
|
||||
@@ -40,6 +41,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_margin_ranking_for_nomenclature",
|
||||
"inventory_profitability_for_item",
|
||||
"inventory_purchase_to_sale_chain"
|
||||
]);
|
||||
@@ -204,7 +206,7 @@ function createAssistantRoutePolicy(deps) {
|
||||
: null;
|
||||
const semanticCanonicalRecommended = semanticExtractionContract?.apply_canonical_recommended !== false;
|
||||
const llmSupportedDeepAddressIntentSignal = llmContractMode === "deep_analysis" &&
|
||||
/^(?:inventory_purchase_provenance_for_item|inventory_purchase_documents_for_item|inventory_sale_trace_for_item|inventory_profitability_for_item|inventory_purchase_to_sale_chain)$/u.test(llmContractIntent ?? "") &&
|
||||
/^(?:inventory_purchase_provenance_for_item|inventory_purchase_documents_for_item|inventory_sale_trace_for_item|inventory_margin_ranking_for_nomenclature|inventory_profitability_for_item|inventory_purchase_to_sale_chain)$/u.test(llmContractIntent ?? "") &&
|
||||
semanticCanonicalRecommended;
|
||||
const llmCanonicalEntitySignal = /(?:заказчик|поставщик|контрагент|компан|customer|supplier|counterparty|company|vendor|client)/iu.test(compactWhitespace(repairedInputMessage.toLowerCase()));
|
||||
const llmCanonicalAppliedSignal = Boolean(llmPreDecomposeMeta?.applied) && llmContractMode !== "deep_analysis";
|
||||
|
||||
@@ -310,6 +310,18 @@ exports.INVENTORY_CAPABILITY_CONTRACTS = [
|
||||
answerObjectShape: "inventory_profitability_bundle",
|
||||
bundleReusePolicy: "sale_trace_bundle_preferred"
|
||||
}),
|
||||
inventoryExactCapability({
|
||||
capability_id: "inventory_inventory_margin_ranking_for_nomenclature",
|
||||
intent_ids: ["inventory_margin_ranking_for_nomenclature"],
|
||||
entry_modes: ["root_entry", "root_followup", "clarification_resume"],
|
||||
transitions: ["T1", "T2", "T7"],
|
||||
requiresFocusObject: false,
|
||||
requiredAnchors: ["period"],
|
||||
resultShape: "nomenclature_margin_ranking",
|
||||
answerObjectShape: "inventory_margin_ranking",
|
||||
bundleReusePolicy: "none",
|
||||
scenarioFamilies: ["canonical", "colloquial", "account_41_correction"]
|
||||
}),
|
||||
inventoryExactCapability({
|
||||
capability_id: "inventory_inventory_purchase_to_sale_chain",
|
||||
intent_ids: ["inventory_purchase_to_sale_chain"],
|
||||
|
||||
@@ -7,6 +7,26 @@ function createAssistantTransitionPolicy(deps) {
|
||||
function normalizeFollowupText(value) {
|
||||
return deps.compactWhitespace(deps.repairAddressMojibake(String(value ?? "")).toLowerCase()).replace(/ё/g, "е");
|
||||
}
|
||||
function hasInventoryMarginRankingFollowupSignal(userMessage, alternateMessage = null, sourceIntentHint = null) {
|
||||
if (sourceIntentHint !== "inventory_margin_ranking_for_nomenclature") {
|
||||
return false;
|
||||
}
|
||||
return [userMessage, alternateMessage]
|
||||
.filter((value) => deps.toNonEmptyString(value))
|
||||
.map((value) => deps.compactWhitespace(deps.repairAddressMojibake(String(value ?? "")).toLowerCase()).replace(/ё/g, "е"))
|
||||
.some((normalized) => {
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const wantsFoundRows = /(?:покажи|показать|выведи|дай|раскрой|show|list|покажи|показать|выведи|дай|раскрой)/iu.test(normalized) &&
|
||||
/(?:найденн|строк|реализац|себестоимостн|баз|найденн|строк|реализац|себестоимостн|баз)/iu.test(normalized) &&
|
||||
/(?:себестоимостн|реализац|марж|прибыл|номенклатур|себестоимостн|реализац|марж|прибыл|номенклат)/iu.test(normalized);
|
||||
const account41Not01 = /\b41(?:[.,]\d{1,2})?\b/iu.test(normalized) &&
|
||||
/\b01(?:[.,]\d{1,2})?\b/iu.test(normalized) &&
|
||||
/(?:\bне\b|вместо|а\s+не|not|instead|РЅРµ|вместо|Р°\s+РЅРµ)/iu.test(normalized);
|
||||
return wantsFoundRows || account41Not01;
|
||||
});
|
||||
}
|
||||
function hasSamePeriodReferenceCue(...values) {
|
||||
return values
|
||||
.map((value) => normalizeFollowupText(value))
|
||||
@@ -545,6 +565,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
deps.isInventorySelectedObjectIntent(sourceIntentHint)));
|
||||
const inventoryPurchaseDateVatBridge = hasInventoryPurchaseDateVatBridgeSignal(userMessage, alternateMessage, sourceIntentHint, hasNavigationInventoryItemFocusHint);
|
||||
const inventoryMarginRankingFollowup = hasInventoryMarginRankingFollowupSignal(userMessage, alternateMessage, sourceIntentHint);
|
||||
let inventoryShortFollowupPrimary = (deps.isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) &&
|
||||
deps.hasShortInventoryObjectFollowupSignal(userMessage);
|
||||
let inventoryShortFollowupAlternate = (deps.isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) &&
|
||||
@@ -573,6 +594,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||
businessOverviewBoundaryFollowupPrimary ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
inventoryMarginRankingFollowup ||
|
||||
explicitSummaryBundleReuseSignal ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation;
|
||||
let hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
||||
@@ -582,6 +604,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||
businessOverviewBoundaryFollowupAlternate ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
inventoryMarginRankingFollowup ||
|
||||
explicitSummaryBundleReuseSignal ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation
|
||||
: false;
|
||||
@@ -622,6 +645,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||
shortValueFlowRetargetAlternate ||
|
||||
businessOverviewBoundaryFollowupPrimary ||
|
||||
businessOverviewBoundaryFollowupAlternate ||
|
||||
inventoryMarginRankingFollowup ||
|
||||
deps.hasFollowupMarker(userMessage) ||
|
||||
deps.hasReferentialPointer(userMessage) ||
|
||||
(deps.toNonEmptyString(alternateMessage)
|
||||
@@ -645,6 +669,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||
shortValueFlowRetargetAlternate ||
|
||||
businessOverviewBoundaryFollowupPrimary ||
|
||||
businessOverviewBoundaryFollowupAlternate ||
|
||||
inventoryMarginRankingFollowup ||
|
||||
deps.hasFollowupMarker(userMessage) ||
|
||||
deps.hasReferentialPointer(userMessage) ||
|
||||
(deps.toNonEmptyString(alternateMessage)
|
||||
@@ -757,6 +782,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||
!inventoryShortFollowupAlternate &&
|
||||
!businessOverviewBoundaryFollowupPrimary &&
|
||||
!businessOverviewBoundaryFollowupAlternate &&
|
||||
!inventoryMarginRankingFollowup &&
|
||||
!foreignAccountingPivotOverInventory &&
|
||||
!deps.hasFollowupMarker(userMessage) &&
|
||||
!deps.hasReferentialPointer(userMessage) &&
|
||||
@@ -816,6 +842,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||
businessOverviewBoundaryFollowupPrimary ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
inventoryMarginRankingFollowup ||
|
||||
explicitSummaryBundleReuseSignal ||
|
||||
hasInventoryRootTemporalFollowupPrimary ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation;
|
||||
@@ -827,6 +854,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||
businessOverviewBoundaryFollowupAlternate ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
inventoryMarginRankingFollowup ||
|
||||
explicitSummaryBundleReuseSignal ||
|
||||
hasInventoryRootTemporalFollowupAlternate ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation
|
||||
@@ -849,6 +877,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||
shortValueFlowRetargetAlternate ||
|
||||
businessOverviewBoundaryFollowupPrimary ||
|
||||
businessOverviewBoundaryFollowupAlternate ||
|
||||
inventoryMarginRankingFollowup ||
|
||||
deps.hasFollowupMarker(userMessage) ||
|
||||
deps.hasReferentialPointer(userMessage) ||
|
||||
(deps.toNonEmptyString(alternateMessage)
|
||||
|
||||
@@ -24,6 +24,7 @@ const SUPPORTED_ADDRESS_INTENTS = new Set([
|
||||
"inventory_purchase_documents_for_item",
|
||||
"inventory_supplier_stock_overlap_as_of_date",
|
||||
"inventory_sale_trace_for_item",
|
||||
"inventory_margin_ranking_for_nomenclature",
|
||||
"inventory_profitability_for_item",
|
||||
"inventory_purchase_to_sale_chain",
|
||||
"inventory_aging_by_purchase_date",
|
||||
|
||||
Reference in New Issue
Block a user