АРЧ - Склад: сохранять полный item-anchor в selected-object sale trace и закрепить buyer follow-up регрессиями
This commit is contained in:
@@ -943,7 +943,7 @@ function usesRecipeDefaultLimit(intent: AddressIntent): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function isLowQualityInventoryItemAnchorValue(rawValue: string): boolean {
|
||||
export function isLowQualityInventoryItemAnchorValue(rawValue: string): boolean {
|
||||
const value = cleanupAnchorValue(rawValue)
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
@@ -959,16 +959,47 @@ function isLowQualityInventoryItemAnchorValue(rawValue: string): boolean {
|
||||
return true;
|
||||
}
|
||||
const lowQualityTokens = new Set([
|
||||
"в",
|
||||
"во",
|
||||
"на",
|
||||
"по",
|
||||
"у",
|
||||
"от",
|
||||
"из",
|
||||
"для",
|
||||
"и",
|
||||
"или",
|
||||
"это",
|
||||
"этот",
|
||||
"эту",
|
||||
"его",
|
||||
"ее",
|
||||
"её",
|
||||
"итог",
|
||||
"итоге",
|
||||
"итогу",
|
||||
"сейчас",
|
||||
"лежат",
|
||||
"лежит",
|
||||
"лежали",
|
||||
"был",
|
||||
"была",
|
||||
"было",
|
||||
"были",
|
||||
"куплен",
|
||||
"куплена",
|
||||
"куплены",
|
||||
"куплено",
|
||||
"продан",
|
||||
"продана",
|
||||
"проданы",
|
||||
"продано",
|
||||
"продали",
|
||||
"реализован",
|
||||
"реализована",
|
||||
"реализованы",
|
||||
"реализовано",
|
||||
"реализовали",
|
||||
"документам",
|
||||
"документами",
|
||||
"документы",
|
||||
@@ -989,6 +1020,42 @@ function isLowQualityInventoryItemAnchorValue(rawValue: string): boolean {
|
||||
return meaningfulTokens.length === 0;
|
||||
}
|
||||
|
||||
function normalizeInventoryItemAnchorForComparison(rawValue: string): string {
|
||||
return cleanupAnchorValue(rawValue)
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/С‘/g, "Рµ")
|
||||
.replace(/\s+/g, " ");
|
||||
}
|
||||
|
||||
function tokenizeInventoryItemAnchorForComparison(rawValue: string): string[] {
|
||||
return normalizeInventoryItemAnchorForComparison(rawValue)
|
||||
.split(/[^a-zа-я0-9]+/iu)
|
||||
.map((token) => token.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
export function isInventoryItemAnchorDegradation(sourceValue: string, candidateValue: string): boolean {
|
||||
const sourceNormalized = normalizeInventoryItemAnchorForComparison(sourceValue);
|
||||
const candidateNormalized = normalizeInventoryItemAnchorForComparison(candidateValue);
|
||||
if (!sourceNormalized || !candidateNormalized || sourceNormalized === candidateNormalized) {
|
||||
return false;
|
||||
}
|
||||
if (isLowQualityInventoryItemAnchorValue(candidateNormalized)) {
|
||||
return true;
|
||||
}
|
||||
if (sourceNormalized.includes(candidateNormalized) && candidateNormalized.length < sourceNormalized.length) {
|
||||
return true;
|
||||
}
|
||||
const sourceTokens = tokenizeInventoryItemAnchorForComparison(sourceNormalized);
|
||||
const candidateTokens = tokenizeInventoryItemAnchorForComparison(candidateNormalized);
|
||||
return (
|
||||
candidateTokens.length > 0 &&
|
||||
candidateTokens.length < sourceTokens.length &&
|
||||
candidateTokens.every((token) => sourceTokens.includes(token))
|
||||
);
|
||||
}
|
||||
|
||||
function cleanupInventoryItemAnchorValue(value: string): string {
|
||||
return String(value ?? "")
|
||||
.replace(/^['"«»“”„`’‘]+|['"«»“”„`’‘]+$/gu, "")
|
||||
@@ -1012,7 +1079,34 @@ function trimInventoryItemAnchorTail(rawValue: string): string {
|
||||
return cleanupInventoryItemAnchorValue(value);
|
||||
}
|
||||
|
||||
function extractSelectedObjectQuotedValue(text: string): string | undefined {
|
||||
function extractQuotedAnchorValue(text: string): string | undefined {
|
||||
const patterns = [/[«"]([^«»"\r\n]+)[»"]/u, /'([^'\r\n]+)'/u];
|
||||
for (const pattern of patterns) {
|
||||
const match = String(text ?? "").match(pattern);
|
||||
const candidate = cleanupInventoryItemAnchorValue(String(match?.[1] ?? ""));
|
||||
if (candidate) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function extractUnicodeQuotedAnchorValue(text: string): string | undefined {
|
||||
const patterns = [
|
||||
/(?:\u00AB|\u0412\u00AB|")([^\u00AB\u00BB"\r\n]+?)(?:\u00BB|\u0412\u00BB|")/u,
|
||||
/'([^'\r\n]+)'/u
|
||||
];
|
||||
for (const pattern of patterns) {
|
||||
const match = String(text ?? "").match(pattern);
|
||||
const candidate = cleanupInventoryItemAnchorValue(String(match?.[1] ?? ""));
|
||||
if (candidate) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function extractSelectedObjectQuotedValue(text: string): string | undefined {
|
||||
const patterns = [
|
||||
/(?:по\s+выбранному\s+объекту|for\s+selected\s+object)\s*[«"]([^»"\r\n]+)[»"]/iu,
|
||||
/(?:по\s+выбранному\s+объекту|for\s+selected\s+object)\s*:\s*[«"]([^»"\r\n]+)[»"]/iu
|
||||
@@ -1024,7 +1118,7 @@ function extractSelectedObjectQuotedValue(text: string): string | undefined {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
return extractUnicodeQuotedAnchorValue(text) ?? extractQuotedAnchorValue(text);
|
||||
}
|
||||
|
||||
function extractInventoryItemFromSelectedObject(text: string): string | undefined {
|
||||
@@ -1051,6 +1145,10 @@ function extractInventoryItemAnchor(text: string): string | undefined {
|
||||
if (selectedObjectItem) {
|
||||
return selectedObjectItem;
|
||||
}
|
||||
const quotedItem = extractUnicodeQuotedAnchorValue(text) ?? extractQuotedAnchorValue(text);
|
||||
if (quotedItem && !isLowQualityInventoryItemAnchorValue(quotedItem)) {
|
||||
return quotedItem;
|
||||
}
|
||||
const patterns = [
|
||||
/(?:товар(?:а|у|ом|ы)?|номенклатур(?:а|у|ы)|позици(?:я|ю|и)|item|product|sku)\s*[«"']([^«»"'?\r\n]+)[»"'](?=$|[\s,.;:!?])/iu,
|
||||
/(?:товар(?:а|у|ом|ы)?|номенклатур(?:а|у|ы)|позици(?:я|ю|и)|item|product|sku)\s+([^\r\n,.;:!?]+?)(?=\s+(?:на|по|у|от|из|для|и|когда|через|сейчас|еще|ещё|котор|которые|который|покупателю|поставщика|поставщику|за|в)\b|[:?]|$)/iu
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AddressIntentResolution } from "../types/addressQuery";
|
||||
import { hasInventoryPurchaseStem, hasInventorySaleCue, hasInventorySupplierCue } from "./inventoryLifecycleCueHelpers";
|
||||
|
||||
const RECEIVABLES_STRONG = [
|
||||
"кто должен нам",
|
||||
@@ -1616,12 +1617,7 @@ function hasSelectedObjectInventoryCue(text: string): boolean {
|
||||
}
|
||||
|
||||
function hasSelectedObjectInventoryProvenanceSignal(text: string): boolean {
|
||||
return (
|
||||
hasSelectedObjectInventoryCue(text) &&
|
||||
/(?:кто\s+(?:(?:это|этот\s+товар|эту\s+позицию)\s+)?(?:нам\s+)?поставил|кто\s+(?:нам\s+)?поставил\s+(?:это|этот\s+товар|эту\s+позицию)|от\s+какого\s+поставщика|у\s+какого\s+поставщика|от\s+кого\s+куплен|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|где\s+куплено|supplier|vendor|поставщик)/iu.test(
|
||||
text
|
||||
)
|
||||
);
|
||||
return hasSelectedObjectInventoryCue(text) && hasInventorySupplierCue(text);
|
||||
}
|
||||
|
||||
function hasSelectedObjectInventoryPurchaseDocumentsSignal(text: string): boolean {
|
||||
@@ -1634,24 +1630,16 @@ function hasSelectedObjectInventoryPurchaseDocumentsSignal(text: string): boolea
|
||||
}
|
||||
|
||||
function hasSelectedObjectInventorySaleTraceSignal(text: string): boolean {
|
||||
return (
|
||||
hasSelectedObjectInventoryCue(text) &&
|
||||
/(?:кому\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали|кому\s+был\s+продан|куда\s+(?:в\s+итоге\s+)?(?:мы\s+)?продали(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|куда\s+(?:была\s+)?реализована\s+(?:позиция|номенклатура|продукция)|кто\s+купил|buyer|sale\s+trace|trace\s+of\s+sale)/iu.test(
|
||||
text
|
||||
)
|
||||
);
|
||||
return hasSelectedObjectInventoryCue(text) && hasInventorySaleCue(text);
|
||||
}
|
||||
|
||||
function hasInventoryProvenanceSignalV2(text: string): boolean {
|
||||
const hasItemCue = /(?:товар|номенклатур|sku|item|product|остат(?:ок|ки)|склад)/iu.test(text);
|
||||
const hasSupplierCue =
|
||||
/(?:от\s+какого\s+поставщика|у\s+какого\s+поставщика|от\s+кого\s+куплен|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|товар|позицию))?|где\s+куплено|кто\s+(?:нам\s+)?поставил|кем\s+поставлен|поставщик|supplier|vendor)/iu.test(
|
||||
text
|
||||
);
|
||||
const hasSupplierCue = hasInventorySupplierCue(text) || /кем\s+поставлен/iu.test(text);
|
||||
const hasPurchaseCue =
|
||||
/(?:куплен(?:ы|а|о)?|закупк|происхождени|откуда|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|товар|позицию))?|где\s+куплено|когда\s+был\s+куплен|когда\s+куплен|дата\s+закупк|кто\s+(?:нам\s+)?поставил|кем\s+поставлен|поставлен(?:ы|а)?|purchase\s+provenance|purchase\s+date)/iu.test(
|
||||
text
|
||||
);
|
||||
) || hasInventoryPurchaseStem(text);
|
||||
return hasItemCue && hasSupplierCue && hasPurchaseCue;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { AddressModeDetection } from "../types/addressQuery";
|
||||
|
||||
import { hasInventoryPurchaseStem, hasInventorySaleCue, hasInventorySupplierCue } from "./inventoryLifecycleCueHelpers";
|
||||
|
||||
const ADDRESS_ACTION_TOKENS = [
|
||||
"show",
|
||||
"list",
|
||||
@@ -286,8 +288,11 @@ function hasSelectedObjectInventoryFollowupSignal(text: string): boolean {
|
||||
if (!/(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции)/iu.test(text)) {
|
||||
return false;
|
||||
}
|
||||
return /(?:у\s+кого\s+купили|у\s+кого\s+куплено|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|товар|позицию))?|где\s+куплено|кто\s+(?:поставил|продал)|кому\s+(?:продали|реализовали)|когда\s+(?:примерно\s+)?купили|по\s+каким\s+документам\s+.*купили)/iu.test(
|
||||
text
|
||||
return (
|
||||
hasInventorySupplierCue(text) ||
|
||||
hasInventorySaleCue(text) ||
|
||||
/(?:кто\s+(?:поставил|продал)|по\s+каким\s+документам\s+.*купили)/iu.test(text) ||
|
||||
(/\bкогда\b/iu.test(text) && hasInventoryPurchaseStem(text))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2008,8 +2008,6 @@ function shouldBoostAutoBroadenedLimit(intent: AddressIntent): boolean {
|
||||
|
||||
function shouldClearAsOfDateForHistoryRecovery(intent: AddressIntent): boolean {
|
||||
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"
|
||||
);
|
||||
@@ -2020,8 +2018,6 @@ function shouldDetachLifecycleExecutionFromSnapshotContext(
|
||||
reasons: string[]
|
||||
): boolean {
|
||||
if (
|
||||
intent !== "inventory_purchase_provenance_for_item" &&
|
||||
intent !== "inventory_purchase_documents_for_item" &&
|
||||
intent !== "inventory_sale_trace_for_item" &&
|
||||
intent !== "inventory_purchase_to_sale_chain"
|
||||
) {
|
||||
|
||||
@@ -47,6 +47,44 @@ __WHERE_CLAUSE__
|
||||
Движения.Период __ORDER_DIRECTION__
|
||||
`;
|
||||
|
||||
const INVENTORY_SALE_DOCUMENTS_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
Товары.Ссылка.Дата КАК Период,
|
||||
ПРЕДСТАВЛЕНИЕ(Товары.Ссылка) КАК Регистратор,
|
||||
"" КАК СчетДт,
|
||||
"41.01" КАК СчетКт,
|
||||
Товары.Сумма КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(Товары.Номенклатура) КАК Номенклатура,
|
||||
ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Контрагент) КАК Контрагент,
|
||||
ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.ДоговорКонтрагента) КАК Договор,
|
||||
ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Организация) КАК Организация,
|
||||
Товары.Количество КАК Количество
|
||||
ИЗ
|
||||
Документ.РеализацияТоваровУслуг.Товары КАК Товары
|
||||
__WHERE_CLAUSE__
|
||||
УПОРЯДОЧИТЬ ПО
|
||||
Товары.Ссылка.Дата __ORDER_DIRECTION__
|
||||
`;
|
||||
|
||||
const INVENTORY_PURCHASE_DOCUMENTS_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
Товары.Ссылка.Дата КАК Период,
|
||||
ПРЕДСТАВЛЕНИЕ(Товары.Ссылка) КАК Регистратор,
|
||||
"41.01" КАК СчетДт,
|
||||
"" КАК СчетКт,
|
||||
Товары.Сумма КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(Товары.Номенклатура) КАК Номенклатура,
|
||||
ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Контрагент) КАК Контрагент,
|
||||
ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.ДоговорКонтрагента) КАК Договор,
|
||||
ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Организация) КАК Организация,
|
||||
Товары.Количество КАК Количество
|
||||
ИЗ
|
||||
Документ.ПоступлениеТоваровУслуг.Товары КАК Товары
|
||||
__WHERE_CLAUSE__
|
||||
УПОРЯДОЧИТЬ ПО
|
||||
Товары.Ссылка.Дата __ORDER_DIRECTION__
|
||||
`;
|
||||
|
||||
const PAYABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
__AS_OF_EXPR__ КАК Период,
|
||||
@@ -972,17 +1010,6 @@ function toQueryStringLiteral(value: string): string {
|
||||
return String(value ?? "").replace(/"/g, '""');
|
||||
}
|
||||
|
||||
function buildOrganizationPresentationCondition(filters: AddressFilterSet, fieldPath: string): string | null {
|
||||
const organization =
|
||||
typeof filters.organization === "string" && filters.organization.trim().length > 0
|
||||
? filters.organization.trim()
|
||||
: "";
|
||||
if (!organization) {
|
||||
return null;
|
||||
}
|
||||
return `ПРЕДСТАВЛЕНИЕ(${fieldPath}) = "${toQueryStringLiteral(organization)}"`;
|
||||
}
|
||||
|
||||
function buildWhereClause(filters: AddressFilterSet, fieldPath: string, extraConditions: string[] = []): string {
|
||||
const periodFromExpr =
|
||||
typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
||||
@@ -1154,15 +1181,56 @@ function buildInventoryMovementQuery(
|
||||
: side === "kt"
|
||||
? creditPredicate
|
||||
: `(${debitPredicate} ИЛИ ${creditPredicate})`;
|
||||
const organizationCondition = buildOrganizationPresentationCondition(filters, "Движения.Организация");
|
||||
return INVENTORY_MOVEMENTS_QUERY_TEMPLATE
|
||||
.replace("__LIMIT__", String(resolvedLimit))
|
||||
.replace("__WHERE_CLAUSE__", buildWhereClause(filters, "Движения.Период", [inventoryCondition]))
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
}
|
||||
|
||||
function buildInventoryItemReferenceCondition(filters: AddressFilterSet, fieldPaths: string[]): string | null {
|
||||
const item = typeof filters.item === "string" ? filters.item.trim() : "";
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
const escapedItem = toQueryStringLiteral(item);
|
||||
const referenceSubquery =
|
||||
`(ВЫБРАТЬ Номенклатура.Ссылка ИЗ Справочник.Номенклатура КАК Номенклатура ` +
|
||||
`ГДЕ Номенклатура.Наименование = "${escapedItem}")`;
|
||||
const clauses = fieldPaths
|
||||
.map((fieldPath) => String(fieldPath ?? "").trim())
|
||||
.filter((fieldPath) => fieldPath.length > 0)
|
||||
.map((fieldPath) => `${fieldPath} В ${referenceSubquery}`);
|
||||
if (clauses.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
|
||||
}
|
||||
|
||||
function buildInventorySaleDocumentQuery(filters: AddressFilterSet, resolvedLimit: number): string {
|
||||
const itemCondition = buildInventoryItemReferenceCondition(filters, ["Товары.Номенклатура"]);
|
||||
return INVENTORY_SALE_DOCUMENTS_QUERY_TEMPLATE
|
||||
.replace("__LIMIT__", String(resolvedLimit))
|
||||
.replace(
|
||||
"__WHERE_CLAUSE__",
|
||||
buildWhereClause(
|
||||
filters,
|
||||
"Движения.Период",
|
||||
[inventoryCondition, organizationCondition].filter((item): item is string => Boolean(item))
|
||||
"Товары.Ссылка.Дата",
|
||||
['Товары.Ссылка.Проведен = ИСТИНА', itemCondition].filter((item): item is string => Boolean(item))
|
||||
)
|
||||
)
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
}
|
||||
|
||||
function buildInventoryPurchaseDocumentQuery(filters: AddressFilterSet, resolvedLimit: number): string {
|
||||
const itemCondition = buildInventoryItemReferenceCondition(filters, ["Товары.Номенклатура"]);
|
||||
return INVENTORY_PURCHASE_DOCUMENTS_QUERY_TEMPLATE
|
||||
.replace("__LIMIT__", String(resolvedLimit))
|
||||
.replace(
|
||||
"__WHERE_CLAUSE__",
|
||||
buildWhereClause(
|
||||
filters,
|
||||
"Товары.Ссылка.Дата",
|
||||
['Товары.Ссылка.Проведен = ИСТИНА', itemCondition].filter((item): item is string => Boolean(item))
|
||||
)
|
||||
)
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
@@ -1395,13 +1463,13 @@ export function buildAddressRecipePlan(
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
})()
|
||||
: recipe.query_template === "inventory_purchase_provenance_profile"
|
||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
||||
? buildInventoryPurchaseDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_purchase_documents_profile"
|
||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
||||
? buildInventoryPurchaseDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_supplier_stock_overlap_profile"
|
||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
||||
: recipe.query_template === "inventory_sale_trace_profile"
|
||||
? buildInventoryMovementQuery(filters, resolvedLimit, "kt")
|
||||
? buildInventorySaleDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
||||
? buildInventoryMovementQuery(filters, resolvedLimit, "either")
|
||||
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
||||
|
||||
@@ -4065,6 +4065,7 @@ export function composeFactualReply(
|
||||
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);
|
||||
@@ -4074,7 +4075,9 @@ export function composeFactualReply(
|
||||
? `По позиции ${itemLabel} подтвержденная дата закупки в доступном контуре не найдена.`
|
||||
: summary.firstPeriod === summary.lastPeriod
|
||||
? `Позиция ${itemLabel} куплена ${firstPurchaseDate}.`
|
||||
: `По позиции ${itemLabel} подтвержденный диапазон закупок: с ${firstPurchaseDate} по ${lastPurchaseDate}.`;
|
||||
: boundedAsOfLabel
|
||||
? `По позиции ${itemLabel} до ${boundedAsOfLabel} подтвержден диапазон закупок: с ${firstPurchaseDate} по ${lastPurchaseDate}.`
|
||||
: `По позиции ${itemLabel} подтвержденный диапазон закупок: с ${firstPurchaseDate} по ${lastPurchaseDate}.`;
|
||||
const lines: string[] = [directAnswerLine];
|
||||
if (purchaseRows.length > 0) {
|
||||
lines.push("", "Подтверждение:");
|
||||
@@ -4090,8 +4093,13 @@ export function composeFactualReply(
|
||||
if (summary.documents.length > 0) {
|
||||
lines.push(`- Первый подтверждающий документ: ${summary.documents[0]}.`);
|
||||
}
|
||||
if (summary.firstPeriod && asOfDate && summary.firstPeriod < asOfDate) {
|
||||
lines.push(`- Дата вопроса по остатку: ${formatDateRu(asOfDate)}; дата закупки показана по подтвержденному закупочному следу.`);
|
||||
if (boundedAsOfLabel) {
|
||||
lines.push(`- Для ответа учтены закупочные документы не позже ${boundedAsOfLabel}.`);
|
||||
}
|
||||
if (summary.counterparties.length > 1) {
|
||||
lines.push(
|
||||
`- Без партионного учета нельзя однозначно связать остаток${boundedAsOfLabel ? ` на ${boundedAsOfLabel}` : ""} с одним конкретным поступлением.`
|
||||
);
|
||||
}
|
||||
}
|
||||
return {
|
||||
@@ -4105,35 +4113,50 @@ export function composeFactualReply(
|
||||
};
|
||||
}
|
||||
const directAnswerLine =
|
||||
summary.counterparties.length === 1
|
||||
? `Товар ${itemLabel} по доступным закупочным движениям связан с поставщиком: ${summary.counterparties[0]}.`
|
||||
: summary.counterparties.length > 1
|
||||
? `По доступным закупочным движениям по товару ${itemLabel} найдено несколько поставщиков: ${summary.counterparties.slice(0, 4).join("; ")}.`
|
||||
: `По товару ${itemLabel} найден закупочный след, но поставщик не материализован отдельным полем в текущем exact-контуре.`;
|
||||
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, "", "Подтверждение:"];
|
||||
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, 4).join("; ")}.`);
|
||||
} else if (purchaseRows.length > 0) {
|
||||
lines.push("- Закупочные документы найдены, но поставщик не материализован отдельным полем в текущем exact-контуре.");
|
||||
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));
|
||||
}
|
||||
if (purchaseRows.length > 0) {
|
||||
lines.push(
|
||||
"",
|
||||
"Сервисно:",
|
||||
"- Без партионности этот контур показывает документально наблюдаемый закупочный след, а не лот-level происхождение текущего остатка."
|
||||
);
|
||||
}
|
||||
return {
|
||||
responseType: purchaseRows.length > 0 ? "FACTUAL_SUMMARY" : "FACTUAL_SUMMARY",
|
||||
responseType: "FACTUAL_SUMMARY",
|
||||
text: joinLines(lines),
|
||||
semantics: {
|
||||
result_mode: "confirmed_balance",
|
||||
|
||||
@@ -9,7 +9,13 @@
|
||||
import { detectAddressQuestionMode } from "../addressQueryClassifier";
|
||||
import { classifyAddressQueryShape } from "../addressQueryShapeClassifier";
|
||||
import { resolveAddressIntent } from "../addressIntentResolver";
|
||||
import { extractAddressFilters } from "../addressFilterExtractor";
|
||||
import {
|
||||
extractSelectedObjectQuotedValue,
|
||||
extractAddressFilters,
|
||||
isInventoryItemAnchorDegradation,
|
||||
isLowQualityInventoryItemAnchorValue
|
||||
} from "../addressFilterExtractor";
|
||||
import { hasInventoryPurchaseStem, hasInventorySaleCue, hasInventorySupplierCue } from "../inventoryLifecycleCueHelpers";
|
||||
import { applyAddressLlmSemanticHintsToExtraction } from "./semanticHintOverlay";
|
||||
import type { AddressLlmSemanticHints } from "../../types/addressQuery";
|
||||
|
||||
@@ -511,25 +517,24 @@ function hasSelectedObjectInventorySignal(text: string): boolean {
|
||||
return /(?:по\s+выбранному\s+объекту|for\s+selected\s+object)/iu.test(String(text ?? ""));
|
||||
}
|
||||
|
||||
function hasInventorySupplierFollowupCue(text: string): boolean {
|
||||
return /(?:кто\s+(?:(?:это|этот\s+товар|эту\s+позицию)\s+)?(?:нам\s+)?поставил|кто\s+(?:нам\s+)?поставил\s+(?:это|этот\s+товар|эту\s+позицию)|от\s+какого\s+поставщика|у\s+какого\s+поставщика|от\s+кого\s+куплен|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|где\s+куплено|supplier|vendor|поставщик)/iu.test(
|
||||
String(text ?? "")
|
||||
);
|
||||
export function hasInventorySupplierFollowupCue(text: string): boolean {
|
||||
return hasInventorySupplierCue(String(text ?? ""));
|
||||
}
|
||||
|
||||
function hasInventoryPurchaseDocumentsFollowupCue(text: string): boolean {
|
||||
export function hasInventoryPurchaseDocumentsFollowupCue(text: string): boolean {
|
||||
return /(?:по\s+каким\s+документам\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|по\s+каким\s+документам\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|какими\s+документами\s+(?:был\s+)?куплен|покажи\s+документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(
|
||||
String(text ?? "")
|
||||
);
|
||||
}
|
||||
|
||||
function hasInventoryPurchaseDateFollowupCue(text: 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(
|
||||
String(text ?? "")
|
||||
);
|
||||
value
|
||||
) || (/когда/iu.test(value) && hasInventoryPurchaseStem(value));
|
||||
}
|
||||
|
||||
function hasBareInventoryPurchaseDateFollowupCue(text: string): boolean {
|
||||
export function hasBareInventoryPurchaseDateFollowupCue(text: string): boolean {
|
||||
const normalized = String(text ?? "").trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
@@ -537,13 +542,11 @@ function hasBareInventoryPurchaseDateFollowupCue(text: string): boolean {
|
||||
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+(?:в\s+итоге\s+)?(?:мы\s+)?продали|куда\s+ушла\s+позиция|куда\s+ушел\s+товар|кто\s+купил|buyer|покупател)/iu.test(
|
||||
String(text ?? "")
|
||||
);
|
||||
export function hasInventorySaleFollowupCue(text: string): boolean {
|
||||
return hasInventorySaleCue(String(text ?? ""));
|
||||
}
|
||||
|
||||
function hasInventoryPurchaseToSaleChainFollowupCue(text: string): boolean {
|
||||
export 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 ?? "")
|
||||
);
|
||||
@@ -777,13 +780,38 @@ function mergeFollowupFilters(
|
||||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date") &&
|
||||
!toNonEmptyString(merged.item)
|
||||
intent === "inventory_aging_by_purchase_date")
|
||||
) {
|
||||
const inheritedItem = previousItem ?? previousAnchorItem;
|
||||
if (inheritedItem && intent !== "inventory_aging_by_purchase_date") {
|
||||
const explicitQuotedItem = toNonEmptyString(extractSelectedObjectQuotedValue(userMessage));
|
||||
const currentItem = toNonEmptyString(merged.item);
|
||||
const shouldAdoptExplicitQuotedItem =
|
||||
Boolean(explicitQuotedItem) &&
|
||||
(!currentItem ||
|
||||
currentItem !== explicitQuotedItem ||
|
||||
isInventoryItemAnchorDegradation(explicitQuotedItem ?? "", currentItem ?? ""));
|
||||
if (explicitQuotedItem && shouldAdoptExplicitQuotedItem) {
|
||||
merged.item = explicitQuotedItem;
|
||||
reasons.push(currentItem ? "item_replaced_from_explicit_quote" : "item_from_explicit_quote");
|
||||
}
|
||||
const effectiveCurrentItem = toNonEmptyString(merged.item);
|
||||
const hasExplicitDifferentQuotedItem =
|
||||
Boolean(explicitQuotedItem) &&
|
||||
Boolean(inheritedItem) &&
|
||||
explicitQuotedItem !== inheritedItem;
|
||||
const shouldInheritItem =
|
||||
Boolean(inheritedItem) &&
|
||||
intent !== "inventory_aging_by_purchase_date" &&
|
||||
!hasExplicitDifferentQuotedItem &&
|
||||
(!effectiveCurrentItem ||
|
||||
(isLowQualityInventoryItemAnchorValue(effectiveCurrentItem) &&
|
||||
!isLowQualityInventoryItemAnchorValue(inheritedItem ?? "")) ||
|
||||
(effectiveCurrentItem &&
|
||||
inheritedItem &&
|
||||
isInventoryItemAnchorDegradation(inheritedItem, effectiveCurrentItem)));
|
||||
if (shouldInheritItem && inheritedItem) {
|
||||
merged.item = inheritedItem;
|
||||
reasons.push("item_from_followup_context");
|
||||
reasons.push(effectiveCurrentItem ? "item_replaced_from_followup_context" : "item_from_followup_context");
|
||||
}
|
||||
}
|
||||
if (sameDateRequested) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
AddressLlmSemanticHints,
|
||||
AddressSemanticFrame
|
||||
} from "../../types/addressQuery";
|
||||
import { isInventoryItemAnchorDegradation, isLowQualityInventoryItemAnchorValue } from "../addressFilterExtractor";
|
||||
|
||||
function toNonEmptyString(value: unknown): string | null {
|
||||
if (value === null || value === undefined) {
|
||||
@@ -83,6 +84,31 @@ function applyDateScopeHint(frame: AddressSemanticFrame, dateScopeKind: AddressL
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeInventoryItemAnchorValue(value: string): string {
|
||||
return String(value ?? "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, " ");
|
||||
}
|
||||
|
||||
function shouldApplyInventoryItemSemanticHint(currentItemValue: string | null, hintedItemValue: string): boolean {
|
||||
if (!hintedItemValue || isLowQualityInventoryItemAnchorValue(hintedItemValue)) {
|
||||
return false;
|
||||
}
|
||||
if (!currentItemValue || isLowQualityInventoryItemAnchorValue(currentItemValue)) {
|
||||
return true;
|
||||
}
|
||||
if (isInventoryItemAnchorDegradation(currentItemValue, hintedItemValue)) {
|
||||
return false;
|
||||
}
|
||||
const currentNormalized = normalizeInventoryItemAnchorValue(currentItemValue);
|
||||
const hintedNormalized = normalizeInventoryItemAnchorValue(hintedItemValue);
|
||||
if (currentNormalized === hintedNormalized) {
|
||||
return false;
|
||||
}
|
||||
return hintedNormalized.includes(currentNormalized);
|
||||
}
|
||||
|
||||
export function applyAddressLlmSemanticHintsToExtraction(
|
||||
extraction: AddressFilterExtraction,
|
||||
semanticHintsInput: unknown
|
||||
@@ -152,11 +178,16 @@ export function applyAddressLlmSemanticHintsToExtraction(
|
||||
}
|
||||
|
||||
if (semanticHints.scope_target_kind === "item" && scopeTargetText) {
|
||||
extractedFilters.item = scopeTargetText;
|
||||
pushWarning(warnings, "item_from_llm_semantics");
|
||||
semanticFrame.scope_kind = "explicit_anchor";
|
||||
semanticFrame.anchor_kind = "item";
|
||||
semanticFrame.anchor_value = scopeTargetText;
|
||||
const currentItemValue = toNonEmptyString(extractedFilters.item);
|
||||
if (shouldApplyInventoryItemSemanticHint(currentItemValue, scopeTargetText)) {
|
||||
extractedFilters.item = scopeTargetText;
|
||||
pushWarning(warnings, "item_from_llm_semantics");
|
||||
semanticFrame.scope_kind = "explicit_anchor";
|
||||
semanticFrame.anchor_kind = "item";
|
||||
semanticFrame.anchor_value = scopeTargetText;
|
||||
} else if (currentItemValue && currentItemValue !== scopeTargetText) {
|
||||
pushWarning(warnings, "item_llm_semantics_ignored");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -11,6 +11,7 @@ import * as addressQueryService_1 from "./addressQueryService";
|
||||
import * as addressQueryClassifier_1 from "./addressQueryClassifier";
|
||||
import * as addressIntentResolver_1 from "./addressIntentResolver";
|
||||
import * as addressFilterExtractor_1 from "./addressFilterExtractor";
|
||||
import * as decomposeStage_1 from "./address_runtime/decomposeStage";
|
||||
import * as predecomposeContract_1 from "./address_runtime/predecomposeContract";
|
||||
import * as openaiResponsesClient_1 from "./openaiResponsesClient";
|
||||
import * as addressMcpClient_1 from "./addressMcpClient";
|
||||
@@ -2725,8 +2726,15 @@ function hasShortInventoryObjectFollowupSignal(userMessage) {
|
||||
if (minTokens > 8) {
|
||||
return false;
|
||||
}
|
||||
const hasDirectSaleFollowupCue = (sample) => /(?:кому|каму|куда)(?:\s+\S+){0,4}\s+(?:продали|продано|продан(?:о|а|ы)?|реализовали|реализован(?:о|а|ы)?)|(?:продали|продано|реализовали|реализован(?:о|а|ы)?)(?:\s+\S+){0,4}\s+(?:кому|каму|куда)|(?:^|\s)(?:продано|продали|реализовано|реализовали)(?=$|[\s,.;:!?])/iu.test(sample);
|
||||
return samples.some((sample) => /^(?:кто|когда|документы|сумма|поставщик|покупатель)(?:\?)?$/iu.test(sample) ||
|
||||
/^(?:когда\s+(?:примерно\s+)?купили(?:\s+ее)?|каким\s+документом|покажи\s+документы|по\s+каким\s+документам|все\s+закупки|все\s+поступления|кому\s+(?:мы\s+)?продали|кто\s+купил|цепочка|путь\s+товара)(?:\?)?$/iu.test(sample));
|
||||
hasDirectSaleFollowupCue(sample) ||
|
||||
(0, decomposeStage_1.hasInventorySupplierFollowupCue)(sample) ||
|
||||
(0, decomposeStage_1.hasInventoryPurchaseDocumentsFollowupCue)(sample) ||
|
||||
(0, decomposeStage_1.hasInventoryPurchaseDateFollowupCue)(sample) ||
|
||||
(0, decomposeStage_1.hasBareInventoryPurchaseDateFollowupCue)(sample) ||
|
||||
(0, decomposeStage_1.hasInventorySaleFollowupCue)(sample) ||
|
||||
(0, decomposeStage_1.hasInventoryPurchaseToSaleChainFollowupCue)(sample));
|
||||
}
|
||||
function resolveDebtRoleSwapFollowupIntent(userMessage, previousIntent) {
|
||||
const normalized = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
||||
@@ -3366,6 +3374,13 @@ function resolveRequiredAnchorTypeForIntent(intent) {
|
||||
if (intent === "list_documents_by_contract" || intent === "bank_operations_by_contract") {
|
||||
return "contract";
|
||||
}
|
||||
if (intent === "inventory_purchase_provenance_for_item" ||
|
||||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date") {
|
||||
return "item";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function evaluateAddressAnchorQuality(message) {
|
||||
@@ -3383,7 +3398,9 @@ function evaluateAddressAnchorQuality(message) {
|
||||
const extracted = (0, addressFilterExtractor_1.extractAddressFilters)(String(message ?? ""), intent);
|
||||
const anchorValue = anchorType === "counterparty"
|
||||
? toNonEmptyString(extracted?.extracted_filters?.counterparty)
|
||||
: toNonEmptyString(extracted?.extracted_filters?.contract);
|
||||
: anchorType === "contract"
|
||||
? toNonEmptyString(extracted?.extracted_filters?.contract)
|
||||
: toNonEmptyString(extracted?.extracted_filters?.item);
|
||||
if (!anchorValue) {
|
||||
return {
|
||||
intent,
|
||||
@@ -3394,7 +3411,9 @@ function evaluateAddressAnchorQuality(message) {
|
||||
}
|
||||
const lowQuality = anchorType === "counterparty"
|
||||
? isLowQualityPredecomposeCounterpartyAnchor(anchorValue)
|
||||
: isLowQualityPredecomposeContractAnchor(anchorValue);
|
||||
: anchorType === "contract"
|
||||
? isLowQualityPredecomposeContractAnchor(anchorValue)
|
||||
: (0, addressFilterExtractor_1.isLowQualityInventoryItemAnchorValue)(anchorValue);
|
||||
return {
|
||||
intent,
|
||||
anchorType,
|
||||
@@ -3618,6 +3637,14 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
|
||||
const sourceAnchorQuality = evaluateAddressAnchorQuality(repairedSourceMessage || userMessage);
|
||||
const candidateAnchorQuality = evaluateAddressAnchorQuality(candidate);
|
||||
const sameIntentForAnchorSafety = sourceAnchorQuality.intent !== "unknown" && sourceAnchorQuality.intent === candidateAnchorQuality.intent;
|
||||
const sourceSelectedObjectItemAnchorValue = toNonEmptyString((0, addressFilterExtractor_1.extractSelectedObjectQuotedValue)(userMessage)) ??
|
||||
toNonEmptyString((0, addressFilterExtractor_1.extractSelectedObjectQuotedValue)(repairedSourceMessage || userMessage));
|
||||
const candidateSemanticItemAnchorValue = (((sameIntentForAnchorSafety &&
|
||||
sourceAnchorQuality.anchorType === "item") ||
|
||||
Boolean(sourceSelectedObjectItemAnchorValue)) &&
|
||||
candidateMeta?.semanticHints?.scope_target_kind === "item"
|
||||
? toNonEmptyString(candidateMeta.semanticHints.scope_target_text)
|
||||
: null);
|
||||
const counterpartyAnchorSubstitutedByCandidate = sameIntentForAnchorSafety &&
|
||||
sourceAnchorQuality.anchorType === "counterparty" &&
|
||||
sourceAnchorQuality.quality >= 2 &&
|
||||
@@ -3642,6 +3669,25 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
|
||||
semanticHints: candidateMeta?.semanticHints ?? null
|
||||
}, userMessage);
|
||||
}
|
||||
const itemSemanticAnchorDegradedByCandidate = (sameIntentForAnchorSafety ||
|
||||
Boolean(sourceSelectedObjectItemAnchorValue)) &&
|
||||
Boolean(sourceSelectedObjectItemAnchorValue ?? sourceAnchorQuality.anchorValue) &&
|
||||
Boolean(candidateSemanticItemAnchorValue) &&
|
||||
(0, addressFilterExtractor_1.isInventoryItemAnchorDegradation)(sourceSelectedObjectItemAnchorValue ?? sourceAnchorQuality.anchorValue ?? "", candidateSemanticItemAnchorValue ?? "");
|
||||
if (itemSemanticAnchorDegradedByCandidate) {
|
||||
return attachAddressPredecomposeContract({
|
||||
...baseMeta,
|
||||
attempted: true,
|
||||
applied: false,
|
||||
traceId: normalized?.trace_id ?? null,
|
||||
llmCanonicalCandidateDetected: true,
|
||||
effectiveMessage: userMessage,
|
||||
reason: "normalized_fragment_rejected_anchor_degradation",
|
||||
fallbackRuleHit: null,
|
||||
sanitizedUserMessage,
|
||||
semanticHints: candidateMeta?.semanticHints ?? null
|
||||
}, userMessage);
|
||||
}
|
||||
const anchorDegradedByCandidate = sameIntentForAnchorSafety &&
|
||||
sourceAnchorQuality.anchorType &&
|
||||
sourceAnchorQuality.quality >= 2 &&
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
function toText(value: string): string {
|
||||
return String(value ?? "");
|
||||
}
|
||||
|
||||
export function hasInventoryPurchaseStem(text: string): boolean {
|
||||
return /купл[а-яёa-z0-9_-]*/iu.test(toText(text));
|
||||
}
|
||||
|
||||
export function hasInventorySupplierCue(text: string): boolean {
|
||||
const value = toText(text);
|
||||
if (
|
||||
/(?:кто\s+(?:(?:это|этот\s+товар|эту\s+позицию)\s+)?(?:нам\s+)?поставил|кто\s+(?:нам\s+)?поставил\s+(?:это|этот\s+товар|эту\s+позицию)|от\s+какого\s+поставщика|у\s+какого\s+поставщика|от\s+кого\s+куплен|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|где\s+куплено|supplier|vendor|поставщик)/iu.test(
|
||||
value
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return hasInventoryPurchaseStem(value) && /(?:у\s+кого|от\s+кого|где)/iu.test(value);
|
||||
}
|
||||
|
||||
export function hasInventorySaleCue(text: string): boolean {
|
||||
const value = toText(text);
|
||||
if (/(?:buyer|покупател)/iu.test(value)) {
|
||||
return true;
|
||||
}
|
||||
if (/(?:куда\s+ушла\s+позиция|куда\s+ушел\s+товар|кто\s+купил)/iu.test(value)) {
|
||||
return true;
|
||||
}
|
||||
const hasDirectionCue = /(?:кому|каму|куда)/iu.test(value);
|
||||
const hasSaleVerb =
|
||||
/(?:продал(?:и|а|о|ы)?|продан(?:а|о|ы)?|продано|реализовал(?:и|а|о|ы)?|реализован(?:а|о|ы)?|реализовано)/iu.test(
|
||||
value
|
||||
);
|
||||
if (hasDirectionCue && hasSaleVerb) {
|
||||
return true;
|
||||
}
|
||||
return /(?:^|[\s,.;:!?])(продано|продали|продан(?:а|о|ы)?|реализовано|реализовали|реализован(?:а|о|ы)?)(?=$|[\s,.;:!?])/iu.test(
|
||||
value
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user