АРЧ АП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
});
});
});
@@ -1,4 +1,49 @@
[
{
"generation_id": "gen-ag04171508-760111",
"created_at": "2026-04-17T15:08:06+00:00",
"mode": "saved_user_sessions",
"title": "AGENT replay for inventory clarification continuity and answer-shape cleanliness",
"count": 8,
"domain": "inventory_answer_shape_and_continuity",
"questions": [
"какие остатки на складе на март 2021",
"давай по Альтернативе Плюс",
"тогда покажи остатки на март 2021",
"По выбранному объекту \"Столешница 600*3050*26 альмандин\": кто нам это поставил?",
"а по этой позиции когда была закупка?",
"покажи документы по этой позиции",
"покажи еще раз остатки на эту же дату",
"а что мы уже выяснили по этой позиции?"
],
"generated_by": "codex_agent",
"saved_case_set_file": "assistant_autogen_saved_user_sessions_20260417150806_gen-ag04171508-760111.json",
"context": {
"llm_provider": null,
"model": null,
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"autogen_personality_id": null,
"autogen_personality_prompt": null,
"source_session_id": null,
"saved_session_file": "assistant_saved_session_20260417150806_gen-ag04171508-760111.json",
"saved_case_set_kind": "agent_semantic_scenario",
"agent_run": true,
"agent_focus": "Targeted AGENT replay for the recent compose/inventory fixes: company clarification, inventory root restore, selected-object provenance, purchase date/documents follow-ups, and protection against technical garbage in user-facing replies.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase4_inventory_answer_shape_continuity.json",
"scenario_id": "address_truth_harness_phase4_inventory_answer_shape_continuity",
"semantic_tags": [
"company_clarification",
"company_selection",
"inventory_root",
"meta_memory",
"meta_scope"
],
"latest_acceptance": null
}
},
{
"generation_id": "gen-ag04171326-15a132",
"created_at": "2026-04-17T13:26:00+00:00",
@@ -55,7 +100,8 @@
"selected_object_documents",
"selected_object_supplier",
"settlements_receivables"
]
],
"latest_acceptance": null
}
},
{
@@ -109,7 +155,8 @@
"selected_object_documents",
"selected_object_supplier",
"settlements_receivables"
]
],
"latest_acceptance": null
}
},
{
@@ -151,7 +198,10 @@
"agent_run": true,
"agent_focus": "mixed documents meta and cross-domain replay for turnaround 11",
"architecture_phase": "turnaround_11_phase7",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase7_meta_domain_mix.json"
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase7_meta_domain_mix.json",
"scenario_id": null,
"semantic_tags": null,
"latest_acceptance": null
}
},
{
@@ -193,7 +243,10 @@
"agent_run": true,
"agent_focus": "mixed documents meta and cross-domain replay for turnaround 11",
"architecture_phase": "turnaround_11_phase7",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase7_meta_domain_mix.json"
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase7_meta_domain_mix.json",
"scenario_id": null,
"semantic_tags": null,
"latest_acceptance": null
}
},
{
@@ -227,7 +280,10 @@
"agent_run": true,
"agent_focus": "scenario acceptance gate over root selected-object restore and human meta",
"architecture_phase": "turnaround_11_phase7",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase7_acceptance_gate_mix.json"
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase7_acceptance_gate_mix.json",
"scenario_id": null,
"semantic_tags": null,
"latest_acceptance": null
}
},
{
@@ -260,7 +316,10 @@
"agent_run": true,
"agent_focus": "provider runtime axis hardening across chat meta and address boundaries",
"architecture_phase": "turnaround_11_phase6",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase6_provider_axis_mix.json"
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase6_provider_axis_mix.json",
"scenario_id": null,
"semantic_tags": null,
"latest_acceptance": null
}
},
{
@@ -294,7 +353,10 @@
"agent_run": true,
"agent_focus": "meta and memory recap replay over interrupted address context",
"architecture_phase": "turnaround_11_phase5",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase5_meta_memory_mix.json"
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase5_meta_memory_mix.json",
"scenario_id": null,
"semantic_tags": null,
"latest_acceptance": null
}
},
{
@@ -327,7 +389,10 @@
"agent_run": true,
"agent_focus": "coverage/evidence contract on factual, fallback, and root-reset branches",
"architecture_phase": "turnaround_11_phase4",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase4_coverage_evidence_mix.json"
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase4_coverage_evidence_mix.json",
"scenario_id": null,
"semantic_tags": null,
"latest_acceptance": null
}
},
{
@@ -364,7 +429,14 @@
"autogen_personality_prompt": null,
"source_session_id": "asst-kBU5iS0mEt",
"saved_session_file": "assistant_saved_session_20260417070448_gen-mo2kcds2-tlqmvng.json",
"saved_case_set_kind": "assistant_session_scenario"
"saved_case_set_kind": "assistant_session_scenario",
"agent_run": null,
"agent_focus": null,
"architecture_phase": null,
"source_spec_file": null,
"scenario_id": null,
"semantic_tags": null,
"latest_acceptance": null
}
},
{
@@ -372,7 +444,7 @@
"created_at": "2026-04-16T18:26:26.191Z",
"mode": "saved_user_sessions",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
"count": 31,
"count": 30,
"domain": null,
"questions": [
"приветик - че как там дела",
@@ -402,7 +474,6 @@
"что нам отгружать чепурнов? какой товар или услугу?",
"какие остатки на складе на сегодня",
"остатки на март 2016",
"это по общей базе уже нужен вывод не по чепурнову",
"хвосты покажи по счету 60 на август 2022",
"Есть ли остатки товара, которые закупались очень давно",
"Какие конкретно номенклатуры формируют остаток по складу на май 2020"
@@ -419,7 +490,14 @@
"autogen_personality_prompt": null,
"source_session_id": "asst-NhPaZfbRYr",
"saved_session_file": "assistant_saved_session_20260416182626_gen-mo1t93wq-jy0453e.json",
"saved_case_set_kind": "assistant_session_scenario"
"saved_case_set_kind": "assistant_session_scenario",
"agent_run": null,
"agent_focus": null,
"architecture_phase": null,
"source_spec_file": null,
"scenario_id": null,
"semantic_tags": null,
"latest_acceptance": null
}
},
{
@@ -444,7 +522,14 @@
"autogen_personality_prompt": null,
"source_session_id": "asst-iIpNheKZpP",
"saved_session_file": "assistant_saved_session_20260416175150_gen-mo1s0m9z-ndf56a3.json",
"saved_case_set_kind": "assistant_session_scenario"
"saved_case_set_kind": "assistant_session_scenario",
"agent_run": null,
"agent_focus": null,
"architecture_phase": null,
"source_spec_file": null,
"scenario_id": null,
"semantic_tags": null,
"latest_acceptance": null
}
},
{
@@ -483,7 +568,14 @@
"autogen_personality_prompt": "Генерируй реалистичные живые вопросы бухгалтера по 1С. Добавляй разговорные формулировки и опечатки, но сохраняй бизнес-смысл. акцент на контрагентов, долги нсд, счета, общий вывод по компании - контрагенты, заказчикам, скока денег кто принес и какие остатки по счетам, поиск документов, сальдо, банковские операции, незакрытые договора, документы по договорам, долги, Активность заказчиков по периодам, Поставщики и выплаты",
"source_session_id": null,
"saved_session_file": null,
"saved_case_set_kind": null
"saved_case_set_kind": null,
"agent_run": null,
"agent_focus": null,
"architecture_phase": null,
"source_spec_file": null,
"scenario_id": null,
"semantic_tags": null,
"latest_acceptance": null
}
},
{
@@ -522,7 +614,14 @@
"autogen_personality_prompt": "Генерируй реалистичные живые вопросы бухгалтера по 1С. Добавляй разговорные формулировки и опечатки, но сохраняй бизнес-смысл. акцент на контрагентов, долги нсд, счета, общий вывод по компании - контрагенты, заказчикам, скока денег кто принес и какие остатки по счетам, поиск документов, сальдо, банковские операции, незакрытые договора, документы по договорам, долги, Активность заказчиков по периодам, Поставщики и выплаты",
"source_session_id": null,
"saved_session_file": null,
"saved_case_set_kind": null
"saved_case_set_kind": null,
"agent_run": null,
"agent_focus": null,
"architecture_phase": null,
"source_spec_file": null,
"scenario_id": null,
"semantic_tags": null,
"latest_acceptance": null
}
},
{
@@ -556,7 +655,14 @@
"autogen_personality_prompt": "Генерируй реалистичные живые вопросы бухгалтера по 1С. Добавляй разговорные формулировки и опечатки, но сохраняй бизнес-смысл. акцент на контрагентов, долги нсд, счета, общий вывод по компании - контрагенты, заказчикам, скока денег кто принес и какие остатки по счетам, поиск документов, сальдо, банковские операции, незакрытые договора, документы по договорам, долги, Активность заказчиков по периодам, Поставщики и выплаты",
"source_session_id": null,
"saved_session_file": null,
"saved_case_set_kind": null
"saved_case_set_kind": null,
"agent_run": null,
"agent_focus": null,
"architecture_phase": null,
"source_spec_file": null,
"scenario_id": null,
"semantic_tags": null,
"latest_acceptance": null
}
},
{
@@ -590,7 +696,14 @@
"autogen_personality_prompt": "Генерируй реалистичные живые вопросы бухгалтера по 1С. Добавляй разговорные формулировки и опечатки, но сохраняй бизнес-смысл. акцент на контрагентов, долги нсд, счета, общий вывод по компании - контрагенты, заказчикам, скока денег кто принес и какие остатки по счетам, поиск документов, сальдо, банковские операции, незакрытые договора, документы по договорам, долги, Активность заказчиков по периодам, Поставщики и выплаты",
"source_session_id": null,
"saved_session_file": null,
"saved_case_set_kind": null
"saved_case_set_kind": null,
"agent_run": null,
"agent_focus": null,
"architecture_phase": null,
"source_spec_file": null,
"scenario_id": null,
"semantic_tags": null,
"latest_acceptance": null
}
},
{
@@ -634,7 +747,14 @@
"autogen_personality_prompt": "Генерируй реалистичные живые вопросы бухгалтера по 1С. Добавляй разговорные формулировки и опечатки, но сохраняй бизнес-смысл. акцент на контрагентов \\ нсд \\ счета \\ общий ваывод по компании - контрагенты заказчиким скока денег кто принес и тп",
"source_session_id": null,
"saved_session_file": null,
"saved_case_set_kind": null
"saved_case_set_kind": null,
"agent_run": null,
"agent_focus": null,
"architecture_phase": null,
"source_spec_file": null,
"scenario_id": null,
"semantic_tags": null,
"latest_acceptance": null
}
},
{
@@ -678,7 +798,14 @@
"autogen_personality_prompt": "Генерируй реалистичные живые вопросы бухгалтера по 1С. Добавляй разговорные формулировки и опечатки, но сохраняй бизнес-смысл. акцент на контрагентов \\ нсд \\ счета \\ общий ваывод по компании - контрагенты заказчиким скока денег кто принес и тп",
"source_session_id": null,
"saved_session_file": null,
"saved_case_set_kind": null
"saved_case_set_kind": null,
"agent_run": null,
"agent_focus": null,
"architecture_phase": null,
"source_spec_file": null,
"scenario_id": null,
"semantic_tags": null,
"latest_acceptance": null
}
},
{
@@ -707,7 +834,14 @@
"autogen_personality_prompt": "Генерируй реалистичные живые вопросы бухгалтера по 1С. Добавляй разговорные формулировки и опечатки, но сохраняй бизнес-смысл.",
"source_session_id": null,
"saved_session_file": null,
"saved_case_set_kind": null
"saved_case_set_kind": null,
"agent_run": null,
"agent_focus": null,
"architecture_phase": null,
"source_spec_file": null,
"scenario_id": null,
"semantic_tags": null,
"latest_acceptance": null
}
},
{
@@ -736,7 +870,14 @@
"autogen_personality_prompt": "Генерируй реалистичные живые вопросы бухгалтера по 1С. Добавляй разговорные формулировки и опечатки, но сохраняй бизнес-смысл.",
"source_session_id": null,
"saved_session_file": null,
"saved_case_set_kind": null
"saved_case_set_kind": null,
"agent_run": null,
"agent_focus": null,
"architecture_phase": null,
"source_spec_file": null,
"scenario_id": null,
"semantic_tags": null,
"latest_acceptance": null
}
},
{
@@ -770,7 +911,14 @@
"autogen_personality_prompt": "Генерируй реалистичные живые вопросы бухгалтера по 1С. Добавляй разговорные формулировки и опечатки, но сохраняй бизнес-смысл.",
"source_session_id": null,
"saved_session_file": null,
"saved_case_set_kind": null
"saved_case_set_kind": null,
"agent_run": null,
"agent_focus": null,
"architecture_phase": null,
"source_spec_file": null,
"scenario_id": null,
"semantic_tags": null,
"latest_acceptance": null
}
},
{
@@ -804,7 +952,14 @@
"autogen_personality_prompt": "Генерируй реалистичные живые вопросы бухгалтера по 1С. Добавляй разговорные формулировки и опечатки, но сохраняй бизнес-смысл.",
"source_session_id": null,
"saved_session_file": null,
"saved_case_set_kind": null
"saved_case_set_kind": null,
"agent_run": null,
"agent_focus": null,
"architecture_phase": null,
"source_spec_file": null,
"scenario_id": null,
"semantic_tags": null,
"latest_acceptance": null
}
},
{
@@ -848,7 +1003,14 @@
"autogen_personality_prompt": "Генерируй реалистичные живые вопросы бухгалтера по 1С. Добавляй разговорные формулировки и опечатки, но сохраняй бизнес-смысл.",
"source_session_id": null,
"saved_session_file": null,
"saved_case_set_kind": null
"saved_case_set_kind": null,
"agent_run": null,
"agent_focus": null,
"architecture_phase": null,
"source_spec_file": null,
"scenario_id": null,
"semantic_tags": null,
"latest_acceptance": null
}
},
{
@@ -874,7 +1036,14 @@
"autogen_personality_prompt": "????????? ??????? ??????? ?? ????????.",
"source_session_id": null,
"saved_session_file": null,
"saved_case_set_kind": null
"saved_case_set_kind": null,
"agent_run": null,
"agent_focus": null,
"architecture_phase": null,
"source_spec_file": null,
"scenario_id": null,
"semantic_tags": null,
"latest_acceptance": null
}
}
]
]
@@ -0,0 +1,129 @@
{
"saved_at": "2026-04-17T15:08:06+00:00",
"generation_id": "gen-ag04171508-760111",
"mode": "saved_user_sessions",
"title": "AGENT replay for inventory clarification continuity and answer-shape cleanliness",
"agent_run": true,
"questions": [
"какие остатки на складе на март 2021",
"давай по Альтернативе Плюс",
"тогда покажи остатки на март 2021",
"По выбранному объекту \"Столешница 600*3050*26 альмандин\": кто нам это поставил?",
"а по этой позиции когда была закупка?",
"покажи документы по этой позиции",
"покажи еще раз остатки на эту же дату",
"а что мы уже выяснили по этой позиции?"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Targeted AGENT replay for the recent compose/inventory fixes: company clarification, inventory root restore, selected-object provenance, purchase date/documents follow-ups, and protection against technical garbage in user-facing replies.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase4_inventory_answer_shape_continuity.json",
"scenario_id": "address_truth_harness_phase4_inventory_answer_shape_continuity",
"semantic_tags": [
"company_clarification",
"company_selection",
"inventory_root",
"meta_memory",
"meta_scope"
]
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "какие остатки на складе на март 2021",
"created_at": "2026-04-17T15:08:06+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "давай по Альтернативе Плюс",
"created_at": "2026-04-17T15:08:06+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "тогда покажи остатки на март 2021",
"created_at": "2026-04-17T15:08:06+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "По выбранному объекту \"Столешница 600*3050*26 альмандин\": кто нам это поставил?",
"created_at": "2026-04-17T15:08:06+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-005",
"role": "user",
"text": "а по этой позиции когда была закупка?",
"created_at": "2026-04-17T15:08:06+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-006",
"role": "user",
"text": "покажи документы по этой позиции",
"created_at": "2026-04-17T15:08:06+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-007",
"role": "user",
"text": "покажи еще раз остатки на эту же дату",
"created_at": "2026-04-17T15:08:06+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-008",
"role": "user",
"text": "а что мы уже выяснили по этой позиции?",
"created_at": "2026-04-17T15:08:06+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
}
],
"agent_run": true,
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Targeted AGENT replay for the recent compose/inventory fixes: company clarification, inventory root restore, selected-object provenance, purchase date/documents follow-ups, and protection against technical garbage in user-facing replies.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase4_inventory_answer_shape_continuity.json",
"scenario_id": "address_truth_harness_phase4_inventory_answer_shape_continuity",
"semantic_tags": [
"company_clarification",
"company_selection",
"inventory_root",
"meta_memory",
"meta_scope"
]
}
}
}
@@ -2,7 +2,7 @@
"suite_id": "assistant_saved_session_gen-mo1t93wq-jy0453e",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-04-16T18:26:26.186Z",
"generated_at": "2026-04-17T15:22:18.956Z",
"generation_id": "gen-mo1t93wq-jy0453e",
"mode": "saved_user_sessions",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
@@ -99,9 +99,6 @@
{
"user_message": "остатки на март 2016"
},
{
"user_message": "это по общей базе уже нужен вывод не по чепурнову"
},
{
"user_message": "хвосты покажи по счету 60 на август 2022"
},
@@ -0,0 +1,49 @@
{
"suite_id": "assistant_saved_session_gen-ag04171508-760111",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-04-17T15:08:06+00:00",
"generation_id": "gen-ag04171508-760111",
"mode": "saved_user_sessions",
"title": "AGENT replay for inventory clarification continuity and answer-shape cleanliness",
"domain": "inventory_answer_shape_and_continuity",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT replay for inventory clarification continuity and answer-shape cleanliness",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "какие остатки на складе на март 2021"
},
{
"user_message": "давай по Альтернативе Плюс"
},
{
"user_message": "тогда покажи остатки на март 2021"
},
{
"user_message": "По выбранному объекту \"Столешница 600*3050*26 альмандин\": кто нам это поставил?"
},
{
"user_message": "а по этой позиции когда была закупка?"
},
{
"user_message": "покажи документы по этой позиции"
},
{
"user_message": "покажи еще раз остатки на эту же дату"
},
{
"user_message": "а что мы уже выяснили по этой позиции?"
}
]
}
]
}
@@ -0,0 +1,45 @@
{
"suite_id": "assistant_saved_session_runtime_job-XDhN0VgANV",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_runtime_v0_1",
"title": "AGENT replay for inventory clarification continuity and answer-shape cleanliness",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "saved_user_sessions_runtime",
"title": "AGENT replay for inventory clarification continuity and answer-shape cleanliness",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "какие остатки на складе на март 2021"
},
{
"user_message": "давай по Альтернативе Плюс"
},
{
"user_message": "тогда покажи остатки на март 2021"
},
{
"user_message": "По выбранному объекту \"Столешница 600*3050*26 альмандин\": кто нам это поставил?"
},
{
"user_message": "а по этой позиции когда была закупка?"
},
{
"user_message": "покажи документы по этой позиции"
},
{
"user_message": "покажи еще раз остатки на эту же дату"
},
{
"user_message": "а что мы уже выяснили по этой позиции?"
}
]
}
]
}
@@ -0,0 +1,114 @@
{
"suite_id": "assistant_saved_session_runtime_job-az4ZDEQptK",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_runtime_v0_1",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "saved_user_sessions_runtime",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "приветик - че как там дела"
},
{
"user_message": "расскажи что можешь интересного"
},
{
"user_message": "кайф - что там на складе по остаткам?"
},
{
"user_message": "а исторические остатки на другие даты умеешь?"
},
{
"user_message": "давай на июль 2017"
},
{
"user_message": "март 2016"
},
{
"user_message": "По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?"
},
{
"user_message": "а кому продали?"
},
{
"user_message": "у тебя написано кто контрагент: рабочая станция - это ошибка?"
},
{
"user_message": "ндс можешь прикинуть на дату покупки рабочей станции?"
},
{
"user_message": "а какой ндс мы должны сгрузить на март 2020?"
},
{
"user_message": "прикинь какой ндс нам надо заплатить на февраль 2017"
},
{
"user_message": "кто у нас самый доходный клиент за все время"
},
{
"user_message": "кто нам должен денег на май 2017"
},
{
"user_message": "а какой ндс мы должны примерно заплатить за этот период?"
},
{
"user_message": "мы должны комуто денег на сегодня?"
},
{
"user_message": "а нам?"
},
{
"user_message": "какой у нас самый доходный год"
},
{
"user_message": "а за 2017 мы скок заработали?"
},
{
"user_message": "сколько вообще денег мы заработали за все время?"
},
{
"user_message": "ты умеешь считать дельту по договорам?"
},
{
"user_message": "по чепурнову покажи все доки"
},
{
"user_message": "а по свк"
},
{
"user_message": "а сейчас у нас есть что на складе?"
},
{
"user_message": "что нам отгружать чепурнов? какой товар или услугу?"
},
{
"user_message": "какие остатки на складе на сегодня"
},
{
"user_message": "остатки на март 2016"
},
{
"user_message": "это по общей базе уже нужен вывод не по чепурнову"
},
{
"user_message": "хвосты покажи по счету 60 на август 2022"
},
{
"user_message": "Есть ли остатки товара, которые закупались очень давно"
},
{
"user_message": "Какие конкретно номенклатуры формируют остаток по складу на май 2020"
}
]
}
]
}