АРЧ АП11 - Оркестрация: дожать агентный прогон по selected-object continuity и сохранить accepted

This commit is contained in:
2026-04-17 18:24:02 +03:00
parent c2bafcff9b
commit 7f42d8ab50
22 changed files with 1775 additions and 816 deletions
@@ -944,6 +944,12 @@ function isLowQualityInventoryItemAnchorValue(rawValue) {
.map((token) => token.trim())
.filter(Boolean)
.filter((token) => !lowQualityTokens.has(token));
if (/^(?:(?:\u043a\u043e\u0433\u0434\u0430|when)\s+)?(?:(?:\u0431\u044b\u043b(?:\u0430|\u0438|\u043e)?|was|were)\s+)?(?:\u0437\u0430\u043a\u0443\u043f[\p{L}\p{N}_-]*|purchase\s+date|purchase)$/iu.test(value)) {
return true;
}
if (/^(?:(?:\u043a\u0442\u043e|who)\s+)?(?:(?:\u043d\u0430\u043c\s+)?(?:\u043f\u043e\u0441\u0442\u0430\u0432[\p{L}\p{N}_-]*|supplier)|supplier)$/iu.test(value)) {
return true;
}
return meaningfulTokens.length === 0;
}
function normalizeInventoryItemAnchorForComparison(rawValue) {
@@ -739,6 +739,10 @@ function hasCounterpartyActivityLifecycleSignal(text) {
return hasCounterpartyLexeme && hasActivityLexeme && (hasTimeWindowLexeme || hasListVerb);
}
function hasCounterpartyShipmentItemFlowSignal(text) {
const hasSelectedObjectInventoryCue = /(?:по\s+этой\s+позици(?:и|я|ю)|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|selected\s+object|по\s+выбранному\s+объекту)/iu.test(text);
if (hasSelectedObjectInventoryCue) {
return false;
}
const hasNamedTailAfterShipmentCue = /(?:отгруж(?:ал|али|ено)|постав(?:лял|ляли|ил|или)|привоз(?:ил|или)|продал)\s+[a-zа-яё][a-zа-яё0-9._-]{2,}/iu.test(text);
const hasPartySignal = hasPartyAnchorMention(text) ||
hasLooseByAnchorMention(text) ||
@@ -1397,8 +1401,9 @@ function hasInventoryProvenanceSignalV2(text) {
return hasItemCue && hasSupplierCue && hasPurchaseCue;
}
function hasInventoryPurchaseDateSignal(text) {
const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text);
const hasPurchaseDateCue = /(?:когда\s+(?:примерно\s+)?(?:мы\s+)?купили|когда\s+был\s+куплен|когда\s+куплен|дата\s+закупк|purchase\s+date)/iu.test(text);
const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text) || hasSelectedObjectInventoryCue(text);
const hasPurchaseDateCue = /(?:когда\s+(?:примерно\s+)?(?:мы\s+)?купили|когда\s+был\s+куплен|когда\s+куплен|дата\s+закупк|purchase\s+date)/iu.test(text) ||
/(?:когда\s+был(?:а|и|о)?\s+закупк\w*|когда\s+закупк\w*)/iu.test(text);
return hasItemCue && hasPurchaseDateCue;
}
function hasInventoryPurchaseDocumentsSignalV2(text) {
@@ -5,6 +5,7 @@ exports.composeFactualReply = composeFactualReply;
exports.inferReplyType = inferReplyType;
const assistantOrganizationMatcher_1 = require("../assistantOrganizationMatcher");
const replyPackaging_1 = require("./replyPackaging");
const inventoryReplyBuilders_1 = require("./inventoryReplyBuilders");
function uniqueStrings(values) {
return Array.from(new Set(values
.map((item) => item.trim())
@@ -2114,6 +2115,26 @@ function composeFactualReply(intent, rows, options = {}) {
}
function composeFactualReplyBody(intent, rows, options = {}) {
const joinLines = replyPackaging_1.joinComposeReplyLines;
const inventoryReply = (0, inventoryReplyBuilders_1.composeInventoryReply)(intent, rows, options, {
resolvePayablesAsOfDate,
buildInventoryOnHandAggregate,
uniqueStrings,
formatDateRu,
formatNumberWithDots,
formatMoneyRub,
isInventoryPurchaseMovement,
summarizeInventoryTraceRows,
formatInventoryTraceRows,
hasInventoryPurchaseDateActionFocus,
inventoryTraceDateLabel,
extractInventoryCounterpartyCandidates,
buildInventoryAgingByItemAggregate,
formatInventoryAgingRows,
isInventorySaleMovement
});
if (inventoryReply) {
return inventoryReply;
}
if (intent === "document_type_and_account_section_profile") {
const rowsByMarker = new Map();
for (const row of rows) {
@@ -3308,370 +3329,6 @@ function composeFactualReplyBody(intent, rows, options = {}) {
text: lines.join("\n")
};
}
if (intent === "inventory_on_hand_as_of_date") {
const asOfDate = resolvePayablesAsOfDate(options);
const positions = buildInventoryOnHandAggregate(rows, asOfDate);
const uniqueItems = uniqueStrings(positions.map((item) => item.item));
const uniqueWarehouses = uniqueStrings(positions.map((item) => String(item.warehouse ?? "").trim()).filter((item) => item.length > 0));
const totalQuantity = positions.reduce((sum, item) => sum + item.quantity, 0);
const totalAmount = positions.reduce((sum, item) => sum + item.amount, 0);
const directAnswerLine = positions.length > 0
? `На ${formatDateRu(asOfDate)} на складе подтверждено ${formatNumberWithDots(positions.length)} позиций с остатком на ${formatMoneyRub(totalAmount)}.`
: `На ${formatDateRu(asOfDate)} подтвержденных товарных остатков по счету 41.01 не найдено.`;
const lines = [directAnswerLine];
if (positions.length > 0) {
lines.push("", "Позиции:");
lines.push(...positions.slice(0, 20).map((item, index) => {
const warehouseLabel = item.warehouse ?? "склад не определен";
const organizationLabel = item.organization ? ` | организация: ${item.organization}` : "";
const periodLabel = item.lastPeriod ? ` | дата строки: ${item.lastPeriod}` : "";
const refsLabel = item.sourceRefs.length > 0 ? ` | source refs: ${item.sourceRefs.slice(0, 2).join("; ")}` : "";
return `${index + 1}. ${item.item} | склад: ${warehouseLabel} | количество: ${formatNumberWithDots(item.quantity, 3)} | стоимость: ${formatMoneyRub(item.amount)}${organizationLabel}${periodLabel}${refsLabel}`;
}));
}
else {
lines.push("", "Позиции:", "- На дату среза товары с ненулевым остатком по счету 41.01 не найдены.");
}
lines.push("", "Подтверждение:", `- Дата среза: ${formatDateRu(asOfDate)}.`, "- Контур: остатки по счету 41.01 «Товары на складах».", `- Уникальных товаров: ${formatNumberWithDots(uniqueItems.length)}.`, `- Уникальных складов: ${formatNumberWithDots(uniqueWarehouses.length)}.`, `- Суммарное количество: ${formatNumberWithDots(totalQuantity, 3)}.`);
if (rows.length !== positions.length) {
lines.push(`- Строк в подтвержденной выборке: ${formatNumberWithDots(rows.length)}.`);
}
return {
responseType: positions.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
text: joinLines(lines),
semantics: {
result_mode: "confirmed_balance",
evidence_strength: positions.length > 0 ? "strong" : "medium",
balance_confirmed: true
}
};
}
if (intent === "inventory_purchase_documents_for_item") {
const asOfDate = resolvePayablesAsOfDate(options);
const purchaseRows = rows.filter((row) => isInventoryPurchaseMovement(row));
const summary = summarizeInventoryTraceRows(purchaseRows);
const itemLabel = summary.item ?? "товар не определен";
const directAnswerLine = purchaseRows.length <= 0
? `По позиции ${itemLabel} подтвержденные документы закупки в доступном контуре не найдены.`
: `По позиции ${itemLabel} найдено ${formatNumberWithDots(summary.documents.length)} подтвержденных документов закупки до ${formatDateRu(asOfDate)}.`;
const lines = [directAnswerLine];
lines.push("", "Подтверждение:");
lines.push(`- Дата верхней границы: ${formatDateRu(asOfDate)}.`);
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, 4).join("; ")}.`);
}
lines.push("", "Документы:");
if (purchaseRows.length > 0) {
lines.push(...formatInventoryTraceRows(purchaseRows, 12));
}
else {
lines.push("- По выбранному товару не найдено проводок поступления на 41.01 в доступном контуре.");
}
return {
responseType: purchaseRows.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
text: joinLines(lines),
semantics: {
result_mode: "confirmed_balance",
evidence_strength: purchaseRows.length > 0 ? "strong" : "medium",
balance_confirmed: purchaseRows.length > 0
}
};
}
if (intent === "inventory_purchase_provenance_for_item") {
const asOfDate = resolvePayablesAsOfDate(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);
const lastPurchaseDate = inventoryTraceDateLabel(summary.lastPeriod);
const directAnswerLine = purchaseRows.length <= 0 || !summary.firstPeriod
? `По позиции ${itemLabel} подтвержденная дата закупки в доступном контуре не найдена.`
: summary.firstPeriod === summary.lastPeriod
? `Позиция ${itemLabel} куплена ${firstPurchaseDate}.`
: boundedAsOfLabel
? `По позиции ${itemLabel} до ${boundedAsOfLabel} подтвержден диапазон закупок: с ${firstPurchaseDate} по ${lastPurchaseDate}.`
: `По позиции ${itemLabel} подтвержденный диапазон закупок: с ${firstPurchaseDate} по ${lastPurchaseDate}.`;
const lines = [directAnswerLine];
if (purchaseRows.length > 0) {
lines.push("", "Подтверждение:");
lines.push(`- Первая подтвержденная дата закупки: ${firstPurchaseDate}.`);
if (summary.firstPeriod !== summary.lastPeriod) {
lines.push(`- Последняя подтвержденная дата закупки: ${lastPurchaseDate}.`);
}
if (summary.counterparties.length === 1) {
lines.push(`- Поставщик в доступном закупочном следе: ${summary.counterparties[0]}.`);
}
else if (summary.counterparties.length > 1) {
lines.push(`- В доступном закупочном следе найдено несколько поставщиков: ${summary.counterparties.slice(0, 4).join("; ")}.`);
}
if (summary.documents.length > 0) {
lines.push(`- Первый подтверждающий документ: ${summary.documents[0]}.`);
}
if (boundedAsOfLabel) {
lines.push(`- Для ответа учтены закупочные документы не позже ${boundedAsOfLabel}.`);
}
if (summary.counterparties.length > 1) {
lines.push(`- Без партионного учета нельзя однозначно связать остаток${boundedAsOfLabel ? ` на ${boundedAsOfLabel}` : ""} с одним конкретным поступлением.`);
}
}
return {
responseType: "FACTUAL_SUMMARY",
text: joinLines(lines),
semantics: {
result_mode: "confirmed_balance",
evidence_strength: purchaseRows.length > 0 ? "strong" : "medium",
balance_confirmed: purchaseRows.length > 0
}
};
}
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, "", "Подтверждение:"];
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 (boundedAsOfLabel) {
lines.push(`- Для ответа проверены закупочные документы не позже ${boundedAsOfLabel}.`);
}
if (summary.documents.length > 0) {
lines.push("", "Опорные документы:", ...formatInventoryTraceRows(purchaseRows, 8));
}
return {
responseType: "FACTUAL_SUMMARY",
text: joinLines(lines),
semantics: {
result_mode: "confirmed_balance",
evidence_strength: purchaseRows.length > 0 ? (summary.counterparties.length === 1 ? "strong" : "medium") : "medium",
balance_confirmed: purchaseRows.length > 0
}
};
}
if (intent === "inventory_supplier_stock_overlap_as_of_date") {
const asOfDate = resolvePayablesAsOfDate(options);
const purchaseRows = rows.filter((row) => isInventoryPurchaseMovement(row));
const summary = summarizeInventoryTraceRows(purchaseRows);
const unresolvedRows = purchaseRows.filter((row) => extractInventoryCounterpartyCandidates(row).length === 0);
const warehouseLabel = summary.warehouses[0] ?? "не указанного склада";
const directAnswerLine = summary.counterparties.length === 1
? `По складскому остатку ${warehouseLabel} выявлен поставщик: ${summary.counterparties[0]}.`
: summary.counterparties.length > 1
? `По складскому остатку ${warehouseLabel} найдено несколько поставщиков: ${summary.counterparties.slice(0, 6).join("; ")}.`
: `По складскому остатку ${warehouseLabel} поставщик в текущем exact-контуре не материализован.`;
const lines = [
directAnswerLine,
`Собран exact-срез supplier overlap для складского остатка до ${formatDateRu(asOfDate)}.`,
"",
"Блок 1. Статус результата",
`- Контур: подтвержденные закупочные движения на 41.01, связанные со складом ${warehouseLabel}.`,
"- Важно: без партионности этот контур показывает документально наблюдаемые supplier candidates, но не подменяет собой лот-level атрибуцию текущего остатка.",
"",
"Блок 2. Сводка",
`- Первая найденная дата закупочного движения: ${inventoryTraceDateLabel(summary.firstPeriod)}.`,
`- Последняя найденная дата закупочного движения: ${inventoryTraceDateLabel(summary.lastPeriod)}.`,
`- Закупочных документов в выборке: ${formatNumberWithDots(summary.documents.length)}.`,
`- Закупочных операций в выборке: ${formatNumberWithDots(purchaseRows.length)}.`
];
if (summary.counterparties.length > 0) {
lines.push(`- Найденные поставщики в наблюдаемом контуре: ${summary.counterparties.slice(0, 6).join("; ")}.`);
}
else if (purchaseRows.length > 0) {
lines.push("- Закупочные движения найдены, но поставщик не материализован отдельным полем в текущем exact-контуре.");
}
else {
lines.push("- В доступном exact-контуре не найдено закупочных движений по 41.01 для выбранного складского среза.");
}
if (unresolvedRows.length > 0) {
lines.push(`- Операций без явно материализованного поставщика: ${formatNumberWithDots(unresolvedRows.length)}.`);
}
if (purchaseRows.length > 0) {
lines.push("", "Блок 3. Опорные документы", ...formatInventoryTraceRows(purchaseRows, 10));
}
return {
responseType: "FACTUAL_SUMMARY",
text: joinLines(lines),
semantics: {
result_mode: "confirmed_balance",
evidence_strength: purchaseRows.length > 0 ? (summary.counterparties.length > 0 ? "strong" : "medium") : "medium",
balance_confirmed: purchaseRows.length > 0
}
};
}
if (intent === "inventory_aging_by_purchase_date") {
const asOfDate = resolvePayablesAsOfDate(options);
const purchaseRows = rows.filter((row) => isInventoryPurchaseMovement(row));
const summary = summarizeInventoryTraceRows(purchaseRows);
const agingItems = buildInventoryAgingByItemAggregate(purchaseRows, asOfDate);
const oldestPurchaseDate = agingItems[0]?.firstPurchasePeriod ?? summary.firstPeriod;
const oldestPurchaseAgeDays = agingItems[0]?.ageDays ?? null;
const oldestAnswerPreview = agingItems
.slice(0, 3)
.map((item) => `${item.item} (${inventoryTraceDateLabel(item.firstPurchasePeriod)})`)
.join("; ");
const directAnswerLine = agingItems.length > 0
? `К старым закупкам на ${formatDateRu(asOfDate)} в первую очередь относятся позиции с самой ранней первой закупкой: ${oldestAnswerPreview}.`
: `По доступному закупочному следу на ${formatDateRu(asOfDate)} позиции старых закупок не материализованы.`;
const lines = [
directAnswerLine,
`Собран exact-срез старых закупок для складского остатка на ${formatDateRu(asOfDate)}.`,
"",
"Блок 1. Статус результата",
"- Контур: показан item-level список товарных позиций с самым ранним документально наблюдаемым закупочным следом на 41.01.",
"- Порядок: позиции отсортированы от самой старой первой закупки к более новым.",
"- Важно: без партионности этот контур не доказывает возраст конкретного лота, а показывает документально наблюдаемый возраст закупочного следа по товарной позиции.",
"",
"Блок 2. Сводка",
`- Дата среза: ${formatDateRu(asOfDate)}.`,
`- Самая ранняя первая закупка среди позиций: ${inventoryTraceDateLabel(oldestPurchaseDate)}.`,
`- Самая поздняя найденная закупка в наблюдаемом следе: ${inventoryTraceDateLabel(summary.lastPeriod)}.`,
`- Позиции в aging-срезе: ${formatNumberWithDots(agingItems.length)}.`,
`- Закупочных документов в наблюдаемом следе: ${formatNumberWithDots(summary.documents.length)}.`,
`- Закупочных операций в наблюдаемом следе: ${formatNumberWithDots(purchaseRows.length)}.`
];
if (oldestPurchaseAgeDays !== null) {
lines.push(`- Между самой ранней первой закупкой и датой среза прошло ${formatNumberWithDots(oldestPurchaseAgeDays)} дн.`);
}
if (summary.counterparties.length > 0) {
lines.push(`- Поставщики, встречающиеся в наблюдаемом закупочном следе: ${summary.counterparties.slice(0, 4).join("; ")}.`);
}
if (agingItems.length > 0) {
lines.push("", "Блок 3. Позиции от самых старых закупок", ...formatInventoryAgingRows(agingItems, asOfDate, 12));
}
else {
lines.push("", "Блок 3. Позиции от самых старых закупок", "- В доступном exact-контуре не найдено закупочных движений по 41.01 для выбранного среза.");
}
return {
responseType: "FACTUAL_SUMMARY",
text: joinLines(lines),
semantics: {
result_mode: "confirmed_balance",
evidence_strength: agingItems.length > 0 ? "strong" : "medium",
balance_confirmed: agingItems.length > 0
}
};
}
if (intent === "inventory_sale_trace_for_item") {
const asOfDate = resolvePayablesAsOfDate(options);
const saleRows = rows.filter((row) => isInventorySaleMovement(row));
const requestedItemHint = String(options.itemHint ?? "").trim();
const provisionalExcludedTokens = requestedItemHint ? [requestedItemHint] : [];
const summary = summarizeInventoryTraceRows(saleRows, provisionalExcludedTokens);
const itemLabel = requestedItemHint || (summary.item ?? "товар не определен");
const excludedCounterpartyTokens = [itemLabel];
const directAnswerLine = summary.counterparties.length === 1
? `По товару ${itemLabel} покупатель определен: ${summary.counterparties[0]}.`
: summary.counterparties.length > 1
? `По товару ${itemLabel} найдено несколько покупателей: ${summary.counterparties.slice(0, 4).join("; ")}.`
: `По товару ${itemLabel} покупатель в текущем exact-контуре не материализован.`;
const lines = [directAnswerLine, "", "Подтверждение:"];
lines.push(`- Первая найденная дата выбытия: ${inventoryTraceDateLabel(summary.firstPeriod)}.`);
lines.push(`- Последняя найденная дата выбытия: ${inventoryTraceDateLabel(summary.lastPeriod)}.`);
lines.push(`- Документов выбытия: ${formatNumberWithDots(summary.documents.length)}.`);
lines.push(`- Операций выбытия: ${formatNumberWithDots(saleRows.length)}.`);
if (summary.counterparties.length === 1) {
lines.push(`- По доступным движениям товар отгружался покупателю: ${summary.counterparties[0]}.`);
}
else if (summary.counterparties.length > 1) {
lines.push(`- По доступным движениям найдено несколько покупателей: ${summary.counterparties.slice(0, 4).join("; ")}.`);
}
else if (saleRows.length > 0) {
lines.push("- Документы выбытия найдены, но покупатель не материализован отдельным полем в текущем exact-контуре.");
}
lines.push("", "Документы выбытия:");
if (saleRows.length > 0) {
lines.push(...formatInventoryTraceRows(saleRows, 12, excludedCounterpartyTokens));
}
else {
lines.push("- По выбранному товару не найдено проводок выбытия со счета 41.01 в доступном контуре.");
}
return {
responseType: saleRows.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
text: joinLines(lines),
semantics: {
result_mode: "confirmed_balance",
evidence_strength: saleRows.length > 0 ? (summary.counterparties.length > 0 ? "strong" : "medium") : "medium",
balance_confirmed: saleRows.length > 0
}
};
}
if (intent === "inventory_purchase_to_sale_chain") {
const asOfDate = resolvePayablesAsOfDate(options);
const purchaseRows = rows.filter((row) => isInventoryPurchaseMovement(row));
const saleRows = rows.filter((row) => isInventorySaleMovement(row));
const purchaseSummary = summarizeInventoryTraceRows(purchaseRows);
const saleSummary = summarizeInventoryTraceRows(saleRows);
const itemLabel = purchaseSummary.item ?? saleSummary.item ?? "товар не определен";
const directAnswerLine = purchaseSummary.counterparties.length === 1 && saleSummary.counterparties.length === 1
? `По товару ${itemLabel} цепочка поставки и продажи связана с поставщиком ${purchaseSummary.counterparties[0]} и покупателем ${saleSummary.counterparties[0]}.`
: `По товару ${itemLabel} цепочка поставки и продажи подтверждена частично или разнообразно: детали идут следом.`;
const lines = [directAnswerLine, "", "Подтверждение:"];
lines.push(`- Закупочных движений на 41.01: ${formatNumberWithDots(purchaseRows.length)}.`);
lines.push(`- Движений выбытия со счета 41.01: ${formatNumberWithDots(saleRows.length)}.`);
if (purchaseRows.length > 0 && saleRows.length > 0) {
lines.push("- В текущем контуре найдены обе стороны цепочки: поступление и последующее выбытие.");
}
else if (purchaseRows.length > 0) {
lines.push("- Найдена только закупочная часть цепочки; выбытие в текущем exact-контуре не подтверждено.");
}
else if (saleRows.length > 0) {
lines.push("- Найдена только часть выбытия; закупочная часть цепочки в текущем exact-контуре не подтверждена.");
}
else {
lines.push("- Для выбранного товара не найдено движений по 41.01, из которых можно собрать цепочку.");
}
if (purchaseRows.length > 0) {
lines.push("", "Закупка:", `- Первая дата: ${inventoryTraceDateLabel(purchaseSummary.firstPeriod)}.`, `- Последняя дата: ${inventoryTraceDateLabel(purchaseSummary.lastPeriod)}.`, ...formatInventoryTraceRows(purchaseRows, 6));
}
if (saleRows.length > 0) {
lines.push("", "Продажа:", `- Первая дата: ${inventoryTraceDateLabel(saleSummary.firstPeriod)}.`, `- Последняя дата: ${inventoryTraceDateLabel(saleSummary.lastPeriod)}.`, ...formatInventoryTraceRows(saleRows, 6));
}
return {
responseType: purchaseRows.length > 0 || saleRows.length > 0 ? "FACTUAL_SUMMARY" : "FACTUAL_SUMMARY",
text: joinLines(lines),
semantics: {
result_mode: "confirmed_balance",
evidence_strength: purchaseRows.length > 0 && saleRows.length > 0 ? "strong" : purchaseRows.length > 0 || saleRows.length > 0 ? "medium" : "weak",
balance_confirmed: purchaseRows.length > 0 || saleRows.length > 0
}
};
}
if (intent === "open_contracts_confirmed_as_of_date") {
const asOfDate = resolvePayablesAsOfDate(options);
const confirmedContracts = buildOpenContractConfirmedBalanceAggregate(rows, asOfDate);
@@ -454,7 +454,7 @@ function shouldRestoreInventoryRootFrame(userMessage, intent, extractedFilters,
return hasTemporalPatch;
}
function hasSelectedObjectInventorySignal(text) {
return /(?:по\s+выбранному\s+объекту|for\s+selected\s+object)/iu.test(String(text ?? ""));
return /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(String(text ?? ""));
}
function hasSelectedObjectInlineSnapshotMetadata(text) {
return /(?:дата\s+строки|строка\s+от|количество\s*:|стоимость\s*:|склад\s*:|организация\s*:|\|\s*(?:склад|количество|стоимость|организация|дата\s+строки)\s*:)/iu.test(String(text ?? ""));
@@ -489,7 +489,9 @@ function hasInventoryProfitabilityFollowupCue(text) {
}
function hasInventoryPurchaseDateFollowupCue(text) {
const value = String(text ?? "");
return /(?:когда\s+(?:примерно\s+)?(?:мы\s+)?купили|когда\s+был\s+куплен|когда\s+куплен|когда\s+это\s+купили|когда\s+эту\s+позицию\s+купили|когда\s+ее\s+купили|дата\s+закупки|purchase\s+date)/iu.test(value) || (/когда/iu.test(value) && (0, inventoryLifecycleCueHelpers_1.hasInventoryPurchaseStem)(value));
return (/(?:когда\s+(?:примерно\s+)?(?:мы\s+)?купили|когда\s+был\s+куплен|когда\s+куплен|когда\s+была\s+закупк(?:а|и)|когда\s+это\s+купили|когда\s+эту\s+позицию\s+купили|когда\s+ее\s+купили|дата\s+закупки|purchase\s+date)/iu.test(value) ||
/(?:по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)[\s\S]{0,40}(?:когда|дата\s+закупки|закупк))/iu.test(value) ||
(/когда/iu.test(value) && (0, inventoryLifecycleCueHelpers_1.hasInventoryPurchaseStem)(value)));
}
function hasBareInventoryPurchaseDateFollowupCue(text) {
const normalized = String(text ?? "").trim().toLowerCase();
@@ -509,7 +511,7 @@ function hasAddressFollowupContextSignal(text) {
if (!normalized) {
return false;
}
if (/(?:по\s+выбранному\s+объекту|for\s+selected\s+object)/iu.test(normalized)) {
if (/(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(normalized)) {
return true;
}
if (hasAllTimeHint(normalized)) {
@@ -1107,6 +1109,10 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
}
if (inventorySelectedObjectFollowup && hasInventoryPurchaseDateFollowupCue(normalizedMessage)) {
if (detectedIntent.intent === "unknown" ||
detectedIntent.intent === "list_documents_by_counterparty" ||
detectedIntent.intent === "list_documents_by_contract" ||
detectedIntent.intent === "bank_operations_by_counterparty" ||
detectedIntent.intent === "bank_operations_by_contract" ||
detectedIntent.intent === "inventory_purchase_provenance_for_item" ||
detectedIntent.intent === sourceIntent ||
detectedIntent.intent === "inventory_on_hand_as_of_date") {
@@ -0,0 +1,317 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.composeInventoryReply = composeInventoryReply;
const replyContracts_1 = require("./replyContracts");
function composeInventoryReply(intent, rows, options, deps) {
if (intent === "inventory_on_hand_as_of_date") {
const asOfDate = deps.resolvePayablesAsOfDate(options);
const positions = deps.buildInventoryOnHandAggregate(rows, asOfDate);
const uniqueItems = deps.uniqueStrings(positions.map((item) => item.item));
const uniqueWarehouses = deps.uniqueStrings(positions.map((item) => String(item.warehouse ?? "").trim()).filter((item) => item.length > 0));
const totalQuantity = positions.reduce((sum, item) => sum + item.quantity, 0);
const totalAmount = positions.reduce((sum, item) => sum + item.amount, 0);
const directAnswerLine = positions.length > 0
? `На ${deps.formatDateRu(asOfDate)} на складе подтверждено ${deps.formatNumberWithDots(positions.length)} позиций с остатком на ${deps.formatMoneyRub(totalAmount)}.`
: `На ${deps.formatDateRu(asOfDate)} подтвержденных товарных остатков по счету 41.01 не найдено.`;
const lines = [directAnswerLine];
if (positions.length > 0) {
lines.push("", "Позиции:");
lines.push(...positions.slice(0, 20).map((item, index) => {
const warehouseLabel = item.warehouse ?? "склад не определен";
const organizationLabel = item.organization ? ` | организация: ${item.organization}` : "";
const periodLabel = item.lastPeriod ? ` | дата строки: ${item.lastPeriod}` : "";
const refsLabel = item.sourceRefs.length > 0 ? ` | source refs: ${item.sourceRefs.slice(0, 2).join("; ")}` : "";
return `${index + 1}. ${item.item} | склад: ${warehouseLabel} | количество: ${deps.formatNumberWithDots(item.quantity, 3)} | стоимость: ${deps.formatMoneyRub(item.amount)}${organizationLabel}${periodLabel}${refsLabel}`;
}));
}
else {
lines.push("", "Позиции:", "- На дату среза товары с ненулевым остатком по счету 41.01 не найдены.");
}
lines.push("", "Подтверждение:", `- Дата среза: ${deps.formatDateRu(asOfDate)}.`, "- Контур: остатки по счету 41.01 «Товары на складах».", `- Уникальных товаров: ${deps.formatNumberWithDots(uniqueItems.length)}.`, `- Уникальных складов: ${deps.formatNumberWithDots(uniqueWarehouses.length)}.`, `- Суммарное количество: ${deps.formatNumberWithDots(totalQuantity, 3)}.`);
if (rows.length !== positions.length) {
lines.push(`- Строк в подтвержденной выборке: ${deps.formatNumberWithDots(rows.length)}.`);
}
return positions.length > 0
? (0, replyContracts_1.buildFactualListReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)("strong"))
: (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)("medium"));
}
if (intent === "inventory_purchase_documents_for_item") {
const asOfDate = deps.resolvePayablesAsOfDate(options);
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
const itemLabel = summary.item ?? "товар не определен";
const directAnswerLine = purchaseRows.length <= 0
? `По позиции ${itemLabel} подтвержденные документы закупки в доступном контуре не найдены.`
: `По позиции ${itemLabel} найдено ${deps.formatNumberWithDots(summary.documents.length)} подтвержденных документов закупки до ${deps.formatDateRu(asOfDate)}.`;
const lines = [directAnswerLine];
lines.push("", "Подтверждение:");
lines.push(`- Дата верхней границы: ${deps.formatDateRu(asOfDate)}.`);
lines.push(`- Операций поступления в выборке: ${deps.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, 4).join("; ")}.`);
}
lines.push("", "Документы:");
if (purchaseRows.length > 0) {
lines.push(...deps.formatInventoryTraceRows(purchaseRows, 12));
}
else {
lines.push("- По выбранному товару не найдено проводок поступления на 41.01 в доступном контуре.");
}
return purchaseRows.length > 0
? (0, replyContracts_1.buildFactualListReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)("strong", true))
: (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)("medium", false));
}
if (intent === "inventory_purchase_provenance_for_item") {
const asOfDate = deps.resolvePayablesAsOfDate(options);
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
const itemLabel = summary.item ?? "товар не определен";
const boundedAsOfLabel = asOfDate ? deps.formatDateRu(asOfDate) : null;
const purchaseDateActionFocus = deps.hasInventoryPurchaseDateActionFocus(options.userMessage);
if (purchaseDateActionFocus) {
const firstPurchaseDate = deps.inventoryTraceDateLabel(summary.firstPeriod);
const lastPurchaseDate = deps.inventoryTraceDateLabel(summary.lastPeriod);
const directAnswerLine = purchaseRows.length <= 0 || !summary.firstPeriod
? `По позиции ${itemLabel} подтвержденная дата закупки в доступном контуре не найдена.`
: summary.firstPeriod === summary.lastPeriod
? `Позиция ${itemLabel} куплена ${firstPurchaseDate}.`
: boundedAsOfLabel
? `По позиции ${itemLabel} до ${boundedAsOfLabel} подтвержден диапазон закупок: с ${firstPurchaseDate} по ${lastPurchaseDate}.`
: `По позиции ${itemLabel} подтвержденный диапазон закупок: с ${firstPurchaseDate} по ${lastPurchaseDate}.`;
const lines = [directAnswerLine];
if (purchaseRows.length > 0) {
lines.push("", "Подтверждение:");
lines.push(`- Первая подтвержденная дата закупки: ${firstPurchaseDate}.`);
if (summary.firstPeriod !== summary.lastPeriod) {
lines.push(`- Последняя подтвержденная дата закупки: ${lastPurchaseDate}.`);
}
if (summary.counterparties.length === 1) {
lines.push(`- Поставщик в доступном закупочном следе: ${summary.counterparties[0]}.`);
}
else if (summary.counterparties.length > 1) {
lines.push(`- В доступном закупочном следе найдено несколько поставщиков: ${summary.counterparties.slice(0, 4).join("; ")}.`);
}
if (summary.documents.length > 0) {
lines.push(`- Первый подтверждающий документ: ${summary.documents[0]}.`);
}
if (boundedAsOfLabel) {
lines.push(`- Для ответа учтены закупочные документы не позже ${boundedAsOfLabel}.`);
}
if (summary.counterparties.length > 1) {
lines.push(`- Без партионного учета нельзя однозначно связать остаток${boundedAsOfLabel ? ` на ${boundedAsOfLabel}` : ""} с одним конкретным поступлением.`);
}
}
return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(purchaseRows.length > 0 ? "strong" : "medium", purchaseRows.length > 0));
}
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, "", "Подтверждение:"];
if (purchaseRows.length > 0) {
lines.push(`- Первая найденная дата закупки: ${deps.inventoryTraceDateLabel(summary.firstPeriod)}.`);
lines.push(`- Последняя найденная дата закупки: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`);
lines.push(`- Документов поступления: ${deps.formatNumberWithDots(summary.documents.length)}.`);
lines.push(`- Операций поступления: ${deps.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 (boundedAsOfLabel) {
lines.push(`- Для ответа проверены закупочные документы не позже ${boundedAsOfLabel}.`);
}
if (summary.documents.length > 0) {
lines.push("", "Опорные документы:", ...deps.formatInventoryTraceRows(purchaseRows, 8));
}
return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(purchaseRows.length > 0 ? (summary.counterparties.length === 1 ? "strong" : "medium") : "medium", purchaseRows.length > 0));
}
if (intent === "inventory_supplier_stock_overlap_as_of_date") {
const asOfDate = deps.resolvePayablesAsOfDate(options);
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
const unresolvedRows = purchaseRows.filter((row) => deps.extractInventoryCounterpartyCandidates(row).length === 0);
const warehouseLabel = summary.warehouses[0] ?? "не указанного склада";
const directAnswerLine = summary.counterparties.length === 1
? `По складскому остатку ${warehouseLabel} выявлен поставщик: ${summary.counterparties[0]}.`
: summary.counterparties.length > 1
? `По складскому остатку ${warehouseLabel} найдено несколько поставщиков: ${summary.counterparties.slice(0, 6).join("; ")}.`
: `По складскому остатку ${warehouseLabel} поставщик в текущем exact-контуре не материализован.`;
const lines = [
directAnswerLine,
`Собран exact-срез supplier overlap для складского остатка до ${deps.formatDateRu(asOfDate)}.`,
"",
"Блок 1. Статус результата",
`- Контур: подтвержденные закупочные движения на 41.01, связанные со складом ${warehouseLabel}.`,
"- Важно: без партионности этот контур не доказывает конкретного владельца каждой партии, а показывает наблюдаемый закупочный след текущего остатка.",
"",
"Блок 2. Подтверждение",
`- Дата среза: ${deps.formatDateRu(asOfDate)}.`,
`- Первая найденная дата закупочного движения: ${deps.inventoryTraceDateLabel(summary.firstPeriod)}.`,
`- Последняя найденная дата закупочного движения: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`,
`- Закупочных документов в выборке: ${deps.formatNumberWithDots(summary.documents.length)}.`,
`- Закупочных операций в выборке: ${deps.formatNumberWithDots(purchaseRows.length)}.`
];
if (summary.counterparties.length > 0) {
lines.push(`- Найденные поставщики в наблюдаемом контуре: ${summary.counterparties.slice(0, 6).join("; ")}.`);
}
else if (purchaseRows.length > 0) {
lines.push("- Закупочные движения найдены, но поставщик не материализован отдельным полем в текущем exact-контуре.");
}
else {
lines.push("- В доступном exact-контуре не найдено закупочных движений по 41.01 для выбранного складского среза.");
}
if (unresolvedRows.length > 0) {
lines.push(`- Операций без явно материализованного поставщика: ${deps.formatNumberWithDots(unresolvedRows.length)}.`);
}
if (purchaseRows.length > 0) {
lines.push("", "Блок 3. Опорные документы", ...deps.formatInventoryTraceRows(purchaseRows, 10));
}
return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(purchaseRows.length > 0 ? (summary.counterparties.length > 0 ? "strong" : "medium") : "medium", purchaseRows.length > 0));
}
if (intent === "inventory_aging_by_purchase_date") {
const asOfDate = deps.resolvePayablesAsOfDate(options);
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
const agingItems = deps.buildInventoryAgingByItemAggregate(purchaseRows, asOfDate);
const oldestPurchaseDate = agingItems[0]?.firstPurchasePeriod ?? summary.firstPeriod;
const oldestPurchaseAgeDays = agingItems[0]?.ageDays ?? null;
const oldestAnswerPreview = agingItems
.slice(0, 3)
.map((item) => `${item.item} (${deps.inventoryTraceDateLabel(item.firstPurchasePeriod)})`)
.join("; ");
const directAnswerLine = agingItems.length > 0
? `К старым закупкам на ${deps.formatDateRu(asOfDate)} в первую очередь относятся позиции с самой ранней первой закупкой: ${oldestAnswerPreview}.`
: `По доступному закупочному следу на ${deps.formatDateRu(asOfDate)} позиции старых закупок не материализованы.`;
const lines = [
directAnswerLine,
`Собран exact-срез старых закупок для складского остатка на ${deps.formatDateRu(asOfDate)}.`,
"",
"Блок 1. Статус результата",
"- Контур: показан item-level список товарных позиций с самым ранним документально наблюдаемым закупочным следом на 41.01.",
"- Порядок: позиции отсортированы от самой старой первой закупки к более новым.",
"- Важно: без партионности этот контур не доказывает возраст конкретного лота, а показывает документально наблюдаемый возраст закупочного следа по товарной позиции.",
"",
"Блок 2. Сводка",
`- Дата среза: ${deps.formatDateRu(asOfDate)}.`,
`- Самая ранняя первая закупка среди позиций: ${deps.inventoryTraceDateLabel(oldestPurchaseDate)}.`,
`- Самая поздняя найденная закупка в наблюдаемом следе: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`,
`- Позиции в aging-срезе: ${deps.formatNumberWithDots(agingItems.length)}.`,
`- Закупочных документов в наблюдаемом следе: ${deps.formatNumberWithDots(summary.documents.length)}.`,
`- Закупочных операций в наблюдаемом следе: ${deps.formatNumberWithDots(purchaseRows.length)}.`
];
if (oldestPurchaseAgeDays !== null) {
lines.push(`- Между самой ранней первой закупкой и датой среза прошло ${deps.formatNumberWithDots(oldestPurchaseAgeDays)} дн.`);
}
if (summary.counterparties.length > 0) {
lines.push(`- Поставщики, встречающиеся в наблюдаемом закупочном следе: ${summary.counterparties.slice(0, 4).join("; ")}.`);
}
if (agingItems.length > 0) {
lines.push("", "Блок 3. Позиции от самых старых закупок", ...deps.formatInventoryAgingRows(agingItems, asOfDate, 12));
}
else {
lines.push("", "Блок 3. Позиции от самых старых закупок", "- В доступном exact-контуре не найдено закупочных движений по 41.01 для выбранного среза.");
}
return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(agingItems.length > 0 ? "strong" : "medium", agingItems.length > 0));
}
if (intent === "inventory_sale_trace_for_item") {
const asOfDate = deps.resolvePayablesAsOfDate(options);
const saleRows = rows.filter((row) => deps.isInventorySaleMovement(row));
const requestedItemHint = String(options.itemHint ?? "").trim();
const provisionalExcludedTokens = requestedItemHint ? [requestedItemHint] : [];
const summary = deps.summarizeInventoryTraceRows(saleRows, provisionalExcludedTokens);
const itemLabel = requestedItemHint || (summary.item ?? "товар не определен");
const excludedCounterpartyTokens = [itemLabel];
const directAnswerLine = summary.counterparties.length === 1
? `По товару ${itemLabel} покупатель определен: ${summary.counterparties[0]}.`
: summary.counterparties.length > 1
? `По товару ${itemLabel} найдено несколько покупателей: ${summary.counterparties.slice(0, 4).join("; ")}.`
: `По товару ${itemLabel} покупатель в текущем exact-контуре не материализован.`;
const lines = [directAnswerLine, "", "Подтверждение:"];
lines.push(`- Первая найденная дата выбытия: ${deps.inventoryTraceDateLabel(summary.firstPeriod)}.`);
lines.push(`- Последняя найденная дата выбытия: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`);
lines.push(`- Документов выбытия: ${deps.formatNumberWithDots(summary.documents.length)}.`);
lines.push(`- Операций выбытия: ${deps.formatNumberWithDots(saleRows.length)}.`);
if (summary.counterparties.length === 1) {
lines.push(`- По доступным движениям товар отгружался покупателю: ${summary.counterparties[0]}.`);
}
else if (summary.counterparties.length > 1) {
lines.push(`- По доступным движениям найдено несколько покупателей: ${summary.counterparties.slice(0, 4).join("; ")}.`);
}
else if (saleRows.length > 0) {
lines.push("- Документы выбытия найдены, но покупатель не материализован отдельным полем в текущем exact-контуре.");
}
lines.push("", "Документы выбытия:");
if (saleRows.length > 0) {
lines.push(...deps.formatInventoryTraceRows(saleRows, 12, excludedCounterpartyTokens));
}
else {
lines.push("- По выбранному товару не найдено проводок выбытия со счета 41.01 в доступном контуре.");
}
return saleRows.length > 0
? (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_purchase_to_sale_chain") {
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
const saleRows = rows.filter((row) => deps.isInventorySaleMovement(row));
const purchaseSummary = deps.summarizeInventoryTraceRows(purchaseRows);
const saleSummary = deps.summarizeInventoryTraceRows(saleRows);
const itemLabel = purchaseSummary.item ?? saleSummary.item ?? "товар не определен";
const directAnswerLine = purchaseSummary.counterparties.length === 1 && saleSummary.counterparties.length === 1
? `По товару ${itemLabel} цепочка поставки и продажи связана с поставщиком ${purchaseSummary.counterparties[0]} и покупателем ${saleSummary.counterparties[0]}.`
: `По товару ${itemLabel} цепочка поставки и продажи подтверждена частично или разнообразно: детали идут следом.`;
const lines = [directAnswerLine, "", "Подтверждение:"];
lines.push(`- Закупочных движений на 41.01: ${deps.formatNumberWithDots(purchaseRows.length)}.`);
lines.push(`- Движений выбытия со счета 41.01: ${deps.formatNumberWithDots(saleRows.length)}.`);
if (purchaseRows.length > 0 && saleRows.length > 0) {
lines.push("- В текущем контуре найдены обе стороны цепочки: поступление и последующее выбытие.");
}
else if (purchaseRows.length > 0) {
lines.push("- Найдена только закупочная часть цепочки; выбытие в текущем exact-контуре не подтверждено.");
}
else if (saleRows.length > 0) {
lines.push("- Найдена только часть выбытия; закупочная часть цепочки в текущем exact-контуре не подтверждена.");
}
else {
lines.push("- Для выбранного товара не найдено движений по 41.01, из которых можно собрать цепочку.");
}
if (purchaseRows.length > 0) {
lines.push("", "Закупка:", `- Первая дата: ${deps.inventoryTraceDateLabel(purchaseSummary.firstPeriod)}.`, `- Последняя дата: ${deps.inventoryTraceDateLabel(purchaseSummary.lastPeriod)}.`, ...deps.formatInventoryTraceRows(purchaseRows, 6));
}
if (saleRows.length > 0) {
lines.push("", "Продажа:", `- Первая дата: ${deps.inventoryTraceDateLabel(saleSummary.firstPeriod)}.`, `- Последняя дата: ${deps.inventoryTraceDateLabel(saleSummary.lastPeriod)}.`, ...deps.formatInventoryTraceRows(saleRows, 6));
}
return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(purchaseRows.length > 0 && saleRows.length > 0
? "strong"
: purchaseRows.length > 0 || saleRows.length > 0
? "medium"
: "weak", purchaseRows.length > 0 || saleRows.length > 0));
}
return null;
}
@@ -0,0 +1,39 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.toComposeReplyText = toComposeReplyText;
exports.buildComposeReplyResult = buildComposeReplyResult;
exports.buildFactualSummaryReply = buildFactualSummaryReply;
exports.buildFactualListReply = buildFactualListReply;
exports.buildConfirmedBalanceSemantics = buildConfirmedBalanceSemantics;
exports.buildHeuristicCandidatesSemantics = buildHeuristicCandidatesSemantics;
const replyPackaging_1 = require("./replyPackaging");
function toComposeReplyText(text) {
return Array.isArray(text) ? (0, replyPackaging_1.joinComposeReplyLines)(text) : String(text ?? "");
}
function buildComposeReplyResult(responseType, text, semantics) {
return {
responseType,
text: toComposeReplyText(text),
...(semantics ? { semantics } : {})
};
}
function buildFactualSummaryReply(text, semantics) {
return buildComposeReplyResult("FACTUAL_SUMMARY", text, semantics);
}
function buildFactualListReply(text, semantics) {
return buildComposeReplyResult("FACTUAL_LIST", text, semantics);
}
function buildConfirmedBalanceSemantics(evidenceStrength = "strong", balanceConfirmed = true) {
return {
result_mode: "confirmed_balance",
evidence_strength: evidenceStrength,
balance_confirmed: balanceConfirmed
};
}
function buildHeuristicCandidatesSemantics(hasCandidates) {
return {
result_mode: "heuristic_candidates",
evidence_strength: hasCandidates ? "medium" : "weak",
balance_confirmed: false
};
}
@@ -1071,6 +1071,20 @@ export function isLowQualityInventoryItemAnchorValue(rawValue: string): boolean
.map((token) => token.trim())
.filter(Boolean)
.filter((token) => !lowQualityTokens.has(token));
if (
/^(?:(?:\u043a\u043e\u0433\u0434\u0430|when)\s+)?(?:(?:\u0431\u044b\u043b(?:\u0430|\u0438|\u043e)?|was|were)\s+)?(?:\u0437\u0430\u043a\u0443\u043f[\p{L}\p{N}_-]*|purchase\s+date|purchase)$/iu.test(
value
)
) {
return true;
}
if (
/^(?:(?:\u043a\u0442\u043e|who)\s+)?(?:(?:\u043d\u0430\u043c\s+)?(?:\u043f\u043e\u0441\u0442\u0430\u0432[\p{L}\p{N}_-]*|supplier)|supplier)$/iu.test(
value
)
) {
return true;
}
return meaningfulTokens.length === 0;
}
@@ -861,6 +861,12 @@ function hasCounterpartyActivityLifecycleSignal(text: string): boolean {
}
function hasCounterpartyShipmentItemFlowSignal(text: string): boolean {
const hasSelectedObjectInventoryCue = /(?:по\s+этой\s+позици(?:и|я|ю)|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|selected\s+object|по\s+выбранному\s+объекту)/iu.test(
text
);
if (hasSelectedObjectInventoryCue) {
return false;
}
const hasNamedTailAfterShipmentCue =
/(?:отгруж(?:ал|али|ено)|постав(?:лял|ляли|ил|или)|привоз(?:ил|или)|продал)\s+[a-zа-яё][a-zа-яё0-9._-]{2,}/iu.test(
text
@@ -1705,10 +1711,13 @@ function hasInventoryProvenanceSignalV2(text: string): boolean {
}
function hasInventoryPurchaseDateSignal(text: string): boolean {
const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text);
const hasPurchaseDateCue = /(?:когда\s+(?:примерно\s+)?(?:мы\s+)?купили|когда\s+был\s+куплен|когда\s+куплен|дата\s+закупк|purchase\s+date)/iu.test(
text
);
const hasItemCue =
/(?:товар|номенклатур|sku|item|product)/iu.test(text) || hasSelectedObjectInventoryCue(text);
const hasPurchaseDateCue =
/(?:когда\s+(?:примерно\s+)?(?:мы\s+)?купили|когда\s+был\s+куплен|когда\s+куплен|дата\s+закупк|purchase\s+date)/iu.test(
text
) ||
/(?:когда\s+был(?:а|и|о)?\s+закупк\w*|когда\s+закупк\w*)/iu.test(text);
return hasItemCue && hasPurchaseDateCue;
}
@@ -13,6 +13,7 @@ import {
type ComposeReplyResult,
type ComposeReplySemantics
} from "./replyPackaging";
import { composeInventoryReply } from "./inventoryReplyBuilders";
export type { ComposeFactualReplyOptions, ComposeReplySemantics } from "./replyPackaging";
@@ -2729,6 +2730,26 @@ function composeFactualReplyBody(
options: ComposeFactualReplyOptions = {}
): ComposeReplyResult {
const joinLines = joinComposeReplyLines;
const inventoryReply = composeInventoryReply(intent, rows, options, {
resolvePayablesAsOfDate,
buildInventoryOnHandAggregate,
uniqueStrings,
formatDateRu,
formatNumberWithDots,
formatMoneyRub,
isInventoryPurchaseMovement,
summarizeInventoryTraceRows,
formatInventoryTraceRows,
hasInventoryPurchaseDateActionFocus,
inventoryTraceDateLabel,
extractInventoryCounterpartyCandidates,
buildInventoryAgingByItemAggregate,
formatInventoryAgingRows,
isInventorySaleMovement
});
if (inventoryReply) {
return inventoryReply;
}
if (intent === "document_type_and_account_section_profile") {
const rowsByMarker = new Map<string, ComposeStageRow[]>();
@@ -4251,401 +4272,6 @@ function composeFactualReplyBody(
};
}
if (intent === "inventory_on_hand_as_of_date") {
const asOfDate = resolvePayablesAsOfDate(options);
const positions = buildInventoryOnHandAggregate(rows, asOfDate);
const uniqueItems = uniqueStrings(positions.map((item) => item.item));
const uniqueWarehouses = uniqueStrings(
positions.map((item) => String(item.warehouse ?? "").trim()).filter((item) => item.length > 0)
);
const totalQuantity = positions.reduce((sum, item) => sum + item.quantity, 0);
const totalAmount = positions.reduce((sum, item) => sum + item.amount, 0);
const directAnswerLine =
positions.length > 0
? `На ${formatDateRu(asOfDate)} на складе подтверждено ${formatNumberWithDots(positions.length)} позиций с остатком на ${formatMoneyRub(totalAmount)}.`
: `На ${formatDateRu(asOfDate)} подтвержденных товарных остатков по счету 41.01 не найдено.`;
const lines: string[] = [directAnswerLine];
if (positions.length > 0) {
lines.push("", "Позиции:");
lines.push(
...positions.slice(0, 20).map((item, index) => {
const warehouseLabel = item.warehouse ?? "склад не определен";
const organizationLabel = item.organization ? ` | организация: ${item.organization}` : "";
const periodLabel = item.lastPeriod ? ` | дата строки: ${item.lastPeriod}` : "";
const refsLabel = item.sourceRefs.length > 0 ? ` | source refs: ${item.sourceRefs.slice(0, 2).join("; ")}` : "";
return `${index + 1}. ${item.item} | склад: ${warehouseLabel} | количество: ${formatNumberWithDots(item.quantity, 3)} | стоимость: ${formatMoneyRub(item.amount)}${organizationLabel}${periodLabel}${refsLabel}`;
})
);
} else {
lines.push("", "Позиции:", "- На дату среза товары с ненулевым остатком по счету 41.01 не найдены.");
}
lines.push(
"",
"Подтверждение:",
`- Дата среза: ${formatDateRu(asOfDate)}.`,
"- Контур: остатки по счету 41.01 «Товары на складах».",
`- Уникальных товаров: ${formatNumberWithDots(uniqueItems.length)}.`,
`- Уникальных складов: ${formatNumberWithDots(uniqueWarehouses.length)}.`,
`- Суммарное количество: ${formatNumberWithDots(totalQuantity, 3)}.`
);
if (rows.length !== positions.length) {
lines.push(`- Строк в подтвержденной выборке: ${formatNumberWithDots(rows.length)}.`);
}
return {
responseType: positions.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
text: joinLines(lines),
semantics: {
result_mode: "confirmed_balance",
evidence_strength: positions.length > 0 ? "strong" : "medium",
balance_confirmed: true
}
};
}
if (intent === "inventory_purchase_documents_for_item") {
const asOfDate = resolvePayablesAsOfDate(options);
const purchaseRows = rows.filter((row) => isInventoryPurchaseMovement(row));
const summary = summarizeInventoryTraceRows(purchaseRows);
const itemLabel = summary.item ?? "товар не определен";
const directAnswerLine =
purchaseRows.length <= 0
? `По позиции ${itemLabel} подтвержденные документы закупки в доступном контуре не найдены.`
: `По позиции ${itemLabel} найдено ${formatNumberWithDots(summary.documents.length)} подтвержденных документов закупки до ${formatDateRu(asOfDate)}.`;
const lines: string[] = [directAnswerLine];
lines.push("", "Подтверждение:");
lines.push(`- Дата верхней границы: ${formatDateRu(asOfDate)}.`);
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, 4).join("; ")}.`);
}
lines.push("", "Документы:");
if (purchaseRows.length > 0) {
lines.push(...formatInventoryTraceRows(purchaseRows, 12));
} else {
lines.push("- По выбранному товару не найдено проводок поступления на 41.01 в доступном контуре.");
}
return {
responseType: purchaseRows.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
text: joinLines(lines),
semantics: {
result_mode: "confirmed_balance",
evidence_strength: purchaseRows.length > 0 ? "strong" : "medium",
balance_confirmed: purchaseRows.length > 0
}
};
}
if (intent === "inventory_purchase_provenance_for_item") {
const asOfDate = resolvePayablesAsOfDate(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);
const lastPurchaseDate = inventoryTraceDateLabel(summary.lastPeriod);
const directAnswerLine =
purchaseRows.length <= 0 || !summary.firstPeriod
? `По позиции ${itemLabel} подтвержденная дата закупки в доступном контуре не найдена.`
: summary.firstPeriod === summary.lastPeriod
? `Позиция ${itemLabel} куплена ${firstPurchaseDate}.`
: boundedAsOfLabel
? `По позиции ${itemLabel} до ${boundedAsOfLabel} подтвержден диапазон закупок: с ${firstPurchaseDate} по ${lastPurchaseDate}.`
: `По позиции ${itemLabel} подтвержденный диапазон закупок: с ${firstPurchaseDate} по ${lastPurchaseDate}.`;
const lines: string[] = [directAnswerLine];
if (purchaseRows.length > 0) {
lines.push("", "Подтверждение:");
lines.push(`- Первая подтвержденная дата закупки: ${firstPurchaseDate}.`);
if (summary.firstPeriod !== summary.lastPeriod) {
lines.push(`- Последняя подтвержденная дата закупки: ${lastPurchaseDate}.`);
}
if (summary.counterparties.length === 1) {
lines.push(`- Поставщик в доступном закупочном следе: ${summary.counterparties[0]}.`);
} else if (summary.counterparties.length > 1) {
lines.push(`- В доступном закупочном следе найдено несколько поставщиков: ${summary.counterparties.slice(0, 4).join("; ")}.`);
}
if (summary.documents.length > 0) {
lines.push(`- Первый подтверждающий документ: ${summary.documents[0]}.`);
}
if (boundedAsOfLabel) {
lines.push(`- Для ответа учтены закупочные документы не позже ${boundedAsOfLabel}.`);
}
if (summary.counterparties.length > 1) {
lines.push(
`- Без партионного учета нельзя однозначно связать остаток${boundedAsOfLabel ? ` на ${boundedAsOfLabel}` : ""} с одним конкретным поступлением.`
);
}
}
return {
responseType: "FACTUAL_SUMMARY",
text: joinLines(lines),
semantics: {
result_mode: "confirmed_balance",
evidence_strength: purchaseRows.length > 0 ? "strong" : "medium",
balance_confirmed: purchaseRows.length > 0
}
};
}
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: string[] = [directAnswerLine, "", "Подтверждение:"];
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 (boundedAsOfLabel) {
lines.push(`- Для ответа проверены закупочные документы не позже ${boundedAsOfLabel}.`);
}
if (summary.documents.length > 0) {
lines.push("", "Опорные документы:", ...formatInventoryTraceRows(purchaseRows, 8));
}
return {
responseType: "FACTUAL_SUMMARY",
text: joinLines(lines),
semantics: {
result_mode: "confirmed_balance",
evidence_strength:
purchaseRows.length > 0 ? (summary.counterparties.length === 1 ? "strong" : "medium") : "medium",
balance_confirmed: purchaseRows.length > 0
}
};
}
if (intent === "inventory_supplier_stock_overlap_as_of_date") {
const asOfDate = resolvePayablesAsOfDate(options);
const purchaseRows = rows.filter((row) => isInventoryPurchaseMovement(row));
const summary = summarizeInventoryTraceRows(purchaseRows);
const unresolvedRows = purchaseRows.filter((row) => extractInventoryCounterpartyCandidates(row).length === 0);
const warehouseLabel = summary.warehouses[0] ?? "не указанного склада";
const directAnswerLine =
summary.counterparties.length === 1
? `По складскому остатку ${warehouseLabel} выявлен поставщик: ${summary.counterparties[0]}.`
: summary.counterparties.length > 1
? `По складскому остатку ${warehouseLabel} найдено несколько поставщиков: ${summary.counterparties.slice(0, 6).join("; ")}.`
: `По складскому остатку ${warehouseLabel} поставщик в текущем exact-контуре не материализован.`;
const lines: string[] = [
directAnswerLine,
`Собран exact-срез supplier overlap для складского остатка до ${formatDateRu(asOfDate)}.`,
"",
"Блок 1. Статус результата",
`- Контур: подтвержденные закупочные движения на 41.01, связанные со складом ${warehouseLabel}.`,
"- Важно: без партионности этот контур показывает документально наблюдаемые supplier candidates, но не подменяет собой лот-level атрибуцию текущего остатка.",
"",
"Блок 2. Сводка",
`- Первая найденная дата закупочного движения: ${inventoryTraceDateLabel(summary.firstPeriod)}.`,
`- Последняя найденная дата закупочного движения: ${inventoryTraceDateLabel(summary.lastPeriod)}.`,
`- Закупочных документов в выборке: ${formatNumberWithDots(summary.documents.length)}.`,
`- Закупочных операций в выборке: ${formatNumberWithDots(purchaseRows.length)}.`
];
if (summary.counterparties.length > 0) {
lines.push(`- Найденные поставщики в наблюдаемом контуре: ${summary.counterparties.slice(0, 6).join("; ")}.`);
} else if (purchaseRows.length > 0) {
lines.push("- Закупочные движения найдены, но поставщик не материализован отдельным полем в текущем exact-контуре.");
} else {
lines.push("- В доступном exact-контуре не найдено закупочных движений по 41.01 для выбранного складского среза.");
}
if (unresolvedRows.length > 0) {
lines.push(`- Операций без явно материализованного поставщика: ${formatNumberWithDots(unresolvedRows.length)}.`);
}
if (purchaseRows.length > 0) {
lines.push("", "Блок 3. Опорные документы", ...formatInventoryTraceRows(purchaseRows, 10));
}
return {
responseType: "FACTUAL_SUMMARY",
text: joinLines(lines),
semantics: {
result_mode: "confirmed_balance",
evidence_strength: purchaseRows.length > 0 ? (summary.counterparties.length > 0 ? "strong" : "medium") : "medium",
balance_confirmed: purchaseRows.length > 0
}
};
}
if (intent === "inventory_aging_by_purchase_date") {
const asOfDate = resolvePayablesAsOfDate(options);
const purchaseRows = rows.filter((row) => isInventoryPurchaseMovement(row));
const summary = summarizeInventoryTraceRows(purchaseRows);
const agingItems = buildInventoryAgingByItemAggregate(purchaseRows, asOfDate);
const oldestPurchaseDate = agingItems[0]?.firstPurchasePeriod ?? summary.firstPeriod;
const oldestPurchaseAgeDays = agingItems[0]?.ageDays ?? null;
const oldestAnswerPreview = agingItems
.slice(0, 3)
.map((item) => `${item.item} (${inventoryTraceDateLabel(item.firstPurchasePeriod)})`)
.join("; ");
const directAnswerLine =
agingItems.length > 0
? `К старым закупкам на ${formatDateRu(asOfDate)} в первую очередь относятся позиции с самой ранней первой закупкой: ${oldestAnswerPreview}.`
: `По доступному закупочному следу на ${formatDateRu(asOfDate)} позиции старых закупок не материализованы.`;
const lines: string[] = [
directAnswerLine,
`Собран exact-срез старых закупок для складского остатка на ${formatDateRu(asOfDate)}.`,
"",
"Блок 1. Статус результата",
"- Контур: показан item-level список товарных позиций с самым ранним документально наблюдаемым закупочным следом на 41.01.",
"- Порядок: позиции отсортированы от самой старой первой закупки к более новым.",
"- Важно: без партионности этот контур не доказывает возраст конкретного лота, а показывает документально наблюдаемый возраст закупочного следа по товарной позиции.",
"",
"Блок 2. Сводка",
`- Дата среза: ${formatDateRu(asOfDate)}.`,
`- Самая ранняя первая закупка среди позиций: ${inventoryTraceDateLabel(oldestPurchaseDate)}.`,
`- Самая поздняя найденная закупка в наблюдаемом следе: ${inventoryTraceDateLabel(summary.lastPeriod)}.`,
`- Позиции в aging-срезе: ${formatNumberWithDots(agingItems.length)}.`,
`- Закупочных документов в наблюдаемом следе: ${formatNumberWithDots(summary.documents.length)}.`,
`- Закупочных операций в наблюдаемом следе: ${formatNumberWithDots(purchaseRows.length)}.`
];
if (oldestPurchaseAgeDays !== null) {
lines.push(`- Между самой ранней первой закупкой и датой среза прошло ${formatNumberWithDots(oldestPurchaseAgeDays)} дн.`);
}
if (summary.counterparties.length > 0) {
lines.push(`- Поставщики, встречающиеся в наблюдаемом закупочном следе: ${summary.counterparties.slice(0, 4).join("; ")}.`);
}
if (agingItems.length > 0) {
lines.push("", "Блок 3. Позиции от самых старых закупок", ...formatInventoryAgingRows(agingItems, asOfDate, 12));
} else {
lines.push("", "Блок 3. Позиции от самых старых закупок", "- В доступном exact-контуре не найдено закупочных движений по 41.01 для выбранного среза.");
}
return {
responseType: "FACTUAL_SUMMARY",
text: joinLines(lines),
semantics: {
result_mode: "confirmed_balance",
evidence_strength: agingItems.length > 0 ? "strong" : "medium",
balance_confirmed: agingItems.length > 0
}
};
}
if (intent === "inventory_sale_trace_for_item") {
const asOfDate = resolvePayablesAsOfDate(options);
const saleRows = rows.filter((row) => isInventorySaleMovement(row));
const requestedItemHint = String(options.itemHint ?? "").trim();
const provisionalExcludedTokens = requestedItemHint ? [requestedItemHint] : [];
const summary = summarizeInventoryTraceRows(saleRows, provisionalExcludedTokens);
const itemLabel = requestedItemHint || (summary.item ?? "товар не определен");
const excludedCounterpartyTokens = [itemLabel];
const directAnswerLine =
summary.counterparties.length === 1
? `По товару ${itemLabel} покупатель определен: ${summary.counterparties[0]}.`
: summary.counterparties.length > 1
? `По товару ${itemLabel} найдено несколько покупателей: ${summary.counterparties.slice(0, 4).join("; ")}.`
: `По товару ${itemLabel} покупатель в текущем exact-контуре не материализован.`;
const lines: string[] = [directAnswerLine, "", "Подтверждение:"];
lines.push(`- Первая найденная дата выбытия: ${inventoryTraceDateLabel(summary.firstPeriod)}.`);
lines.push(`- Последняя найденная дата выбытия: ${inventoryTraceDateLabel(summary.lastPeriod)}.`);
lines.push(`- Документов выбытия: ${formatNumberWithDots(summary.documents.length)}.`);
lines.push(`- Операций выбытия: ${formatNumberWithDots(saleRows.length)}.`);
if (summary.counterparties.length === 1) {
lines.push(`- По доступным движениям товар отгружался покупателю: ${summary.counterparties[0]}.`);
} else if (summary.counterparties.length > 1) {
lines.push(`- По доступным движениям найдено несколько покупателей: ${summary.counterparties.slice(0, 4).join("; ")}.`);
} else if (saleRows.length > 0) {
lines.push("- Документы выбытия найдены, но покупатель не материализован отдельным полем в текущем exact-контуре.");
}
lines.push("", "Документы выбытия:");
if (saleRows.length > 0) {
lines.push(...formatInventoryTraceRows(saleRows, 12, excludedCounterpartyTokens));
} else {
lines.push("- По выбранному товару не найдено проводок выбытия со счета 41.01 в доступном контуре.");
}
return {
responseType: saleRows.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
text: joinLines(lines),
semantics: {
result_mode: "confirmed_balance",
evidence_strength: saleRows.length > 0 ? (summary.counterparties.length > 0 ? "strong" : "medium") : "medium",
balance_confirmed: saleRows.length > 0
}
};
}
if (intent === "inventory_purchase_to_sale_chain") {
const asOfDate = resolvePayablesAsOfDate(options);
const purchaseRows = rows.filter((row) => isInventoryPurchaseMovement(row));
const saleRows = rows.filter((row) => isInventorySaleMovement(row));
const purchaseSummary = summarizeInventoryTraceRows(purchaseRows);
const saleSummary = summarizeInventoryTraceRows(saleRows);
const itemLabel = purchaseSummary.item ?? saleSummary.item ?? "товар не определен";
const directAnswerLine =
purchaseSummary.counterparties.length === 1 && saleSummary.counterparties.length === 1
? `По товару ${itemLabel} цепочка поставки и продажи связана с поставщиком ${purchaseSummary.counterparties[0]} и покупателем ${saleSummary.counterparties[0]}.`
: `По товару ${itemLabel} цепочка поставки и продажи подтверждена частично или разнообразно: детали идут следом.`;
const lines: string[] = [directAnswerLine, "", "Подтверждение:"];
lines.push(`- Закупочных движений на 41.01: ${formatNumberWithDots(purchaseRows.length)}.`);
lines.push(`- Движений выбытия со счета 41.01: ${formatNumberWithDots(saleRows.length)}.`);
if (purchaseRows.length > 0 && saleRows.length > 0) {
lines.push("- В текущем контуре найдены обе стороны цепочки: поступление и последующее выбытие.");
} else if (purchaseRows.length > 0) {
lines.push("- Найдена только закупочная часть цепочки; выбытие в текущем exact-контуре не подтверждено.");
} else if (saleRows.length > 0) {
lines.push("- Найдена только часть выбытия; закупочная часть цепочки в текущем exact-контуре не подтверждена.");
} else {
lines.push("- Для выбранного товара не найдено движений по 41.01, из которых можно собрать цепочку.");
}
if (purchaseRows.length > 0) {
lines.push(
"",
"Закупка:",
`- Первая дата: ${inventoryTraceDateLabel(purchaseSummary.firstPeriod)}.`,
`- Последняя дата: ${inventoryTraceDateLabel(purchaseSummary.lastPeriod)}.`,
...formatInventoryTraceRows(purchaseRows, 6)
);
}
if (saleRows.length > 0) {
lines.push(
"",
"Продажа:",
`- Первая дата: ${inventoryTraceDateLabel(saleSummary.firstPeriod)}.`,
`- Последняя дата: ${inventoryTraceDateLabel(saleSummary.lastPeriod)}.`,
...formatInventoryTraceRows(saleRows, 6)
);
}
return {
responseType: purchaseRows.length > 0 || saleRows.length > 0 ? "FACTUAL_SUMMARY" : "FACTUAL_SUMMARY",
text: joinLines(lines),
semantics: {
result_mode: "confirmed_balance",
evidence_strength: purchaseRows.length > 0 && saleRows.length > 0 ? "strong" : purchaseRows.length > 0 || saleRows.length > 0 ? "medium" : "weak",
balance_confirmed: purchaseRows.length > 0 || saleRows.length > 0
}
};
}
if (intent === "open_contracts_confirmed_as_of_date") {
const asOfDate = resolvePayablesAsOfDate(options);
const confirmedContracts = buildOpenContractConfirmedBalanceAggregate(rows, asOfDate);
@@ -580,7 +580,9 @@ function shouldRestoreInventoryRootFrame(
}
function hasSelectedObjectInventorySignal(text: string): boolean {
return /(?:по\s+выбранному\s+объекту|for\s+selected\s+object)/iu.test(String(text ?? ""));
return /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(
String(text ?? "")
);
}
function hasSelectedObjectInlineSnapshotMetadata(text: string): boolean {
@@ -629,9 +631,13 @@ export function hasInventoryProfitabilityFollowupCue(text: string): boolean {
export function hasInventoryPurchaseDateFollowupCue(text: string): boolean {
const value = String(text ?? "");
return /(?:когда\s+(?:примерно\s+)?(?:мы\s+)?купили|когда\s+был\s+куплен|когда\s+куплен|когда\s+это\s+купили|когда\s+эту\s+позицию\s+купили|когда\s+ее\s+купили|дата\s+закупки|purchase\s+date)/iu.test(
value
) || (/когда/iu.test(value) && hasInventoryPurchaseStem(value));
return (
/(?:когда\s+(?:примерно\s+)?(?:мы\s+)?купили|когда\s+был\s+куплен|когда\s+куплен|когда\s+была\s+закупк(?:а|и)|когда\s+это\s+купили|когда\s+эту\s+позицию\s+купили|когда\s+ее\s+купили|дата\s+закупки|purchase\s+date)/iu.test(
value
) ||
/(?:по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)[\s\S]{0,40}(?:когда|дата\s+закупки|закупк))/iu.test(value) ||
(/когда/iu.test(value) && hasInventoryPurchaseStem(value))
);
}
export function hasBareInventoryPurchaseDateFollowupCue(text: string): boolean {
@@ -657,7 +663,11 @@ export function hasAddressFollowupContextSignal(text: string): boolean {
if (!normalized) {
return false;
}
if (/(?:по\s+выбранному\s+объекту|for\s+selected\s+object)/iu.test(normalized)) {
if (
/(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(
normalized
)
) {
return true;
}
if (hasAllTimeHint(normalized)) {
@@ -1373,6 +1383,10 @@ function deriveIntentWithFollowupContext(
if (inventorySelectedObjectFollowup && hasInventoryPurchaseDateFollowupCue(normalizedMessage)) {
if (
detectedIntent.intent === "unknown" ||
detectedIntent.intent === "list_documents_by_counterparty" ||
detectedIntent.intent === "list_documents_by_contract" ||
detectedIntent.intent === "bank_operations_by_counterparty" ||
detectedIntent.intent === "bank_operations_by_contract" ||
detectedIntent.intent === "inventory_purchase_provenance_for_item" ||
detectedIntent.intent === sourceIntent ||
detectedIntent.intent === "inventory_on_hand_as_of_date"
@@ -0,0 +1,446 @@
import type { AddressIntent } from "../../types/addressQuery";
import {
buildConfirmedBalanceSemantics,
buildFactualListReply,
buildFactualSummaryReply
} from "./replyContracts";
import type { ComposeReplyResult } from "./replyPackaging";
import type { ComposeStageRow } from "./composeStage";
interface InventoryComposeOptions {
userMessage?: string;
itemHint?: string;
asOfDate?: string;
periodFrom?: string;
periodTo?: string;
}
interface InventoryOnHandAggregate {
item: string;
warehouse: string | null;
organization: string | null;
quantity: number;
amount: number;
operations: number;
firstPeriod: string | null;
lastPeriod: string | null;
sourceRefs: string[];
}
interface InventoryTraceSummary {
item: string | null;
warehouses: string[];
organizations: string[];
counterparties: string[];
documents: string[];
firstPeriod: string | null;
lastPeriod: string | null;
totalAmount: number;
}
interface InventoryAgingByItemAggregate {
item: string;
warehouse: string | null;
organization: string | null;
firstPurchasePeriod: string | null;
lastPurchasePeriod: string | null;
operations: number;
documentCount: number;
counterparties: string[];
ageDays: number | null;
}
interface InventoryReplyDeps {
resolvePayablesAsOfDate: (options: InventoryComposeOptions) => string;
buildInventoryOnHandAggregate: (rows: ComposeStageRow[], asOfDate: string) => InventoryOnHandAggregate[];
uniqueStrings: (values: string[]) => string[];
formatDateRu: (isoDate: string) => string;
formatNumberWithDots: (value: number, fractionDigits?: number) => string;
formatMoneyRub: (value: number) => string;
isInventoryPurchaseMovement: (row: ComposeStageRow) => boolean;
summarizeInventoryTraceRows: (rows: ComposeStageRow[], excludedCounterpartyTokens?: string[]) => InventoryTraceSummary;
formatInventoryTraceRows: (rows: ComposeStageRow[], limit?: number, excludedCounterpartyTokens?: string[]) => string[];
hasInventoryPurchaseDateActionFocus: (userMessage: string | null | undefined) => boolean;
inventoryTraceDateLabel: (value: string | null) => string;
extractInventoryCounterpartyCandidates: (row: ComposeStageRow, excludedTokens?: string[]) => string[];
buildInventoryAgingByItemAggregate: (rows: ComposeStageRow[], asOfDate: string) => InventoryAgingByItemAggregate[];
formatInventoryAgingRows: (items: InventoryAgingByItemAggregate[], asOfDate: string, limit?: number) => string[];
isInventorySaleMovement: (row: ComposeStageRow) => boolean;
}
export function composeInventoryReply(
intent: AddressIntent,
rows: ComposeStageRow[],
options: InventoryComposeOptions,
deps: InventoryReplyDeps
): ComposeReplyResult | null {
if (intent === "inventory_on_hand_as_of_date") {
const asOfDate = deps.resolvePayablesAsOfDate(options);
const positions = deps.buildInventoryOnHandAggregate(rows, asOfDate);
const uniqueItems = deps.uniqueStrings(positions.map((item) => item.item));
const uniqueWarehouses = deps.uniqueStrings(
positions.map((item) => String(item.warehouse ?? "").trim()).filter((item) => item.length > 0)
);
const totalQuantity = positions.reduce((sum, item) => sum + item.quantity, 0);
const totalAmount = positions.reduce((sum, item) => sum + item.amount, 0);
const directAnswerLine =
positions.length > 0
? `На ${deps.formatDateRu(asOfDate)} на складе подтверждено ${deps.formatNumberWithDots(positions.length)} позиций с остатком на ${deps.formatMoneyRub(totalAmount)}.`
: `На ${deps.formatDateRu(asOfDate)} подтвержденных товарных остатков по счету 41.01 не найдено.`;
const lines: string[] = [directAnswerLine];
if (positions.length > 0) {
lines.push("", "Позиции:");
lines.push(
...positions.slice(0, 20).map((item, index) => {
const warehouseLabel = item.warehouse ?? "склад не определен";
const organizationLabel = item.organization ? ` | организация: ${item.organization}` : "";
const periodLabel = item.lastPeriod ? ` | дата строки: ${item.lastPeriod}` : "";
const refsLabel = item.sourceRefs.length > 0 ? ` | source refs: ${item.sourceRefs.slice(0, 2).join("; ")}` : "";
return `${index + 1}. ${item.item} | склад: ${warehouseLabel} | количество: ${deps.formatNumberWithDots(item.quantity, 3)} | стоимость: ${deps.formatMoneyRub(item.amount)}${organizationLabel}${periodLabel}${refsLabel}`;
})
);
} else {
lines.push("", "Позиции:", "- На дату среза товары с ненулевым остатком по счету 41.01 не найдены.");
}
lines.push(
"",
"Подтверждение:",
`- Дата среза: ${deps.formatDateRu(asOfDate)}.`,
"- Контур: остатки по счету 41.01 «Товары на складах».",
`- Уникальных товаров: ${deps.formatNumberWithDots(uniqueItems.length)}.`,
`- Уникальных складов: ${deps.formatNumberWithDots(uniqueWarehouses.length)}.`,
`- Суммарное количество: ${deps.formatNumberWithDots(totalQuantity, 3)}.`
);
if (rows.length !== positions.length) {
lines.push(`- Строк в подтвержденной выборке: ${deps.formatNumberWithDots(rows.length)}.`);
}
return positions.length > 0
? buildFactualListReply(lines, buildConfirmedBalanceSemantics("strong"))
: buildFactualSummaryReply(lines, buildConfirmedBalanceSemantics("medium"));
}
if (intent === "inventory_purchase_documents_for_item") {
const asOfDate = deps.resolvePayablesAsOfDate(options);
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
const itemLabel = summary.item ?? "товар не определен";
const directAnswerLine =
purchaseRows.length <= 0
? `По позиции ${itemLabel} подтвержденные документы закупки в доступном контуре не найдены.`
: `По позиции ${itemLabel} найдено ${deps.formatNumberWithDots(summary.documents.length)} подтвержденных документов закупки до ${deps.formatDateRu(asOfDate)}.`;
const lines: string[] = [directAnswerLine];
lines.push("", "Подтверждение:");
lines.push(`- Дата верхней границы: ${deps.formatDateRu(asOfDate)}.`);
lines.push(`- Операций поступления в выборке: ${deps.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, 4).join("; ")}.`);
}
lines.push("", "Документы:");
if (purchaseRows.length > 0) {
lines.push(...deps.formatInventoryTraceRows(purchaseRows, 12));
} else {
lines.push("- По выбранному товару не найдено проводок поступления на 41.01 в доступном контуре.");
}
return purchaseRows.length > 0
? buildFactualListReply(lines, buildConfirmedBalanceSemantics("strong", true))
: buildFactualSummaryReply(lines, buildConfirmedBalanceSemantics("medium", false));
}
if (intent === "inventory_purchase_provenance_for_item") {
const asOfDate = deps.resolvePayablesAsOfDate(options);
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
const itemLabel = summary.item ?? "товар не определен";
const boundedAsOfLabel = asOfDate ? deps.formatDateRu(asOfDate) : null;
const purchaseDateActionFocus = deps.hasInventoryPurchaseDateActionFocus(options.userMessage);
if (purchaseDateActionFocus) {
const firstPurchaseDate = deps.inventoryTraceDateLabel(summary.firstPeriod);
const lastPurchaseDate = deps.inventoryTraceDateLabel(summary.lastPeriod);
const directAnswerLine =
purchaseRows.length <= 0 || !summary.firstPeriod
? `По позиции ${itemLabel} подтвержденная дата закупки в доступном контуре не найдена.`
: summary.firstPeriod === summary.lastPeriod
? `Позиция ${itemLabel} куплена ${firstPurchaseDate}.`
: boundedAsOfLabel
? `По позиции ${itemLabel} до ${boundedAsOfLabel} подтвержден диапазон закупок: с ${firstPurchaseDate} по ${lastPurchaseDate}.`
: `По позиции ${itemLabel} подтвержденный диапазон закупок: с ${firstPurchaseDate} по ${lastPurchaseDate}.`;
const lines: string[] = [directAnswerLine];
if (purchaseRows.length > 0) {
lines.push("", "Подтверждение:");
lines.push(`- Первая подтвержденная дата закупки: ${firstPurchaseDate}.`);
if (summary.firstPeriod !== summary.lastPeriod) {
lines.push(`- Последняя подтвержденная дата закупки: ${lastPurchaseDate}.`);
}
if (summary.counterparties.length === 1) {
lines.push(`- Поставщик в доступном закупочном следе: ${summary.counterparties[0]}.`);
} else if (summary.counterparties.length > 1) {
lines.push(`- В доступном закупочном следе найдено несколько поставщиков: ${summary.counterparties.slice(0, 4).join("; ")}.`);
}
if (summary.documents.length > 0) {
lines.push(`- Первый подтверждающий документ: ${summary.documents[0]}.`);
}
if (boundedAsOfLabel) {
lines.push(`- Для ответа учтены закупочные документы не позже ${boundedAsOfLabel}.`);
}
if (summary.counterparties.length > 1) {
lines.push(
`- Без партионного учета нельзя однозначно связать остаток${boundedAsOfLabel ? ` на ${boundedAsOfLabel}` : ""} с одним конкретным поступлением.`
);
}
}
return buildFactualSummaryReply(
lines,
buildConfirmedBalanceSemantics(purchaseRows.length > 0 ? "strong" : "medium", purchaseRows.length > 0)
);
}
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: string[] = [directAnswerLine, "", "Подтверждение:"];
if (purchaseRows.length > 0) {
lines.push(`- Первая найденная дата закупки: ${deps.inventoryTraceDateLabel(summary.firstPeriod)}.`);
lines.push(`- Последняя найденная дата закупки: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`);
lines.push(`- Документов поступления: ${deps.formatNumberWithDots(summary.documents.length)}.`);
lines.push(`- Операций поступления: ${deps.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 (boundedAsOfLabel) {
lines.push(`- Для ответа проверены закупочные документы не позже ${boundedAsOfLabel}.`);
}
if (summary.documents.length > 0) {
lines.push("", "Опорные документы:", ...deps.formatInventoryTraceRows(purchaseRows, 8));
}
return buildFactualSummaryReply(
lines,
buildConfirmedBalanceSemantics(
purchaseRows.length > 0 ? (summary.counterparties.length === 1 ? "strong" : "medium") : "medium",
purchaseRows.length > 0
)
);
}
if (intent === "inventory_supplier_stock_overlap_as_of_date") {
const asOfDate = deps.resolvePayablesAsOfDate(options);
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
const unresolvedRows = purchaseRows.filter((row) => deps.extractInventoryCounterpartyCandidates(row).length === 0);
const warehouseLabel = summary.warehouses[0] ?? "не указанного склада";
const directAnswerLine =
summary.counterparties.length === 1
? `По складскому остатку ${warehouseLabel} выявлен поставщик: ${summary.counterparties[0]}.`
: summary.counterparties.length > 1
? `По складскому остатку ${warehouseLabel} найдено несколько поставщиков: ${summary.counterparties.slice(0, 6).join("; ")}.`
: `По складскому остатку ${warehouseLabel} поставщик в текущем exact-контуре не материализован.`;
const lines: string[] = [
directAnswerLine,
`Собран exact-срез supplier overlap для складского остатка до ${deps.formatDateRu(asOfDate)}.`,
"",
"Блок 1. Статус результата",
`- Контур: подтвержденные закупочные движения на 41.01, связанные со складом ${warehouseLabel}.`,
"- Важно: без партионности этот контур не доказывает конкретного владельца каждой партии, а показывает наблюдаемый закупочный след текущего остатка.",
"",
"Блок 2. Подтверждение",
`- Дата среза: ${deps.formatDateRu(asOfDate)}.`,
`- Первая найденная дата закупочного движения: ${deps.inventoryTraceDateLabel(summary.firstPeriod)}.`,
`- Последняя найденная дата закупочного движения: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`,
`- Закупочных документов в выборке: ${deps.formatNumberWithDots(summary.documents.length)}.`,
`- Закупочных операций в выборке: ${deps.formatNumberWithDots(purchaseRows.length)}.`
];
if (summary.counterparties.length > 0) {
lines.push(`- Найденные поставщики в наблюдаемом контуре: ${summary.counterparties.slice(0, 6).join("; ")}.`);
} else if (purchaseRows.length > 0) {
lines.push("- Закупочные движения найдены, но поставщик не материализован отдельным полем в текущем exact-контуре.");
} else {
lines.push("- В доступном exact-контуре не найдено закупочных движений по 41.01 для выбранного складского среза.");
}
if (unresolvedRows.length > 0) {
lines.push(`- Операций без явно материализованного поставщика: ${deps.formatNumberWithDots(unresolvedRows.length)}.`);
}
if (purchaseRows.length > 0) {
lines.push("", "Блок 3. Опорные документы", ...deps.formatInventoryTraceRows(purchaseRows, 10));
}
return buildFactualSummaryReply(
lines,
buildConfirmedBalanceSemantics(
purchaseRows.length > 0 ? (summary.counterparties.length > 0 ? "strong" : "medium") : "medium",
purchaseRows.length > 0
)
);
}
if (intent === "inventory_aging_by_purchase_date") {
const asOfDate = deps.resolvePayablesAsOfDate(options);
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
const agingItems = deps.buildInventoryAgingByItemAggregate(purchaseRows, asOfDate);
const oldestPurchaseDate = agingItems[0]?.firstPurchasePeriod ?? summary.firstPeriod;
const oldestPurchaseAgeDays = agingItems[0]?.ageDays ?? null;
const oldestAnswerPreview = agingItems
.slice(0, 3)
.map((item) => `${item.item} (${deps.inventoryTraceDateLabel(item.firstPurchasePeriod)})`)
.join("; ");
const directAnswerLine =
agingItems.length > 0
? `К старым закупкам на ${deps.formatDateRu(asOfDate)} в первую очередь относятся позиции с самой ранней первой закупкой: ${oldestAnswerPreview}.`
: `По доступному закупочному следу на ${deps.formatDateRu(asOfDate)} позиции старых закупок не материализованы.`;
const lines: string[] = [
directAnswerLine,
`Собран exact-срез старых закупок для складского остатка на ${deps.formatDateRu(asOfDate)}.`,
"",
"Блок 1. Статус результата",
"- Контур: показан item-level список товарных позиций с самым ранним документально наблюдаемым закупочным следом на 41.01.",
"- Порядок: позиции отсортированы от самой старой первой закупки к более новым.",
"- Важно: без партионности этот контур не доказывает возраст конкретного лота, а показывает документально наблюдаемый возраст закупочного следа по товарной позиции.",
"",
"Блок 2. Сводка",
`- Дата среза: ${deps.formatDateRu(asOfDate)}.`,
`- Самая ранняя первая закупка среди позиций: ${deps.inventoryTraceDateLabel(oldestPurchaseDate)}.`,
`- Самая поздняя найденная закупка в наблюдаемом следе: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`,
`- Позиции в aging-срезе: ${deps.formatNumberWithDots(agingItems.length)}.`,
`- Закупочных документов в наблюдаемом следе: ${deps.formatNumberWithDots(summary.documents.length)}.`,
`- Закупочных операций в наблюдаемом следе: ${deps.formatNumberWithDots(purchaseRows.length)}.`
];
if (oldestPurchaseAgeDays !== null) {
lines.push(`- Между самой ранней первой закупкой и датой среза прошло ${deps.formatNumberWithDots(oldestPurchaseAgeDays)} дн.`);
}
if (summary.counterparties.length > 0) {
lines.push(`- Поставщики, встречающиеся в наблюдаемом закупочном следе: ${summary.counterparties.slice(0, 4).join("; ")}.`);
}
if (agingItems.length > 0) {
lines.push("", "Блок 3. Позиции от самых старых закупок", ...deps.formatInventoryAgingRows(agingItems, asOfDate, 12));
} else {
lines.push("", "Блок 3. Позиции от самых старых закупок", "- В доступном exact-контуре не найдено закупочных движений по 41.01 для выбранного среза.");
}
return buildFactualSummaryReply(
lines,
buildConfirmedBalanceSemantics(agingItems.length > 0 ? "strong" : "medium", agingItems.length > 0)
);
}
if (intent === "inventory_sale_trace_for_item") {
const asOfDate = deps.resolvePayablesAsOfDate(options);
const saleRows = rows.filter((row) => deps.isInventorySaleMovement(row));
const requestedItemHint = String(options.itemHint ?? "").trim();
const provisionalExcludedTokens = requestedItemHint ? [requestedItemHint] : [];
const summary = deps.summarizeInventoryTraceRows(saleRows, provisionalExcludedTokens);
const itemLabel = requestedItemHint || (summary.item ?? "товар не определен");
const excludedCounterpartyTokens = [itemLabel];
const directAnswerLine =
summary.counterparties.length === 1
? `По товару ${itemLabel} покупатель определен: ${summary.counterparties[0]}.`
: summary.counterparties.length > 1
? `По товару ${itemLabel} найдено несколько покупателей: ${summary.counterparties.slice(0, 4).join("; ")}.`
: `По товару ${itemLabel} покупатель в текущем exact-контуре не материализован.`;
const lines: string[] = [directAnswerLine, "", "Подтверждение:"];
lines.push(`- Первая найденная дата выбытия: ${deps.inventoryTraceDateLabel(summary.firstPeriod)}.`);
lines.push(`- Последняя найденная дата выбытия: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`);
lines.push(`- Документов выбытия: ${deps.formatNumberWithDots(summary.documents.length)}.`);
lines.push(`- Операций выбытия: ${deps.formatNumberWithDots(saleRows.length)}.`);
if (summary.counterparties.length === 1) {
lines.push(`- По доступным движениям товар отгружался покупателю: ${summary.counterparties[0]}.`);
} else if (summary.counterparties.length > 1) {
lines.push(`- По доступным движениям найдено несколько покупателей: ${summary.counterparties.slice(0, 4).join("; ")}.`);
} else if (saleRows.length > 0) {
lines.push("- Документы выбытия найдены, но покупатель не материализован отдельным полем в текущем exact-контуре.");
}
lines.push("", "Документы выбытия:");
if (saleRows.length > 0) {
lines.push(...deps.formatInventoryTraceRows(saleRows, 12, excludedCounterpartyTokens));
} else {
lines.push("- По выбранному товару не найдено проводок выбытия со счета 41.01 в доступном контуре.");
}
return saleRows.length > 0
? buildFactualListReply(
lines,
buildConfirmedBalanceSemantics(summary.counterparties.length > 0 ? "strong" : "medium", true)
)
: buildFactualSummaryReply(lines, buildConfirmedBalanceSemantics("medium", false));
}
if (intent === "inventory_purchase_to_sale_chain") {
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
const saleRows = rows.filter((row) => deps.isInventorySaleMovement(row));
const purchaseSummary = deps.summarizeInventoryTraceRows(purchaseRows);
const saleSummary = deps.summarizeInventoryTraceRows(saleRows);
const itemLabel = purchaseSummary.item ?? saleSummary.item ?? "товар не определен";
const directAnswerLine =
purchaseSummary.counterparties.length === 1 && saleSummary.counterparties.length === 1
? `По товару ${itemLabel} цепочка поставки и продажи связана с поставщиком ${purchaseSummary.counterparties[0]} и покупателем ${saleSummary.counterparties[0]}.`
: `По товару ${itemLabel} цепочка поставки и продажи подтверждена частично или разнообразно: детали идут следом.`;
const lines: string[] = [directAnswerLine, "", "Подтверждение:"];
lines.push(`- Закупочных движений на 41.01: ${deps.formatNumberWithDots(purchaseRows.length)}.`);
lines.push(`- Движений выбытия со счета 41.01: ${deps.formatNumberWithDots(saleRows.length)}.`);
if (purchaseRows.length > 0 && saleRows.length > 0) {
lines.push("- В текущем контуре найдены обе стороны цепочки: поступление и последующее выбытие.");
} else if (purchaseRows.length > 0) {
lines.push("- Найдена только закупочная часть цепочки; выбытие в текущем exact-контуре не подтверждено.");
} else if (saleRows.length > 0) {
lines.push("- Найдена только часть выбытия; закупочная часть цепочки в текущем exact-контуре не подтверждена.");
} else {
lines.push("- Для выбранного товара не найдено движений по 41.01, из которых можно собрать цепочку.");
}
if (purchaseRows.length > 0) {
lines.push(
"",
"Закупка:",
`- Первая дата: ${deps.inventoryTraceDateLabel(purchaseSummary.firstPeriod)}.`,
`- Последняя дата: ${deps.inventoryTraceDateLabel(purchaseSummary.lastPeriod)}.`,
...deps.formatInventoryTraceRows(purchaseRows, 6)
);
}
if (saleRows.length > 0) {
lines.push(
"",
"Продажа:",
`- Первая дата: ${deps.inventoryTraceDateLabel(saleSummary.firstPeriod)}.`,
`- Последняя дата: ${deps.inventoryTraceDateLabel(saleSummary.lastPeriod)}.`,
...deps.formatInventoryTraceRows(saleRows, 6)
);
}
return buildFactualSummaryReply(
lines,
buildConfirmedBalanceSemantics(
purchaseRows.length > 0 && saleRows.length > 0
? "strong"
: purchaseRows.length > 0 || saleRows.length > 0
? "medium"
: "weak",
purchaseRows.length > 0 || saleRows.length > 0
)
);
}
return null;
}
@@ -0,0 +1,53 @@
import type { AddressEvidenceStrength, AddressResponseType } from "../../types/addressQuery";
import { joinComposeReplyLines, type ComposeReplyResult, type ComposeReplySemantics } from "./replyPackaging";
export type ComposeReplyText = string | string[];
export function toComposeReplyText(text: ComposeReplyText): string {
return Array.isArray(text) ? joinComposeReplyLines(text) : String(text ?? "");
}
export function buildComposeReplyResult(
responseType: AddressResponseType,
text: ComposeReplyText,
semantics?: ComposeReplySemantics
): ComposeReplyResult {
return {
responseType,
text: toComposeReplyText(text),
...(semantics ? { semantics } : {})
};
}
export function buildFactualSummaryReply(
text: ComposeReplyText,
semantics?: ComposeReplySemantics
): ComposeReplyResult {
return buildComposeReplyResult("FACTUAL_SUMMARY", text, semantics);
}
export function buildFactualListReply(
text: ComposeReplyText,
semantics?: ComposeReplySemantics
): ComposeReplyResult {
return buildComposeReplyResult("FACTUAL_LIST", text, semantics);
}
export function buildConfirmedBalanceSemantics(
evidenceStrength: AddressEvidenceStrength = "strong",
balanceConfirmed = true
): ComposeReplySemantics {
return {
result_mode: "confirmed_balance",
evidence_strength: evidenceStrength,
balance_confirmed: balanceConfirmed
};
}
export function buildHeuristicCandidatesSemantics(hasCandidates: boolean): ComposeReplySemantics {
return {
result_mode: "heuristic_candidates",
evidence_strength: hasCandidates ? "medium" : "weak",
balance_confirmed: false
};
}
@@ -4120,6 +4120,27 @@ describe("address decompose stage follow-up carryover", () => {
).toBe(true);
});
it("promotes selected-item purchase-date wording 'а по этой позиции когда была закупка' into inventory provenance", () => {
const result = runAddressDecomposeStage("а по этой позиции когда была закупка?", {
previous_intent: "inventory_purchase_provenance_for_item",
previous_filters: {
as_of_date: "2021-03-31",
period_from: "2021-03-01",
period_to: "2021-03-31",
item: "Столешница 600*3050*26 альмандин"
},
previous_anchor_type: "unknown",
previous_anchor_value: null
});
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("inventory_purchase_provenance_for_item");
expect(result?.filters.extracted_filters.item).toBe("Столешница 600*3050*26 альмандин");
expect(
result?.baseReasons?.includes("intent_adjusted_to_inventory_followup_context") ||
result?.intent.reasons.includes("inventory_purchase_date_signal_detected")
).toBe(true);
});
it("promotes conversational buyer follow-up into inventory sale trace with inherited date context", () => {
const result = runAddressDecomposeStage("кому в итоге мы продали этот товар?", {
previous_intent: "inventory_purchase_provenance_for_item",
@@ -5141,6 +5162,11 @@ it("routes old purchase residue questions to aging-by-purchase-date", () => {
expect(result.intent).toBe("inventory_purchase_provenance_for_item");
});
it("keeps selected-object purchase-date pronoun wording out of generic counterparty docs intent", () => {
const result = resolveAddressIntent("а по этой позиции когда была закупка?");
expect(result.intent).toBe("inventory_purchase_provenance_for_item");
});
it("keeps direct item supplier questions in provenance intent even with current-stock tail", () => {
const result = resolveAddressIntent(
"От какого поставщика куплен товар Диван трехместный из текущего остатка на складе Основной склад?"
@@ -0,0 +1,45 @@
import { describe, expect, it } from "vitest";
import {
buildConfirmedBalanceSemantics,
buildFactualListReply,
buildFactualSummaryReply,
buildHeuristicCandidatesSemantics
} from "../src/services/address_runtime/replyContracts";
describe("replyContracts", () => {
it("builds factual replies from line arrays", () => {
const result = buildFactualListReply(["Первая строка", "Вторая строка"], buildConfirmedBalanceSemantics("strong"));
expect(result).toEqual({
responseType: "FACTUAL_LIST",
text: "Первая строка\nВторая строка",
semantics: {
result_mode: "confirmed_balance",
evidence_strength: "strong",
balance_confirmed: true
}
});
});
it("supports downgraded confirmed-balance semantics", () => {
expect(buildFactualSummaryReply("Нет подтвержденных строк", buildConfirmedBalanceSemantics("medium", false)).semantics).toEqual({
result_mode: "confirmed_balance",
evidence_strength: "medium",
balance_confirmed: false
});
});
it("builds heuristic candidate semantics from candidate presence", () => {
expect(buildHeuristicCandidatesSemantics(true)).toEqual({
result_mode: "heuristic_candidates",
evidence_strength: "medium",
balance_confirmed: false
});
expect(buildHeuristicCandidatesSemantics(false)).toEqual({
result_mode: "heuristic_candidates",
evidence_strength: "weak",
balance_confirmed: false
});
});
});