ДОМЕНЫ - ВОПРОСЫ - СКЛАД - Склад: усилить follow-up оркестрацию и business-first формат ответов
This commit is contained in:
@@ -1642,7 +1642,9 @@ function hasInventoryProvenanceSignalV2(text: string): boolean {
|
||||
|
||||
function hasInventoryPurchaseDateSignal(text: string): boolean {
|
||||
const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text);
|
||||
const hasPurchaseDateCue = /(?:когда\s+был\s+куплен|когда\s+куплен|дата\s+закупк|purchase\s+date)/iu.test(text);
|
||||
const hasPurchaseDateCue = /(?:когда\s+(?:примерно\s+)?(?:мы\s+)?купили|когда\s+был\s+куплен|когда\s+куплен|дата\s+закупк|purchase\s+date)/iu.test(
|
||||
text
|
||||
);
|
||||
return hasItemCue && hasPurchaseDateCue;
|
||||
}
|
||||
|
||||
@@ -1656,9 +1658,10 @@ function hasInventoryPurchaseDocumentsSignalV2(text: string): boolean {
|
||||
|
||||
function hasInventorySaleTraceSignalV2(text: string): boolean {
|
||||
const hasItemCue = /(?:товар|номенклатур|sku|item|product)/iu.test(text);
|
||||
const hasTraceCue = /(?:кому\s+был\s+продан|кто\s+купил|buyer|sale\s+trace|trace\s+of\s+sale|через\s+какие\s+документы\s+прош[её]л\s+путь\s+товара|закупк.*склад.*продаж|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*warehouse\s*->\s*sale|purchase\s*->\s*stock\s*->\s*sale)/iu.test(
|
||||
text
|
||||
);
|
||||
const hasTraceCue =
|
||||
/(?:кому\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали|кому\s+был\s+продан|кто\s+купил|buyer|sale\s+trace|trace\s+of\s+sale|через\s+какие\s+документы\s+прош[её]л\s+путь\s+товара|закупк.*склад.*продаж|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*warehouse\s*->\s*sale|purchase\s*->\s*stock\s*->\s*sale)/iu.test(
|
||||
text
|
||||
);
|
||||
return hasItemCue && hasTraceCue;
|
||||
}
|
||||
|
||||
|
||||
@@ -216,7 +216,11 @@ function resolveNavigationAction(debug: Record<string, unknown>, hasFocusObject:
|
||||
}
|
||||
|
||||
function buildFocusObjectFromDebug(debug: Record<string, unknown>, resultSetId: string, createdAt: string): AddressFocusObject | null {
|
||||
const rawValue = toNonEmptyString(debug.anchor_value_resolved) ?? toNonEmptyString(debug.anchor_value_raw);
|
||||
const extractedFilters = toObject(debug.extracted_filters) ?? {};
|
||||
const rawValue =
|
||||
toNonEmptyString(debug.anchor_value_resolved) ??
|
||||
toNonEmptyString(debug.anchor_value_raw) ??
|
||||
toNonEmptyString(extractedFilters.item);
|
||||
if (!rawValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -369,6 +369,19 @@ function normalizeQuestionText(value: string | null | undefined): string {
|
||||
.trim();
|
||||
}
|
||||
|
||||
function hasInventoryPurchaseDateActionFocus(userMessage: string | null | undefined): boolean {
|
||||
const text = normalizeQuestionText(userMessage);
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
if (/^(?:когда|когда\?|дата\s+закупки|purchase\s+date)\??$/iu.test(text)) {
|
||||
return true;
|
||||
}
|
||||
return /(?:когда\s+(?:примерно\s+)?(?:мы\s+)?(?:купили|был\s+куплен|куплен|это\s+купили|эту\s+позицию\s+купили|ее\s+купили)|дата\s+закупки|purchase\s+date)/iu.test(
|
||||
text
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeIsoDateOnly(value: string | null | undefined): string | null {
|
||||
const parsed = parseIsoDateToken(value);
|
||||
if (!parsed) {
|
||||
@@ -3967,30 +3980,14 @@ export function composeFactualReply(
|
||||
);
|
||||
const totalQuantity = positions.reduce((sum, item) => sum + item.quantity, 0);
|
||||
const totalAmount = positions.reduce((sum, item) => sum + item.amount, 0);
|
||||
|
||||
const lines: string[] = [
|
||||
`Собран подтвержденный срез товаров на складах на ${formatDateRu(asOfDate)}.`,
|
||||
"",
|
||||
"Блок 1. Статус результата",
|
||||
"- Результат: подтвержденный список товарных остатков на дату.",
|
||||
"",
|
||||
"Блок 2. Что учтено",
|
||||
`- Дата среза: ${formatDateRu(asOfDate)}.`,
|
||||
"- Контур: остатки по счету 41.01 «Товары на складах».",
|
||||
"- Базовая единица детализации: одна строка = товар, склад и организация на дату.",
|
||||
"",
|
||||
"Блок 3. Сводка",
|
||||
`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`,
|
||||
`- Позиции с ненулевым остатком: ${formatNumberWithDots(positions.length)}.`,
|
||||
`- Уникальных товаров: ${formatNumberWithDots(uniqueItems.length)}.`,
|
||||
`- Уникальных складов: ${formatNumberWithDots(uniqueWarehouses.length)}.`,
|
||||
`- Суммарное количество: ${formatNumberWithDots(totalQuantity, 3)}.`,
|
||||
`- Суммарная стоимость: ${formatMoneyRub(totalAmount)}.`,
|
||||
"",
|
||||
"Блок 4. Подтвержденные позиции"
|
||||
];
|
||||
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 ?? "склад не определен";
|
||||
@@ -4001,7 +3998,20 @@ export function composeFactualReply(
|
||||
})
|
||||
);
|
||||
} else {
|
||||
lines.push("- На дату среза товары с ненулевым остатком по счету 41.01 не найдены.");
|
||||
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 {
|
||||
@@ -4021,28 +4031,19 @@ export function composeFactualReply(
|
||||
const summary = summarizeInventoryTraceRows(purchaseRows);
|
||||
const itemLabel = summary.item ?? "товар не определен";
|
||||
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,
|
||||
`Собран подтвержденный список документов поступления по товару ${itemLabel} до ${formatDateRu(asOfDate)}.`,
|
||||
"",
|
||||
"Блок 1. Статус результата",
|
||||
"- Результат: подтвержденные движения поступления товара на 41.01 по доступным бухгалтерским проводкам.",
|
||||
"",
|
||||
"Блок 2. Что учтено",
|
||||
`- Дата верхней границы: ${formatDateRu(asOfDate)}.`,
|
||||
"- Контур: движения, где товар поступает на счет 41.01.",
|
||||
`- Документов в выборке: ${formatNumberWithDots(summary.documents.length)}.`,
|
||||
`- Операций в выборке: ${formatNumberWithDots(purchaseRows.length)}.`
|
||||
];
|
||||
if (summary.counterparties.length > 0) {
|
||||
lines.push(`- Найденные контрагенты в закупочных движениях: ${summary.counterparties.slice(0, 3).join("; ")}.`);
|
||||
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("", "Блок 3. Документы");
|
||||
lines.push("", "Документы:");
|
||||
if (purchaseRows.length > 0) {
|
||||
lines.push(...formatInventoryTraceRows(purchaseRows, 12));
|
||||
} else {
|
||||
@@ -4064,26 +4065,56 @@ export function composeFactualReply(
|
||||
const purchaseRows = rows.filter((row) => isInventoryPurchaseMovement(row));
|
||||
const summary = summarizeInventoryTraceRows(purchaseRows);
|
||||
const itemLabel = summary.item ?? "товар не определен";
|
||||
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}.`
|
||||
: `По позиции ${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 (summary.firstPeriod && asOfDate && summary.firstPeriod < asOfDate) {
|
||||
lines.push(`- Дата вопроса по остатку: ${formatDateRu(asOfDate)}; дата закупки показана по подтвержденному закупочному следу.`);
|
||||
}
|
||||
}
|
||||
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 =
|
||||
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,
|
||||
`Собран подтвержденный закупочный след по товару ${itemLabel} до ${formatDateRu(asOfDate)}.`,
|
||||
"",
|
||||
"Блок 1. Статус результата",
|
||||
"- Результат: показаны подтвержденные закупочные движения на 41.01 по выбранному товару.",
|
||||
"- Важно: без партионности этот контур не подменяет собой лот-level доказательство происхождения текущего остатка.",
|
||||
"",
|
||||
"Блок 2. Сводка",
|
||||
`- Первая найденная дата закупочного движения: ${inventoryTraceDateLabel(summary.firstPeriod)}.`,
|
||||
`- Последняя найденная дата закупочного движения: ${inventoryTraceDateLabel(summary.lastPeriod)}.`,
|
||||
`- Документов поступления: ${formatNumberWithDots(summary.documents.length)}.`,
|
||||
`- Операций поступления: ${formatNumberWithDots(purchaseRows.length)}.`
|
||||
];
|
||||
const lines: string[] = [directAnswerLine, "", "Подтверждение:"];
|
||||
lines.push(`- Первая найденная дата закупки: ${inventoryTraceDateLabel(summary.firstPeriod)}.`);
|
||||
lines.push(`- Последняя найденная дата закупки: ${inventoryTraceDateLabel(summary.lastPeriod)}.`);
|
||||
lines.push(`- Документов поступления: ${formatNumberWithDots(summary.documents.length)}.`);
|
||||
lines.push(`- Операций поступления: ${formatNumberWithDots(purchaseRows.length)}.`);
|
||||
if (summary.counterparties.length === 1) {
|
||||
lines.push(`- По доступным закупочным движениям товар связан с поставщиком: ${summary.counterparties[0]}.`);
|
||||
} else if (summary.counterparties.length > 1) {
|
||||
@@ -4092,7 +4123,14 @@ export function composeFactualReply(
|
||||
lines.push("- Закупочные документы найдены, но поставщик не материализован отдельным полем в текущем exact-контуре.");
|
||||
}
|
||||
if (summary.documents.length > 0) {
|
||||
lines.push("", "Блок 3. Опорные документы", ...formatInventoryTraceRows(purchaseRows, 8));
|
||||
lines.push("", "Опорные документы:", ...formatInventoryTraceRows(purchaseRows, 8));
|
||||
}
|
||||
if (purchaseRows.length > 0) {
|
||||
lines.push(
|
||||
"",
|
||||
"Сервисно:",
|
||||
"- Без партионности этот контур показывает документально наблюдаемый закупочный след, а не лот-level происхождение текущего остатка."
|
||||
);
|
||||
}
|
||||
return {
|
||||
responseType: purchaseRows.length > 0 ? "FACTUAL_SUMMARY" : "FACTUAL_SUMMARY",
|
||||
@@ -4221,19 +4259,11 @@ export function composeFactualReply(
|
||||
: summary.counterparties.length > 1
|
||||
? `По товару ${itemLabel} найдено несколько покупателей: ${summary.counterparties.slice(0, 4).join("; ")}.`
|
||||
: `По товару ${itemLabel} покупатель в текущем exact-контуре не материализован.`;
|
||||
const lines: string[] = [
|
||||
directAnswerLine,
|
||||
`Собран подтвержденный след выбытия по товару ${itemLabel} до ${formatDateRu(asOfDate)}.`,
|
||||
"",
|
||||
"Блок 1. Статус результата",
|
||||
"- Результат: показаны подтвержденные движения выбытия товара со счета 41.01.",
|
||||
"",
|
||||
"Блок 2. Сводка",
|
||||
`- Первая найденная дата выбытия: ${inventoryTraceDateLabel(summary.firstPeriod)}.`,
|
||||
`- Последняя найденная дата выбытия: ${inventoryTraceDateLabel(summary.lastPeriod)}.`,
|
||||
`- Документов выбытия: ${formatNumberWithDots(summary.documents.length)}.`,
|
||||
`- Операций выбытия: ${formatNumberWithDots(saleRows.length)}.`
|
||||
];
|
||||
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) {
|
||||
@@ -4241,7 +4271,7 @@ export function composeFactualReply(
|
||||
} else if (saleRows.length > 0) {
|
||||
lines.push("- Документы выбытия найдены, но покупатель не материализован отдельным полем в текущем exact-контуре.");
|
||||
}
|
||||
lines.push("", "Блок 3. Документы выбытия");
|
||||
lines.push("", "Документы выбытия:");
|
||||
if (saleRows.length > 0) {
|
||||
lines.push(...formatInventoryTraceRows(saleRows, 12));
|
||||
} else {
|
||||
@@ -4269,14 +4299,9 @@ export function composeFactualReply(
|
||||
purchaseSummary.counterparties.length === 1 && saleSummary.counterparties.length === 1
|
||||
? `По товару ${itemLabel} цепочка поставки и продажи связана с поставщиком ${purchaseSummary.counterparties[0]} и покупателем ${saleSummary.counterparties[0]}.`
|
||||
: `По товару ${itemLabel} цепочка поставки и продажи подтверждена частично или разнообразно: детали идут следом.`;
|
||||
const lines: string[] = [
|
||||
directAnswerLine,
|
||||
`Собрана документальная цепочка по товару ${itemLabel} до ${formatDateRu(asOfDate)}.`,
|
||||
"",
|
||||
"Блок 1. Статус результата",
|
||||
`- Закупочных движений на 41.01: ${formatNumberWithDots(purchaseRows.length)}.`,
|
||||
`- Движений выбытия со счета 41.01: ${formatNumberWithDots(saleRows.length)}.`
|
||||
];
|
||||
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) {
|
||||
@@ -4289,7 +4314,7 @@ export function composeFactualReply(
|
||||
if (purchaseRows.length > 0) {
|
||||
lines.push(
|
||||
"",
|
||||
"Блок 2. Закупка",
|
||||
"Закупка:",
|
||||
`- Первая дата: ${inventoryTraceDateLabel(purchaseSummary.firstPeriod)}.`,
|
||||
`- Последняя дата: ${inventoryTraceDateLabel(purchaseSummary.lastPeriod)}.`,
|
||||
...formatInventoryTraceRows(purchaseRows, 6)
|
||||
@@ -4298,7 +4323,7 @@ export function composeFactualReply(
|
||||
if (saleRows.length > 0) {
|
||||
lines.push(
|
||||
"",
|
||||
"Блок 3. Выбытие",
|
||||
"Продажа:",
|
||||
`- Первая дата: ${inventoryTraceDateLabel(saleSummary.firstPeriod)}.`,
|
||||
`- Последняя дата: ${inventoryTraceDateLabel(saleSummary.lastPeriod)}.`,
|
||||
...formatInventoryTraceRows(saleRows, 6)
|
||||
|
||||
@@ -334,6 +334,32 @@ function hasInventoryPurchaseDocumentsFollowupCue(text: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function hasInventoryPurchaseDateFollowupCue(text: string): boolean {
|
||||
return /(?:когда\s+(?:примерно\s+)?(?:мы\s+)?купили|когда\s+был\s+куплен|когда\s+куплен|когда\s+это\s+купили|когда\s+эту\s+позицию\s+купили|когда\s+ее\s+купили|дата\s+закупки|purchase\s+date)/iu.test(
|
||||
String(text ?? "")
|
||||
);
|
||||
}
|
||||
|
||||
function hasBareInventoryPurchaseDateFollowupCue(text: string): boolean {
|
||||
const normalized = String(text ?? "").trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return /(?:^|\s)(?:когда|а\s+когда|ну\s+когда)(?=$|[\s,.;:!?])/iu.test(normalized) && normalized.split(/\s+/).filter(Boolean).length <= 3;
|
||||
}
|
||||
|
||||
function hasInventorySaleFollowupCue(text: string): boolean {
|
||||
return /(?:кому\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали|кому\s+был\s+продан|кому\s+дальше\s+продали|кто\s+купил|buyer|покупател)/iu.test(
|
||||
String(text ?? "")
|
||||
);
|
||||
}
|
||||
|
||||
function hasInventoryPurchaseToSaleChainFollowupCue(text: string): boolean {
|
||||
return /(?:через\s+какие\s+документы\s+прош[её]л\s+путь\s+товара|закупк.*склад.*продаж|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*(?:warehouse|stock)\s*->\s*sale)/iu.test(
|
||||
String(text ?? "")
|
||||
);
|
||||
}
|
||||
|
||||
export function hasAddressFollowupContextSignal(text: string): boolean {
|
||||
const normalized = String(text ?? "").trim();
|
||||
if (!normalized) {
|
||||
@@ -564,8 +590,10 @@ function mergeFollowupFilters(
|
||||
!toNonEmptyString(merged.item) &&
|
||||
previousItem
|
||||
) {
|
||||
merged.item = previousItem;
|
||||
reasons.push("item_from_followup_context");
|
||||
if (intent !== "inventory_aging_by_purchase_date") {
|
||||
merged.item = previousItem;
|
||||
reasons.push("item_from_followup_context");
|
||||
}
|
||||
}
|
||||
if (sameDateRequested) {
|
||||
const inheritedAsOfDate = previousAsOfDate ?? previousPeriodTo ?? previousPeriodFrom;
|
||||
@@ -576,7 +604,11 @@ function mergeFollowupFilters(
|
||||
}
|
||||
if (
|
||||
!sameDateRequested &&
|
||||
(intent === "inventory_sale_trace_for_item" || intent === "inventory_purchase_to_sale_chain") &&
|
||||
(intent === "inventory_purchase_provenance_for_item" ||
|
||||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date") &&
|
||||
!hasExplicitPeriodLiteral(userMessage) &&
|
||||
!hasExplicitCurrentDateHint(userMessage)
|
||||
) {
|
||||
@@ -589,6 +621,15 @@ function mergeFollowupFilters(
|
||||
reasons.push("as_of_date_from_followup_context");
|
||||
}
|
||||
}
|
||||
if (intent === "inventory_aging_by_purchase_date") {
|
||||
const explicitItemMention = /(?:^|[\s,.;:!?()\-\u2014])(?:товар(?:у|а|ом)?|позици(?:и|я|ю)|item|row|line)(?=$|[\s,.;:!?()\-\u2014])/iu.test(
|
||||
String(userMessage ?? "")
|
||||
);
|
||||
if (toNonEmptyString(merged.item) && !explicitItemMention) {
|
||||
delete merged.item;
|
||||
reasons.push("item_cleared_for_stock_slice_aging");
|
||||
}
|
||||
}
|
||||
if (
|
||||
!sameDateRequested &&
|
||||
hasFollowupSignalForConfirmed &&
|
||||
@@ -826,6 +867,64 @@ function deriveIntentWithFollowupContext(
|
||||
}
|
||||
}
|
||||
|
||||
if (inventorySelectedObjectFollowup && hasInventoryPurchaseDateFollowupCue(normalizedMessage)) {
|
||||
if (
|
||||
detectedIntent.intent === "unknown" ||
|
||||
detectedIntent.intent === "inventory_purchase_provenance_for_item" ||
|
||||
detectedIntent.intent === previousIntent ||
|
||||
detectedIntent.intent === "inventory_on_hand_as_of_date"
|
||||
) {
|
||||
return {
|
||||
intent: "inventory_purchase_provenance_for_item",
|
||||
confidence: "low",
|
||||
reasons: [...detectedIntent.reasons, "intent_adjusted_to_inventory_followup_context"]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (inventorySelectedObjectFollowup && hasInventorySaleFollowupCue(normalizedMessage)) {
|
||||
if (
|
||||
detectedIntent.intent === "unknown" ||
|
||||
detectedIntent.intent === "inventory_purchase_provenance_for_item" ||
|
||||
detectedIntent.intent === "inventory_on_hand_as_of_date" ||
|
||||
detectedIntent.intent === previousIntent
|
||||
) {
|
||||
return {
|
||||
intent: "inventory_sale_trace_for_item",
|
||||
confidence: "low",
|
||||
reasons: [...detectedIntent.reasons, "intent_adjusted_to_inventory_followup_context"]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (inventorySelectedObjectFollowup && hasInventoryPurchaseToSaleChainFollowupCue(normalizedMessage)) {
|
||||
if (
|
||||
detectedIntent.intent === "unknown" ||
|
||||
detectedIntent.intent === "inventory_sale_trace_for_item" ||
|
||||
detectedIntent.intent === "inventory_on_hand_as_of_date" ||
|
||||
detectedIntent.intent === previousIntent
|
||||
) {
|
||||
return {
|
||||
intent: "inventory_purchase_to_sale_chain",
|
||||
confidence: "low",
|
||||
reasons: [...detectedIntent.reasons, "intent_adjusted_to_inventory_followup_context"]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
previousIsInventoryFamily &&
|
||||
hasFollowupSignal &&
|
||||
hasBareInventoryPurchaseDateFollowupCue(normalizedMessage) &&
|
||||
(detectedIntent.intent === "unknown" || detectedIntent.intent === previousIntent)
|
||||
) {
|
||||
return {
|
||||
intent: "inventory_purchase_provenance_for_item",
|
||||
confidence: "low",
|
||||
reasons: [...detectedIntent.reasons, "intent_adjusted_to_inventory_followup_context"]
|
||||
};
|
||||
}
|
||||
|
||||
if (hasPreviousContract) {
|
||||
if (detectedIntent.intent === "list_contracts_by_counterparty") {
|
||||
if (hasBankSignal(normalizedMessage)) {
|
||||
|
||||
@@ -2120,6 +2120,9 @@ function readAddressFilterString(addressDebug, key) {
|
||||
}
|
||||
return toNonEmptyString(filters[key]);
|
||||
}
|
||||
function readAddressInventoryItemFilter(addressDebug) {
|
||||
return readAddressFilterString(addressDebug, "item");
|
||||
}
|
||||
function isAddressLaneDebugPayload(debug) {
|
||||
if (!debug || typeof debug !== "object") {
|
||||
return false;
|
||||
@@ -2473,6 +2476,7 @@ function buildAddressFollowupOffer(addressDebug) {
|
||||
const anchorType = toNonEmptyString(addressDebug.anchor_type);
|
||||
const anchorValue = toNonEmptyString(addressDebug.anchor_value_resolved) ??
|
||||
toNonEmptyString(addressDebug.anchor_value_raw) ??
|
||||
readAddressInventoryItemFilter(addressDebug) ??
|
||||
readAddressFilterString(addressDebug, "counterparty") ??
|
||||
readAddressFilterString(addressDebug, "contract") ??
|
||||
readAddressFilterString(addressDebug, "account");
|
||||
@@ -2646,6 +2650,26 @@ function hasShortDebtMirrorFollowupSignal(userMessage) {
|
||||
/^(?:р°|a|рё|i)\s+(?:рЅр°рј\s+)?рєс‚рѕ(?=$|[\s,.;:!?])/iu.test(sample) ||
|
||||
/^(?:р°|a|рё|i)\s+(?:рјс‹\s+)?рєрѕрјсѓ(?=$|[\s,.;:!?])/iu.test(sample));
|
||||
}
|
||||
function isInventorySelectedObjectIntent(intent) {
|
||||
return intent === "inventory_purchase_provenance_for_item" ||
|
||||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain";
|
||||
}
|
||||
function hasShortInventoryObjectFollowupSignal(userMessage) {
|
||||
const rawText = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
||||
const repairedText = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase());
|
||||
const samples = [rawText, repairedText].filter((item) => item.length > 0);
|
||||
if (samples.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const minTokens = samples.reduce((min, sample) => Math.min(min, countTokens(sample)), Number.POSITIVE_INFINITY);
|
||||
if (minTokens > 8) {
|
||||
return false;
|
||||
}
|
||||
return samples.some((sample) => /^(?:кто|когда|документы|сумма|поставщик|покупатель)(?:\?)?$/iu.test(sample) ||
|
||||
/^(?:когда\s+(?:примерно\s+)?купили(?:\s+ее)?|каким\s+документом|покажи\s+документы|по\s+каким\s+документам|все\s+закупки|все\s+поступления|кому\s+(?:мы\s+)?продали|кто\s+купил|цепочка|путь\s+товара)(?:\?)?$/iu.test(sample));
|
||||
}
|
||||
function resolveDebtRoleSwapFollowupIntent(userMessage, previousIntent) {
|
||||
const normalized = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
||||
if (!normalized || countTokens(normalized) > 10) {
|
||||
@@ -2676,14 +2700,18 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||
(isImplicitAddressContinuationByLlm(userMessage, llmPreDecomposeMeta) ||
|
||||
(toNonEmptyString(alternateMessage) ? isImplicitAddressContinuationByLlm(alternateMessage, llmPreDecomposeMeta) : false));
|
||||
const sourceIntentHint = toNonEmptyString(previousAddressDebug?.detected_intent);
|
||||
const inventoryShortFollowupPrimary = isInventorySelectedObjectIntent(sourceIntentHint) && hasShortInventoryObjectFollowupSignal(userMessage);
|
||||
const inventoryShortFollowupAlternate = isInventorySelectedObjectIntent(sourceIntentHint) && toNonEmptyString(alternateMessage)
|
||||
? hasShortInventoryObjectFollowupSignal(String(alternateMessage ?? ""))
|
||||
: false;
|
||||
const debtRoleSwapPrimary = sourceIntentHint ? resolveDebtRoleSwapFollowupIntent(userMessage, sourceIntentHint) : null;
|
||||
const debtRoleSwapAlternate = sourceIntentHint && toNonEmptyString(alternateMessage)
|
||||
? resolveDebtRoleSwapFollowupIntent(String(alternateMessage ?? ""), sourceIntentHint)
|
||||
: null;
|
||||
const debtRoleSwapIntent = debtRoleSwapPrimary ?? debtRoleSwapAlternate ?? null;
|
||||
const hasPrimaryFollowupSignal = hasAddressFollowupContextSignal(userMessage) || Boolean(debtRoleSwapPrimary);
|
||||
const hasPrimaryFollowupSignal = hasAddressFollowupContextSignal(userMessage) || Boolean(debtRoleSwapPrimary) || inventoryShortFollowupPrimary;
|
||||
const hasAlternateFollowupSignal = toNonEmptyString(alternateMessage)
|
||||
? hasAddressFollowupContextSignal(alternateMessage) || Boolean(debtRoleSwapAlternate)
|
||||
? hasAddressFollowupContextSignal(alternateMessage) || Boolean(debtRoleSwapAlternate) || inventoryShortFollowupAlternate
|
||||
: false;
|
||||
const hasPrimaryIndexReferenceSignal = extractDisplayedEntityIndexMention(userMessage) !== null;
|
||||
const hasAlternateIndexReferenceSignal = toNonEmptyString(alternateMessage)
|
||||
@@ -2723,6 +2751,7 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||
let previousAnchorType = toNonEmptyString(previousAddressDebug.anchor_type);
|
||||
let previousAnchor = toNonEmptyString(previousAddressDebug.anchor_value_resolved) ??
|
||||
toNonEmptyString(previousAddressDebug.anchor_value_raw) ??
|
||||
readAddressFilterString(previousAddressDebug, "item") ??
|
||||
readAddressFilterString(previousAddressDebug, "counterparty") ??
|
||||
readAddressFilterString(previousAddressDebug, "account") ??
|
||||
readAddressFilterString(previousAddressDebug, "contract");
|
||||
@@ -3924,6 +3953,12 @@ export function resolveAssistantOrchestrationDecision(input) {
|
||||
hasShortDebtMirrorFollowupSignal(repairedRawUserMessage) ||
|
||||
hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) ||
|
||||
hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage);
|
||||
const protectedInventoryShortFollowup = Boolean(followupContext &&
|
||||
isInventorySelectedObjectIntent(toNonEmptyString(followupContext.previous_intent)) &&
|
||||
(hasShortInventoryObjectFollowupSignal(rawUserMessage) ||
|
||||
hasShortInventoryObjectFollowupSignal(repairedRawUserMessage) ||
|
||||
hasShortInventoryObjectFollowupSignal(effectiveAddressUserMessage) ||
|
||||
hasShortInventoryObjectFollowupSignal(repairedEffectiveAddressUserMessage)));
|
||||
const effectiveAddressFollowupSignal = explicitAddressFollowupSignal && !dangerOrCoercionSignal;
|
||||
const deterministicNonDomainGuard = Boolean(!dataScopeMetaQuery &&
|
||||
!capabilityMetaQuery &&
|
||||
@@ -3933,7 +3968,8 @@ export function resolveAssistantOrchestrationDecision(input) {
|
||||
intentResolution.intent === "unknown");
|
||||
const nonDomainQueryIndexed = Boolean(!llmFirstAddressCandidate &&
|
||||
deterministicNonDomainGuard &&
|
||||
(llmFirstUnsupportedCandidate || llmContractMode === null));
|
||||
(llmFirstUnsupportedCandidate || llmContractMode === null) &&
|
||||
!protectedInventoryShortFollowup);
|
||||
const hardMetaMode = dataScopeMetaQuery
|
||||
? "data_scope"
|
||||
: capabilityMetaQuery && !dataRetrievalSignal
|
||||
|
||||
Reference in New Issue
Block a user