АРЧ - Склад: сохранять полный item-anchor в selected-object sale trace и закрепить buyer follow-up регрессиями

This commit is contained in:
2026-04-15 17:57:05 +03:00
parent 7a6d8eb070
commit f911f9893b
27 changed files with 1539 additions and 180 deletions
@@ -3,6 +3,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.isLowQualityInventoryItemAnchorValue = isLowQualityInventoryItemAnchorValue;
exports.isInventoryItemAnchorDegradation = isInventoryItemAnchorDegradation;
exports.extractSelectedObjectQuotedValue = extractSelectedObjectQuotedValue;
exports.extractAddressFilters = extractAddressFilters;
const iconv_lite_1 = __importDefault(require("iconv-lite"));
const ACCOUNT_PATTERN = /(?:сч[её]т|счет|account)[^0-9]{0,12}(\d{2}(?:[.,]\d{1,2})?)/i;
@@ -836,16 +839,47 @@ function isLowQualityInventoryItemAnchorValue(rawValue) {
return true;
}
const lowQualityTokens = new Set([
"в",
"во",
"на",
"по",
"у",
"от",
"из",
"для",
"и",
"или",
"это",
"этот",
"эту",
"его",
"ее",
"её",
"итог",
"итоге",
"итогу",
"сейчас",
"лежат",
"лежит",
"лежали",
"был",
"была",
"было",
"были",
"куплен",
"куплена",
"куплены",
"куплено",
"продан",
"продана",
"проданы",
"продано",
"продали",
"реализован",
"реализована",
"реализованы",
"реализовано",
"реализовали",
"документам",
"документами",
"документы",
@@ -865,6 +899,37 @@ function isLowQualityInventoryItemAnchorValue(rawValue) {
.filter((token) => !lowQualityTokens.has(token));
return meaningfulTokens.length === 0;
}
function normalizeInventoryItemAnchorForComparison(rawValue) {
return cleanupAnchorValue(rawValue)
.trim()
.toLowerCase()
.replace(/С‘/g, "Рµ")
.replace(/\s+/g, " ");
}
function tokenizeInventoryItemAnchorForComparison(rawValue) {
return normalizeInventoryItemAnchorForComparison(rawValue)
.split(/[^a-zа-я0-9]+/iu)
.map((token) => token.trim())
.filter(Boolean);
}
function isInventoryItemAnchorDegradation(sourceValue, candidateValue) {
const sourceNormalized = normalizeInventoryItemAnchorForComparison(sourceValue);
const candidateNormalized = normalizeInventoryItemAnchorForComparison(candidateValue);
if (!sourceNormalized || !candidateNormalized || sourceNormalized === candidateNormalized) {
return false;
}
if (isLowQualityInventoryItemAnchorValue(candidateNormalized)) {
return true;
}
if (sourceNormalized.includes(candidateNormalized) && candidateNormalized.length < sourceNormalized.length) {
return true;
}
const sourceTokens = tokenizeInventoryItemAnchorForComparison(sourceNormalized);
const candidateTokens = tokenizeInventoryItemAnchorForComparison(candidateNormalized);
return (candidateTokens.length > 0 &&
candidateTokens.length < sourceTokens.length &&
candidateTokens.every((token) => sourceTokens.includes(token)));
}
function cleanupInventoryItemAnchorValue(value) {
return String(value ?? "")
.replace(/^['"«»“”„`’‘]+|['"«»“”„`’‘]+$/gu, "")
@@ -886,6 +951,31 @@ function trimInventoryItemAnchorTail(rawValue) {
}
return cleanupInventoryItemAnchorValue(value);
}
function extractQuotedAnchorValue(text) {
const patterns = [/[«"]([^«»"\r\n]+)[»"]/u, /'([^'\r\n]+)'/u];
for (const pattern of patterns) {
const match = String(text ?? "").match(pattern);
const candidate = cleanupInventoryItemAnchorValue(String(match?.[1] ?? ""));
if (candidate) {
return candidate;
}
}
return undefined;
}
function extractUnicodeQuotedAnchorValue(text) {
const patterns = [
/(?:\u00AB|\u0412\u00AB|")([^\u00AB\u00BB"\r\n]+?)(?:\u00BB|\u0412\u00BB|")/u,
/'([^'\r\n]+)'/u
];
for (const pattern of patterns) {
const match = String(text ?? "").match(pattern);
const candidate = cleanupInventoryItemAnchorValue(String(match?.[1] ?? ""));
if (candidate) {
return candidate;
}
}
return undefined;
}
function extractSelectedObjectQuotedValue(text) {
const patterns = [
/(?:по\s+выбранному\s+объекту|for\s+selected\s+object)\s*[«"]([^»"\r\n]+)[»"]/iu,
@@ -898,7 +988,7 @@ function extractSelectedObjectQuotedValue(text) {
return candidate;
}
}
return undefined;
return extractUnicodeQuotedAnchorValue(text) ?? extractQuotedAnchorValue(text);
}
function extractInventoryItemFromSelectedObject(text) {
const selectedObject = extractSelectedObjectQuotedValue(text);
@@ -923,6 +1013,10 @@ function extractInventoryItemAnchor(text) {
if (selectedObjectItem) {
return selectedObjectItem;
}
const quotedItem = extractUnicodeQuotedAnchorValue(text) ?? extractQuotedAnchorValue(text);
if (quotedItem && !isLowQualityInventoryItemAnchorValue(quotedItem)) {
return quotedItem;
}
const patterns = [
/(?:товар(?:а|у|ом|ы)?|номенклатур(?:а|у|ы)|позици(?:я|ю|и)|item|product|sku)\s*[«"']([^«»"'?\r\n]+)[»"'](?=$|[\s,.;:!?])/iu,
/(?:товар(?:а|у|ом|ы)?|номенклатур(?:а|у|ы)|позици(?:я|ю|и)|item|product|sku)\s+([^\r\n,.;:!?]+?)(?=\s+(?:на|по|у|от|из|для|и|когда|через|сейчас|еще|ещё|котор|которые|который|покупателю|поставщика|поставщику|за|в)\b|[:?]|$)/iu
@@ -1,6 +1,7 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveAddressIntent = resolveAddressIntent;
const inventoryLifecycleCueHelpers_1 = require("./inventoryLifecycleCueHelpers");
const RECEIVABLES_STRONG = [
"кто должен нам",
"кто нам должен",
@@ -1345,21 +1346,19 @@ function hasSelectedObjectInventoryCue(text) {
return /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+нему|по\s+ней|по\s+нему\s+же|по\s+ней\s+же|selected\s+object)/iu.test(text);
}
function hasSelectedObjectInventoryProvenanceSignal(text) {
return (hasSelectedObjectInventoryCue(text) &&
/(?:кто\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(text));
return hasSelectedObjectInventoryCue(text) && (0, inventoryLifecycleCueHelpers_1.hasInventorySupplierCue)(text);
}
function hasSelectedObjectInventoryPurchaseDocumentsSignal(text) {
return (hasSelectedObjectInventoryCue(text) &&
/(?:по\s+каким\s+документам\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|по\s+каким\s+документам\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|какими\s+документами\s+(?:был\s+)?куплен|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(text));
}
function hasSelectedObjectInventorySaleTraceSignal(text) {
return (hasSelectedObjectInventoryCue(text) &&
/(?:кому\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали|кому\s+был\s+продан|куда\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|куда\s+(?:была\s+)?реализована\s+(?:позиция|номенклатура|продукция)|кто\s+купил|buyer|sale\s+trace|trace\s+of\s+sale)/iu.test(text));
return hasSelectedObjectInventoryCue(text) && (0, inventoryLifecycleCueHelpers_1.hasInventorySaleCue)(text);
}
function hasInventoryProvenanceSignalV2(text) {
const hasItemCue = /(?:товар|номенклатур|sku|item|product|остат(?:ок|ки)|склад)/iu.test(text);
const hasSupplierCue = /(?:от\s+какого\s+поставщика|у\s+какого\s+поставщика|от\s+кого\s+куплен|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|товар|позицию))?|где\s+куплено|кто\s+(?:нам\s+)?поставил|кем\s+поставлен|поставщик|supplier|vendor)/iu.test(text);
const hasPurchaseCue = /(?:куплен(?:ы|а|о)?|закупк|происхождени|откуда|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|товар|позицию))?|где\s+куплено|когда\s+был\s+куплен|когда\s+куплен|дата\s+закупк|кто\s+(?:нам\s+)?поставил|кем\s+поставлен|поставлен(?:ы|а)?|purchase\s+provenance|purchase\s+date)/iu.test(text);
const hasSupplierCue = (0, inventoryLifecycleCueHelpers_1.hasInventorySupplierCue)(text) || /кем\s+поставлен/iu.test(text);
const hasPurchaseCue = /(?:куплен(?:ы|а|о)?|закупк|происхождени|откуда|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|товар|позицию))?|где\s+куплено|когда\s+был\s+куплен|когда\s+куплен|дата\s+закупк|кто\s+(?:нам\s+)?поставил|кем\s+поставлен|поставлен(?:ы|а)?|purchase\s+provenance|purchase\s+date)/iu.test(text) || (0, inventoryLifecycleCueHelpers_1.hasInventoryPurchaseStem)(text);
return hasItemCue && hasSupplierCue && hasPurchaseCue;
}
function hasInventoryPurchaseDateSignal(text) {
@@ -1,6 +1,7 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.detectAddressQuestionMode = detectAddressQuestionMode;
const inventoryLifecycleCueHelpers_1 = require("./inventoryLifecycleCueHelpers");
const ADDRESS_ACTION_TOKENS = [
"show",
"list",
@@ -277,7 +278,10 @@ function hasSelectedObjectInventoryFollowupSignal(text) {
if (!/(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции)/iu.test(text)) {
return false;
}
return /(?:у\s+кого\s+купили|у\s+кого\s+куплено|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|товар|позицию))?|где\s+куплено|кто\s+(?:поставил|продал)|кому\s+(?:продали|реализовали)|когда\s+(?:примерно\s+)?купили|по\s+каким\s+документам\s+.*купили)/iu.test(text);
return ((0, inventoryLifecycleCueHelpers_1.hasInventorySupplierCue)(text) ||
(0, inventoryLifecycleCueHelpers_1.hasInventorySaleCue)(text) ||
/(?:кто\s+(?:поставил|продал)|по\s+каким\s+документам\s+.*купили)/iu.test(text) ||
(/\bкогда\b/iu.test(text) && (0, inventoryLifecycleCueHelpers_1.hasInventoryPurchaseStem)(text)));
}
function hasDocsOrBankSignal(text) {
return /(?:док(?:и|умент|ументы|ументов)|docs?|documents?|банк|выписк|платеж|платёж|оплат|поступлен|списан|транзак|transactions?|bank\s+ops|bank\s+operations?)/iu.test(text);
@@ -1611,15 +1611,11 @@ function shouldBoostAutoBroadenedLimit(intent) {
intent === "inventory_aging_by_purchase_date");
}
function shouldClearAsOfDateForHistoryRecovery(intent) {
return (intent === "inventory_purchase_provenance_for_item" ||
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_sale_trace_for_item" ||
return (intent === "inventory_sale_trace_for_item" ||
intent === "inventory_purchase_to_sale_chain");
}
function shouldDetachLifecycleExecutionFromSnapshotContext(intent, reasons) {
if (intent !== "inventory_purchase_provenance_for_item" &&
intent !== "inventory_purchase_documents_for_item" &&
intent !== "inventory_sale_trace_for_item" &&
if (intent !== "inventory_sale_trace_for_item" &&
intent !== "inventory_purchase_to_sale_chain") {
return false;
}
+71 -14
View File
@@ -42,6 +42,42 @@ __WHERE_CLAUSE__
УПОРЯДОЧИТЬ ПО
Движения.Период __ORDER_DIRECTION__
`;
const INVENTORY_SALE_DOCUMENTS_QUERY_TEMPLATE = `
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
Товары.Ссылка.Дата КАК Период,
ПРЕДСТАВЛЕНИЕ(Товары.Ссылка) КАК Регистратор,
"" КАК СчетДт,
"41.01" КАК СчетКт,
Товары.Сумма КАК Сумма,
ПРЕДСТАВЛЕНИЕ(Товары.Номенклатура) КАК Номенклатура,
ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Контрагент) КАК Контрагент,
ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.ДоговорКонтрагента) КАК Договор,
ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Организация) КАК Организация,
Товары.Количество КАК Количество
ИЗ
Документ.РеализацияТоваровУслуг.Товары КАК Товары
__WHERE_CLAUSE__
УПОРЯДОЧИТЬ ПО
Товары.Ссылка.Дата __ORDER_DIRECTION__
`;
const INVENTORY_PURCHASE_DOCUMENTS_QUERY_TEMPLATE = `
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
Товары.Ссылка.Дата КАК Период,
ПРЕДСТАВЛЕНИЕ(Товары.Ссылка) КАК Регистратор,
"41.01" КАК СчетДт,
"" КАК СчетКт,
Товары.Сумма КАК Сумма,
ПРЕДСТАВЛЕНИЕ(Товары.Номенклатура) КАК Номенклатура,
ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Контрагент) КАК Контрагент,
ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.ДоговорКонтрагента) КАК Договор,
ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Организация) КАК Организация,
Товары.Количество КАК Количество
ИЗ
Документ.ПоступлениеТоваровУслуг.Товары КАК Товары
__WHERE_CLAUSE__
УПОРЯДОЧИТЬ ПО
Товары.Ссылка.Дата __ORDER_DIRECTION__
`;
const PAYABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
__AS_OF_EXPR__ КАК Период,
@@ -938,15 +974,6 @@ function toDateTimeExpr(isoDate, endOfDay) {
function toQueryStringLiteral(value) {
return String(value ?? "").replace(/"/g, '""');
}
function buildOrganizationPresentationCondition(filters, fieldPath) {
const organization = typeof filters.organization === "string" && filters.organization.trim().length > 0
? filters.organization.trim()
: "";
if (!organization) {
return null;
}
return `ПРЕДСТАВЛЕНИЕ(${fieldPath}) = "${toQueryStringLiteral(organization)}"`;
}
function buildWhereClause(filters, fieldPath, extraConditions = []) {
const periodFromExpr = typeof filters.period_from === "string" && filters.period_from.trim().length > 0
? toDateTimeExpr(filters.period_from, false)
@@ -1087,10 +1114,40 @@ function buildInventoryMovementQuery(filters, resolvedLimit, side) {
: side === "kt"
? creditPredicate
: `(${debitPredicate} ИЛИ ${creditPredicate})`;
const organizationCondition = buildOrganizationPresentationCondition(filters, "Движения.Организация");
return INVENTORY_MOVEMENTS_QUERY_TEMPLATE
.replace("__LIMIT__", String(resolvedLimit))
.replace("__WHERE_CLAUSE__", buildWhereClause(filters, "Движения.Период", [inventoryCondition, organizationCondition].filter((item) => Boolean(item))))
.replace("__WHERE_CLAUSE__", buildWhereClause(filters, "Движения.Период", [inventoryCondition]))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
}
function buildInventoryItemReferenceCondition(filters, fieldPaths) {
const item = typeof filters.item === "string" ? filters.item.trim() : "";
if (!item) {
return null;
}
const escapedItem = toQueryStringLiteral(item);
const referenceSubquery = `(ВЫБРАТЬ Номенклатура.Ссылка ИЗ Справочник.Номенклатура КАК Номенклатура ` +
`ГДЕ Номенклатура.Наименование = "${escapedItem}")`;
const clauses = fieldPaths
.map((fieldPath) => String(fieldPath ?? "").trim())
.filter((fieldPath) => fieldPath.length > 0)
.map((fieldPath) => `${fieldPath} В ${referenceSubquery}`);
if (clauses.length === 0) {
return null;
}
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
}
function buildInventorySaleDocumentQuery(filters, resolvedLimit) {
const itemCondition = buildInventoryItemReferenceCondition(filters, ["Товары.Номенклатура"]);
return INVENTORY_SALE_DOCUMENTS_QUERY_TEMPLATE
.replace("__LIMIT__", String(resolvedLimit))
.replace("__WHERE_CLAUSE__", buildWhereClause(filters, "Товары.Ссылка.Дата", ['Товары.Ссылка.Проведен = ИСТИНА', itemCondition].filter((item) => Boolean(item))))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
}
function buildInventoryPurchaseDocumentQuery(filters, resolvedLimit) {
const itemCondition = buildInventoryItemReferenceCondition(filters, ["Товары.Номенклатура"]);
return INVENTORY_PURCHASE_DOCUMENTS_QUERY_TEMPLATE
.replace("__LIMIT__", String(resolvedLimit))
.replace("__WHERE_CLAUSE__", buildWhereClause(filters, "Товары.Ссылка.Дата", ['Товары.Ссылка.Проведен = ИСТИНА', itemCondition].filter((item) => Boolean(item))))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
}
function shouldBoostLimitForAllTimeCounterparty(filters) {
@@ -1269,13 +1326,13 @@ function buildAddressRecipePlan(recipe, filters) {
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
})()
: recipe.query_template === "inventory_purchase_provenance_profile"
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
? buildInventoryPurchaseDocumentQuery(filters, resolvedLimit)
: recipe.query_template === "inventory_purchase_documents_profile"
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
? buildInventoryPurchaseDocumentQuery(filters, resolvedLimit)
: recipe.query_template === "inventory_supplier_stock_overlap_profile"
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
: recipe.query_template === "inventory_sale_trace_profile"
? buildInventoryMovementQuery(filters, resolvedLimit, "kt")
? buildInventorySaleDocumentQuery(filters, resolvedLimit)
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
? buildInventoryMovementQuery(filters, resolvedLimit, "either")
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
@@ -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) {
@@ -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,
+49 -3
View File
@@ -57,6 +57,7 @@ const addressQueryService_1 = __importStar(require("./addressQueryService"));
const addressQueryClassifier_1 = __importStar(require("./addressQueryClassifier"));
const addressIntentResolver_1 = __importStar(require("./addressIntentResolver"));
const addressFilterExtractor_1 = __importStar(require("./addressFilterExtractor"));
const decomposeStage_1 = __importStar(require("./address_runtime/decomposeStage"));
const predecomposeContract_1 = __importStar(require("./address_runtime/predecomposeContract"));
const openaiResponsesClient_1 = __importStar(require("./openaiResponsesClient"));
const addressMcpClient_1 = __importStar(require("./addressMcpClient"));
@@ -2767,8 +2768,15 @@ function hasShortInventoryObjectFollowupSignal(userMessage) {
if (minTokens > 8) {
return false;
}
const hasDirectSaleFollowupCue = (sample) => /(?:кому|каму|куда)(?:\s+\S+){0,4}\s+(?:продали|продано|продан(?:о|а|ы)?|реализовали|реализован(?:о|а|ы)?)|(?:продали|продано|реализовали|реализован(?:о|а|ы)?)(?:\s+\S+){0,4}\s+(?:кому|каму|куда)|(?:^|\s)(?:продано|продали|реализовано|реализовали)(?=$|[\s,.;:!?])/iu.test(sample);
return samples.some((sample) => /^(?:кто|когда|документы|сумма|поставщик|покупатель)(?:\?)?$/iu.test(sample) ||
/^(?:когда\s+(?:примерно\s+)?купили(?:\s+ее)?|каким\s+документом|покажи\s+документы|по\s+каким\s+документам|все\s+закупки|все\s+поступления|кому\s+(?:мы\s+)?продали|кто\s+купил|цепочка|путь\s+товара)(?:\?)?$/iu.test(sample));
hasDirectSaleFollowupCue(sample) ||
(0, decomposeStage_1.hasInventorySupplierFollowupCue)(sample) ||
(0, decomposeStage_1.hasInventoryPurchaseDocumentsFollowupCue)(sample) ||
(0, decomposeStage_1.hasInventoryPurchaseDateFollowupCue)(sample) ||
(0, decomposeStage_1.hasBareInventoryPurchaseDateFollowupCue)(sample) ||
(0, decomposeStage_1.hasInventorySaleFollowupCue)(sample) ||
(0, decomposeStage_1.hasInventoryPurchaseToSaleChainFollowupCue)(sample));
}
function resolveDebtRoleSwapFollowupIntent(userMessage, previousIntent) {
const normalized = compactWhitespace(String(userMessage ?? "").toLowerCase());
@@ -3408,6 +3416,13 @@ function resolveRequiredAnchorTypeForIntent(intent) {
if (intent === "list_documents_by_contract" || intent === "bank_operations_by_contract") {
return "contract";
}
if (intent === "inventory_purchase_provenance_for_item" ||
intent === "inventory_purchase_documents_for_item" ||
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date") {
return "item";
}
return null;
}
function evaluateAddressAnchorQuality(message) {
@@ -3425,7 +3440,9 @@ function evaluateAddressAnchorQuality(message) {
const extracted = (0, addressFilterExtractor_1.extractAddressFilters)(String(message ?? ""), intent);
const anchorValue = anchorType === "counterparty"
? toNonEmptyString(extracted?.extracted_filters?.counterparty)
: toNonEmptyString(extracted?.extracted_filters?.contract);
: anchorType === "contract"
? toNonEmptyString(extracted?.extracted_filters?.contract)
: toNonEmptyString(extracted?.extracted_filters?.item);
if (!anchorValue) {
return {
intent,
@@ -3436,7 +3453,9 @@ function evaluateAddressAnchorQuality(message) {
}
const lowQuality = anchorType === "counterparty"
? isLowQualityPredecomposeCounterpartyAnchor(anchorValue)
: isLowQualityPredecomposeContractAnchor(anchorValue);
: anchorType === "contract"
? isLowQualityPredecomposeContractAnchor(anchorValue)
: (0, addressFilterExtractor_1.isLowQualityInventoryItemAnchorValue)(anchorValue);
return {
intent,
anchorType,
@@ -3660,6 +3679,14 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
const sourceAnchorQuality = evaluateAddressAnchorQuality(repairedSourceMessage || userMessage);
const candidateAnchorQuality = evaluateAddressAnchorQuality(candidate);
const sameIntentForAnchorSafety = sourceAnchorQuality.intent !== "unknown" && sourceAnchorQuality.intent === candidateAnchorQuality.intent;
const sourceSelectedObjectItemAnchorValue = toNonEmptyString((0, addressFilterExtractor_1.extractSelectedObjectQuotedValue)(userMessage)) ??
toNonEmptyString((0, addressFilterExtractor_1.extractSelectedObjectQuotedValue)(repairedSourceMessage || userMessage));
const candidateSemanticItemAnchorValue = (((sameIntentForAnchorSafety &&
sourceAnchorQuality.anchorType === "item") ||
Boolean(sourceSelectedObjectItemAnchorValue)) &&
candidateMeta?.semanticHints?.scope_target_kind === "item"
? toNonEmptyString(candidateMeta.semanticHints.scope_target_text)
: null);
const counterpartyAnchorSubstitutedByCandidate = sameIntentForAnchorSafety &&
sourceAnchorQuality.anchorType === "counterparty" &&
sourceAnchorQuality.quality >= 2 &&
@@ -3684,6 +3711,25 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
semanticHints: candidateMeta?.semanticHints ?? null
}, userMessage);
}
const itemSemanticAnchorDegradedByCandidate = (sameIntentForAnchorSafety ||
Boolean(sourceSelectedObjectItemAnchorValue)) &&
Boolean(sourceSelectedObjectItemAnchorValue ?? sourceAnchorQuality.anchorValue) &&
Boolean(candidateSemanticItemAnchorValue) &&
(0, addressFilterExtractor_1.isInventoryItemAnchorDegradation)(sourceSelectedObjectItemAnchorValue ?? sourceAnchorQuality.anchorValue ?? "", candidateSemanticItemAnchorValue ?? "");
if (itemSemanticAnchorDegradedByCandidate) {
return attachAddressPredecomposeContract({
...baseMeta,
attempted: true,
applied: false,
traceId: normalized?.trace_id ?? null,
llmCanonicalCandidateDetected: true,
effectiveMessage: userMessage,
reason: "normalized_fragment_rejected_anchor_degradation",
fallbackRuleHit: null,
sanitizedUserMessage,
semanticHints: candidateMeta?.semanticHints ?? null
}, userMessage);
}
const anchorDegradedByCandidate = sameIntentForAnchorSafety &&
sourceAnchorQuality.anchorType &&
sourceAnchorQuality.quality >= 2 &&
@@ -0,0 +1,33 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.hasInventoryPurchaseStem = hasInventoryPurchaseStem;
exports.hasInventorySupplierCue = hasInventorySupplierCue;
exports.hasInventorySaleCue = hasInventorySaleCue;
function toText(value) {
return String(value ?? "");
}
function hasInventoryPurchaseStem(text) {
return /купл[а-яёa-z0-9_-]*/iu.test(toText(text));
}
function hasInventorySupplierCue(text) {
const value = toText(text);
if (/(?:кто\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(value)) {
return true;
}
return hasInventoryPurchaseStem(value) && /(?:у\s+кого|от\s+кого|где)/iu.test(value);
}
function hasInventorySaleCue(text) {
const value = toText(text);
if (/(?:buyer|покупател)/iu.test(value)) {
return true;
}
if (/(?:куда\s+ушла\s+позиция|куда\s+ушел\s+товар|кто\s+купил)/iu.test(value)) {
return true;
}
const hasDirectionCue = /(?:кому|каму|куда)/iu.test(value);
const hasSaleVerb = /(?:продал(?:и|а|о|ы)?|продан(?:а|о|ы)?|продано|реализовал(?:и|а|о|ы)?|реализован(?:а|о|ы)?|реализовано)/iu.test(value);
if (hasDirectionCue && hasSaleVerb) {
return true;
}
return /(?:^|[\s,.;:!?])(продано|продали|продан(?:а|о|ы)?|реализовано|реализовали|реализован(?:а|о|ы)?)(?=$|[\s,.;:!?])/iu.test(value);
}