АРЧ - Склад: сохранять полный item-anchor в selected-object sale trace и закрепить buyer follow-up регрессиями
This commit is contained in:
@@ -3140,6 +3140,7 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||
const purchaseRows = rows.filter((row) => isInventoryPurchaseMovement(row));
|
||||
const summary = summarizeInventoryTraceRows(purchaseRows);
|
||||
const itemLabel = summary.item ?? "товар не определен";
|
||||
const boundedAsOfLabel = asOfDate ? formatDateRu(asOfDate) : null;
|
||||
const purchaseDateActionFocus = hasInventoryPurchaseDateActionFocus(options.userMessage);
|
||||
if (purchaseDateActionFocus) {
|
||||
const firstPurchaseDate = inventoryTraceDateLabel(summary.firstPeriod);
|
||||
@@ -3148,7 +3149,9 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||
? `По позиции ${itemLabel} подтвержденная дата закупки в доступном контуре не найдена.`
|
||||
: summary.firstPeriod === summary.lastPeriod
|
||||
? `Позиция ${itemLabel} куплена ${firstPurchaseDate}.`
|
||||
: `По позиции ${itemLabel} подтвержденный диапазон закупок: с ${firstPurchaseDate} по ${lastPurchaseDate}.`;
|
||||
: boundedAsOfLabel
|
||||
? `По позиции ${itemLabel} до ${boundedAsOfLabel} подтвержден диапазон закупок: с ${firstPurchaseDate} по ${lastPurchaseDate}.`
|
||||
: `По позиции ${itemLabel} подтвержденный диапазон закупок: с ${firstPurchaseDate} по ${lastPurchaseDate}.`;
|
||||
const lines = [directAnswerLine];
|
||||
if (purchaseRows.length > 0) {
|
||||
lines.push("", "Подтверждение:");
|
||||
@@ -3165,8 +3168,11 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||
if (summary.documents.length > 0) {
|
||||
lines.push(`- Первый подтверждающий документ: ${summary.documents[0]}.`);
|
||||
}
|
||||
if (summary.firstPeriod && asOfDate && summary.firstPeriod < asOfDate) {
|
||||
lines.push(`- Дата вопроса по остатку: ${formatDateRu(asOfDate)}; дата закупки показана по подтвержденному закупочному следу.`);
|
||||
if (boundedAsOfLabel) {
|
||||
lines.push(`- Для ответа учтены закупочные документы не позже ${boundedAsOfLabel}.`);
|
||||
}
|
||||
if (summary.counterparties.length > 1) {
|
||||
lines.push(`- Без партионного учета нельзя однозначно связать остаток${boundedAsOfLabel ? ` на ${boundedAsOfLabel}` : ""} с одним конкретным поступлением.`);
|
||||
}
|
||||
}
|
||||
return {
|
||||
@@ -3179,33 +3185,51 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||
}
|
||||
};
|
||||
}
|
||||
const directAnswerLine = summary.counterparties.length === 1
|
||||
? `Товар ${itemLabel} по доступным закупочным движениям связан с поставщиком: ${summary.counterparties[0]}.`
|
||||
: summary.counterparties.length > 1
|
||||
? `По доступным закупочным движениям по товару ${itemLabel} найдено несколько поставщиков: ${summary.counterparties.slice(0, 4).join("; ")}.`
|
||||
: `По товару ${itemLabel} найден закупочный след, но поставщик не материализован отдельным полем в текущем exact-контуре.`;
|
||||
const directAnswerLine = purchaseRows.length <= 0
|
||||
? boundedAsOfLabel
|
||||
? `По позиции ${itemLabel} подтвержденные закупочные документы до ${boundedAsOfLabel} не найдены.`
|
||||
: `По позиции ${itemLabel} подтвержденные закупочные документы не найдены.`
|
||||
: summary.counterparties.length === 1
|
||||
? boundedAsOfLabel
|
||||
? `По позиции ${itemLabel} до ${boundedAsOfLabel} подтвержден поставщик: ${summary.counterparties[0]}.`
|
||||
: `По позиции ${itemLabel} подтвержден поставщик: ${summary.counterparties[0]}.`
|
||||
: summary.counterparties.length > 1
|
||||
? boundedAsOfLabel
|
||||
? `По позиции ${itemLabel} до ${boundedAsOfLabel} однозначный поставщик не подтвержден: в документах встречаются ${summary.counterparties.slice(0, 4).join("; ")}.`
|
||||
: `По позиции ${itemLabel} однозначный поставщик не подтвержден: в документах встречаются ${summary.counterparties.slice(0, 4).join("; ")}.`
|
||||
: boundedAsOfLabel
|
||||
? `По позиции ${itemLabel} до ${boundedAsOfLabel} закупочные документы найдены, но поставщик в них не выделен отдельным полем.`
|
||||
: `По позиции ${itemLabel} закупочные документы найдены, но поставщик в них не выделен отдельным полем.`;
|
||||
const lines = [directAnswerLine, "", "Подтверждение:"];
|
||||
lines.push(`- Первая найденная дата закупки: ${inventoryTraceDateLabel(summary.firstPeriod)}.`);
|
||||
lines.push(`- Последняя найденная дата закупки: ${inventoryTraceDateLabel(summary.lastPeriod)}.`);
|
||||
lines.push(`- Документов поступления: ${formatNumberWithDots(summary.documents.length)}.`);
|
||||
lines.push(`- Операций поступления: ${formatNumberWithDots(purchaseRows.length)}.`);
|
||||
if (summary.counterparties.length === 1) {
|
||||
lines.push(`- По доступным закупочным движениям товар связан с поставщиком: ${summary.counterparties[0]}.`);
|
||||
if (purchaseRows.length > 0) {
|
||||
lines.push(`- Первая найденная дата закупки: ${inventoryTraceDateLabel(summary.firstPeriod)}.`);
|
||||
lines.push(`- Последняя найденная дата закупки: ${inventoryTraceDateLabel(summary.lastPeriod)}.`);
|
||||
lines.push(`- Документов поступления: ${formatNumberWithDots(summary.documents.length)}.`);
|
||||
lines.push(`- Операций поступления: ${formatNumberWithDots(purchaseRows.length)}.`);
|
||||
if (summary.counterparties.length === 1) {
|
||||
lines.push(`- Поставщик в найденных документах: ${summary.counterparties[0]}.`);
|
||||
}
|
||||
else if (summary.counterparties.length > 1) {
|
||||
lines.push(`- Поставщики в найденных документах: ${summary.counterparties.slice(0, 6).join("; ")}.`);
|
||||
}
|
||||
else {
|
||||
lines.push("- Закупочные документы найдены, но поставщик в них не выделен отдельным полем.");
|
||||
}
|
||||
if (boundedAsOfLabel) {
|
||||
lines.push(`- Для ответа учтены закупочные документы не позже ${boundedAsOfLabel}.`);
|
||||
}
|
||||
if (summary.counterparties.length > 1) {
|
||||
lines.push(`- Без партионного учета нельзя однозначно связать остаток${boundedAsOfLabel ? ` на ${boundedAsOfLabel}` : ""} с одним конкретным поступлением.`);
|
||||
}
|
||||
}
|
||||
else if (summary.counterparties.length > 1) {
|
||||
lines.push(`- По доступным закупочным движениям найдено несколько поставщиков: ${summary.counterparties.slice(0, 4).join("; ")}.`);
|
||||
}
|
||||
else if (purchaseRows.length > 0) {
|
||||
lines.push("- Закупочные документы найдены, но поставщик не материализован отдельным полем в текущем exact-контуре.");
|
||||
else if (boundedAsOfLabel) {
|
||||
lines.push(`- Для ответа проверены закупочные документы не позже ${boundedAsOfLabel}.`);
|
||||
}
|
||||
if (summary.documents.length > 0) {
|
||||
lines.push("", "Опорные документы:", ...formatInventoryTraceRows(purchaseRows, 8));
|
||||
}
|
||||
if (purchaseRows.length > 0) {
|
||||
lines.push("", "Сервисно:", "- Без партионности этот контур показывает документально наблюдаемый закупочный след, а не лот-level происхождение текущего остатка.");
|
||||
}
|
||||
return {
|
||||
responseType: purchaseRows.length > 0 ? "FACTUAL_SUMMARY" : "FACTUAL_SUMMARY",
|
||||
responseType: "FACTUAL_SUMMARY",
|
||||
text: joinLines(lines),
|
||||
semantics: {
|
||||
result_mode: "confirmed_balance",
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.hasInventorySupplierFollowupCue = hasInventorySupplierFollowupCue;
|
||||
exports.hasInventoryPurchaseDocumentsFollowupCue = hasInventoryPurchaseDocumentsFollowupCue;
|
||||
exports.hasInventoryPurchaseDateFollowupCue = hasInventoryPurchaseDateFollowupCue;
|
||||
exports.hasBareInventoryPurchaseDateFollowupCue = hasBareInventoryPurchaseDateFollowupCue;
|
||||
exports.hasInventorySaleFollowupCue = hasInventorySaleFollowupCue;
|
||||
exports.hasInventoryPurchaseToSaleChainFollowupCue = hasInventoryPurchaseToSaleChainFollowupCue;
|
||||
exports.hasAddressFollowupContextSignal = hasAddressFollowupContextSignal;
|
||||
exports.runAddressDecomposeStage = runAddressDecomposeStage;
|
||||
const addressQueryClassifier_1 = require("../addressQueryClassifier");
|
||||
const addressQueryShapeClassifier_1 = require("../addressQueryShapeClassifier");
|
||||
const addressIntentResolver_1 = require("../addressIntentResolver");
|
||||
const addressFilterExtractor_1 = require("../addressFilterExtractor");
|
||||
const inventoryLifecycleCueHelpers_1 = require("../inventoryLifecycleCueHelpers");
|
||||
const semanticHintOverlay_1 = require("./semanticHintOverlay");
|
||||
function hasExplicitPeriodWindow(filters) {
|
||||
return ((typeof filters.period_from === "string" && filters.period_from.trim().length > 0) ||
|
||||
@@ -402,13 +409,14 @@ function hasSelectedObjectInventorySignal(text) {
|
||||
return /(?:по\s+выбранному\s+объекту|for\s+selected\s+object)/iu.test(String(text ?? ""));
|
||||
}
|
||||
function hasInventorySupplierFollowupCue(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+куплено|supplier|vendor|поставщик)/iu.test(String(text ?? ""));
|
||||
return (0, inventoryLifecycleCueHelpers_1.hasInventorySupplierCue)(String(text ?? ""));
|
||||
}
|
||||
function hasInventoryPurchaseDocumentsFollowupCue(text) {
|
||||
return /(?:по\s+каким\s+документам\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|по\s+каким\s+документам\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|какими\s+документами\s+(?:был\s+)?куплен|покажи\s+документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(String(text ?? ""));
|
||||
}
|
||||
function hasInventoryPurchaseDateFollowupCue(text) {
|
||||
return /(?:когда\s+(?:примерно\s+)?(?:мы\s+)?купили|когда\s+был\s+куплен|когда\s+куплен|когда\s+это\s+купили|когда\s+эту\s+позицию\s+купили|когда\s+ее\s+купили|дата\s+закупки|purchase\s+date)/iu.test(String(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));
|
||||
}
|
||||
function hasBareInventoryPurchaseDateFollowupCue(text) {
|
||||
const normalized = String(text ?? "").trim().toLowerCase();
|
||||
@@ -418,7 +426,7 @@ function hasBareInventoryPurchaseDateFollowupCue(text) {
|
||||
return /(?:^|\s)(?:когда|а\s+когда|ну\s+когда)(?=$|[\s,.;:!?])/iu.test(normalized) && normalized.split(/\s+/).filter(Boolean).length <= 3;
|
||||
}
|
||||
function hasInventorySaleFollowupCue(text) {
|
||||
return /(?:кому\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали|кому\s+был\s+продан|кому\s+дальше\s+продали|куда\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали|куда\s+ушла\s+позиция|куда\s+ушел\s+товар|кто\s+купил|buyer|покупател)/iu.test(String(text ?? ""));
|
||||
return (0, inventoryLifecycleCueHelpers_1.hasInventorySaleCue)(String(text ?? ""));
|
||||
}
|
||||
function hasInventoryPurchaseToSaleChainFollowupCue(text) {
|
||||
return /(?:через\s+какие\s+документы\s+прош[её]л\s+путь\s+товара|закупк.*склад.*продаж|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*(?:warehouse|stock)\s*->\s*sale)/iu.test(String(text ?? ""));
|
||||
@@ -608,12 +616,34 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date") &&
|
||||
!toNonEmptyString(merged.item)) {
|
||||
intent === "inventory_aging_by_purchase_date")) {
|
||||
const inheritedItem = previousItem ?? previousAnchorItem;
|
||||
if (inheritedItem && intent !== "inventory_aging_by_purchase_date") {
|
||||
const explicitQuotedItem = toNonEmptyString((0, addressFilterExtractor_1.extractSelectedObjectQuotedValue)(userMessage));
|
||||
const currentItem = toNonEmptyString(merged.item);
|
||||
const shouldAdoptExplicitQuotedItem = Boolean(explicitQuotedItem) &&
|
||||
(!currentItem ||
|
||||
currentItem !== explicitQuotedItem ||
|
||||
(0, addressFilterExtractor_1.isInventoryItemAnchorDegradation)(explicitQuotedItem ?? "", currentItem ?? ""));
|
||||
if (explicitQuotedItem && shouldAdoptExplicitQuotedItem) {
|
||||
merged.item = explicitQuotedItem;
|
||||
reasons.push(currentItem ? "item_replaced_from_explicit_quote" : "item_from_explicit_quote");
|
||||
}
|
||||
const effectiveCurrentItem = toNonEmptyString(merged.item);
|
||||
const hasExplicitDifferentQuotedItem = Boolean(explicitQuotedItem) &&
|
||||
Boolean(inheritedItem) &&
|
||||
explicitQuotedItem !== inheritedItem;
|
||||
const shouldInheritItem = Boolean(inheritedItem) &&
|
||||
intent !== "inventory_aging_by_purchase_date" &&
|
||||
!hasExplicitDifferentQuotedItem &&
|
||||
(!effectiveCurrentItem ||
|
||||
((0, addressFilterExtractor_1.isLowQualityInventoryItemAnchorValue)(effectiveCurrentItem) &&
|
||||
!(0, addressFilterExtractor_1.isLowQualityInventoryItemAnchorValue)(inheritedItem ?? "")) ||
|
||||
(effectiveCurrentItem &&
|
||||
inheritedItem &&
|
||||
(0, addressFilterExtractor_1.isInventoryItemAnchorDegradation)(inheritedItem, effectiveCurrentItem)));
|
||||
if (shouldInheritItem && inheritedItem) {
|
||||
merged.item = inheritedItem;
|
||||
reasons.push("item_from_followup_context");
|
||||
reasons.push(effectiveCurrentItem ? "item_replaced_from_followup_context" : "item_from_followup_context");
|
||||
}
|
||||
}
|
||||
if (sameDateRequested) {
|
||||
|
||||
+35
-5
@@ -2,6 +2,7 @@
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.normalizeAddressLlmSemanticHints = normalizeAddressLlmSemanticHints;
|
||||
exports.applyAddressLlmSemanticHintsToExtraction = applyAddressLlmSemanticHintsToExtraction;
|
||||
const addressFilterExtractor_1 = require("../addressFilterExtractor");
|
||||
function toNonEmptyString(value) {
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
@@ -66,6 +67,29 @@ function applyDateScopeHint(frame, dateScopeKind) {
|
||||
frame.date_basis_hint = "implicit_current_snapshot";
|
||||
}
|
||||
}
|
||||
function normalizeInventoryItemAnchorValue(value) {
|
||||
return String(value ?? "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, " ");
|
||||
}
|
||||
function shouldApplyInventoryItemSemanticHint(currentItemValue, hintedItemValue) {
|
||||
if (!hintedItemValue || (0, addressFilterExtractor_1.isLowQualityInventoryItemAnchorValue)(hintedItemValue)) {
|
||||
return false;
|
||||
}
|
||||
if (!currentItemValue || (0, addressFilterExtractor_1.isLowQualityInventoryItemAnchorValue)(currentItemValue)) {
|
||||
return true;
|
||||
}
|
||||
if ((0, addressFilterExtractor_1.isInventoryItemAnchorDegradation)(currentItemValue, hintedItemValue)) {
|
||||
return false;
|
||||
}
|
||||
const currentNormalized = normalizeInventoryItemAnchorValue(currentItemValue);
|
||||
const hintedNormalized = normalizeInventoryItemAnchorValue(hintedItemValue);
|
||||
if (currentNormalized === hintedNormalized) {
|
||||
return false;
|
||||
}
|
||||
return hintedNormalized.includes(currentNormalized);
|
||||
}
|
||||
function applyAddressLlmSemanticHintsToExtraction(extraction, semanticHintsInput) {
|
||||
const semanticHints = normalizeAddressLlmSemanticHints(semanticHintsInput);
|
||||
if (!semanticHints) {
|
||||
@@ -123,11 +147,17 @@ function applyAddressLlmSemanticHintsToExtraction(extraction, semanticHintsInput
|
||||
semanticFrame.anchor_value = scopeTargetText;
|
||||
}
|
||||
if (semanticHints.scope_target_kind === "item" && scopeTargetText) {
|
||||
extractedFilters.item = scopeTargetText;
|
||||
pushWarning(warnings, "item_from_llm_semantics");
|
||||
semanticFrame.scope_kind = "explicit_anchor";
|
||||
semanticFrame.anchor_kind = "item";
|
||||
semanticFrame.anchor_value = scopeTargetText;
|
||||
const currentItemValue = toNonEmptyString(extractedFilters.item);
|
||||
if (shouldApplyInventoryItemSemanticHint(currentItemValue, scopeTargetText)) {
|
||||
extractedFilters.item = scopeTargetText;
|
||||
pushWarning(warnings, "item_from_llm_semantics");
|
||||
semanticFrame.scope_kind = "explicit_anchor";
|
||||
semanticFrame.anchor_kind = "item";
|
||||
semanticFrame.anchor_value = scopeTargetText;
|
||||
}
|
||||
else if (currentItemValue && currentItemValue !== scopeTargetText) {
|
||||
pushWarning(warnings, "item_llm_semantics_ignored");
|
||||
}
|
||||
}
|
||||
return {
|
||||
...extraction,
|
||||
|
||||
Reference in New Issue
Block a user