Закрепить складской provenance и supplier-overlap ассистента
This commit is contained in:
@@ -559,6 +559,9 @@ function shouldRestoreInventoryRootFrame(userMessage, intent, extractedFilters,
|
||||
if (!canReenterInventoryRoot) {
|
||||
return false;
|
||||
}
|
||||
if (intent === "inventory_aging_by_purchase_date") {
|
||||
return false;
|
||||
}
|
||||
if (intent !== "unknown" && !isInventoryIntent(intent) && !hasInventoryRootRestatementCue) {
|
||||
return false;
|
||||
}
|
||||
@@ -584,13 +587,13 @@ function shouldRestoreInventoryRootFrame(userMessage, intent, extractedFilters,
|
||||
}
|
||||
function hasSelectedObjectInventorySignal(text) {
|
||||
const repairedSelectedObjectText = textWithRepairedVariant(String(text ?? ""));
|
||||
if (/(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(repairedSelectedObjectText)) {
|
||||
if (/(?:по\s+выбранному\s+объекту|выбранн(?:ому|ым)\s+(?:объекту|товару|товаром|позиции|позицией)|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(repairedSelectedObjectText)) {
|
||||
return true;
|
||||
}
|
||||
if (/(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(String(text ?? ""))) {
|
||||
if (/(?:по\s+выбранному\s+объекту|выбранн(?:ому|ым)\s+(?:объекту|товару|товаром|позиции|позицией)|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(String(text ?? ""))) {
|
||||
return true;
|
||||
}
|
||||
return /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(String(text ?? ""));
|
||||
return /(?:по\s+выбранному\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 ?? ""));
|
||||
@@ -618,6 +621,7 @@ function hasInventorySupplierFollowupCue(text) {
|
||||
function hasInventoryPurchaseDocumentsFollowupCue(text) {
|
||||
const value = textWithRepairedVariant(String(text ?? ""));
|
||||
return (/(?:по\s+каким\s+документам\s+(?:это|его|ее|её|этот\s+товар|эту\s+позицию)\s+купили|по\s+каким\s+документам\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|какими\s+документами\s+(?:был\s+)?куплен|каким\s+документом\s+(?:это|его|ее|её|она|он|этот\s+товар|эта\s+позиция|эту\s+позицию)?\s*(?:подтвержден[оа]?|подтверждена\s+закупка|доказан[оа]?|закрыт[оа]?)|закупк[аи]\s+подтвержден[аы]?\s+документ|покажи\s+документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему))/iu.test(value) ||
|
||||
/(?:документ[а-яё]*[\s\S]{0,40}поступлен[а-яё]*[\s\S]{0,140}(?:выбранн|товар|позици)|какие\s+документ[а-яё]*[\s\S]{0,40}поступлен[а-яё]*[\s\S]{0,140}(?:выбранн|товар|позици))/iu.test(value) ||
|
||||
/(?:по\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+(?:этой\s+позиции|этому\s+товару|ней|нему)|документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(value) ||
|
||||
/(?:(?:покажи|показать|выведи|дай)?[\s\S]{0,30}док(?:и|умент[а-яё]*)[\s\S]{0,80}(?:по\s+(?:ним|ней|нему|этой\s+позиции|этому\s+товару)|операци)|(?:по\s+(?:ним|ней|нему|этой\s+позиции|этому\s+товару))[\s\S]{0,80}док(?:и|умент[а-яё]*))/iu.test(value));
|
||||
}
|
||||
@@ -823,6 +827,22 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||
reasons.push("item_cleared_for_stock_slice_aging");
|
||||
}
|
||||
};
|
||||
const clearInventoryOnHandWarehouseAliasItem = () => {
|
||||
if (intent !== "inventory_on_hand_as_of_date") {
|
||||
return;
|
||||
}
|
||||
const currentItem = toNonEmptyString(merged.item);
|
||||
const currentWarehouse = toNonEmptyString(merged.warehouse);
|
||||
if (!currentItem || !currentWarehouse) {
|
||||
return;
|
||||
}
|
||||
const normalizedItem = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeSearchText)(currentItem);
|
||||
const normalizedWarehouse = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeSearchText)(currentWarehouse);
|
||||
if (normalizedItem && normalizedWarehouse && normalizedItem === normalizedWarehouse) {
|
||||
delete merged.item;
|
||||
reasons.push("item_cleared_as_warehouse_scope_alias_for_inventory_snapshot");
|
||||
}
|
||||
};
|
||||
if (!followupContext) {
|
||||
if ((intent === "list_open_contracts" ||
|
||||
intent === "open_contracts_confirmed_as_of_date" ||
|
||||
@@ -838,17 +858,20 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||
: "as_of_date_derived_from_period_for_open_contracts");
|
||||
}
|
||||
}
|
||||
clearInventoryOnHandWarehouseAliasItem();
|
||||
clearInventoryAgingOrganizationAliasItem(null);
|
||||
return { filters: merged, reasons };
|
||||
}
|
||||
const previous = followupContext.previous_filters ?? {};
|
||||
const root = followupContext.root_filters ?? {};
|
||||
const previousAnchorValue = toNonEmptyString(followupContext.previous_anchor_value);
|
||||
const previousCounterparty = toNonEmptyString(previous.counterparty);
|
||||
const previousContract = toNonEmptyString(previous.contract);
|
||||
const previousAccount = toNonEmptyString(previous.account);
|
||||
const previousItem = toNonEmptyString(previous.item);
|
||||
const previousAnchorItem = followupContext.previous_anchor_type === "item" ? previousAnchorValue : null;
|
||||
const previousOrganization = toNonEmptyString(previous.organization);
|
||||
const previousOrganization = toNonEmptyString(previous.organization) ?? toNonEmptyString(root.organization);
|
||||
const previousWarehouse = toNonEmptyString(previous.warehouse) ?? toNonEmptyString(root.warehouse);
|
||||
const previousAsOfDate = toNonEmptyString(previous.as_of_date);
|
||||
const previousPeriodFrom = toNonEmptyString(previous.period_from);
|
||||
const previousPeriodTo = toNonEmptyString(previous.period_to);
|
||||
@@ -1231,6 +1254,10 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||
reasons.push("period_derived_from_inventory_root_frame_year");
|
||||
}
|
||||
if (intent === "inventory_aging_by_purchase_date") {
|
||||
if (previousWarehouse && !toNonEmptyString(merged.warehouse)) {
|
||||
merged.warehouse = previousWarehouse;
|
||||
reasons.push("warehouse_from_followup_context");
|
||||
}
|
||||
clearInventoryAgingOrganizationAliasItem(previousOrganization);
|
||||
}
|
||||
if (!sameDateRequested &&
|
||||
@@ -1254,6 +1281,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||
delete merged.period_to;
|
||||
reasons.push("period_cleared_by_all_time_followup");
|
||||
}
|
||||
clearInventoryOnHandWarehouseAliasItem();
|
||||
return { filters: merged, reasons };
|
||||
}
|
||||
if (intent === "counterparty_activity_lifecycle" &&
|
||||
@@ -1414,6 +1442,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||
reasons.push("counterparty_cleared_as_organization_scope_alias");
|
||||
}
|
||||
}
|
||||
clearInventoryOnHandWarehouseAliasItem();
|
||||
return { filters: merged, reasons };
|
||||
}
|
||||
function resolveMissingRequiredFilters(intent, filters) {
|
||||
@@ -1441,16 +1470,50 @@ function resolveMissingRequiredFilters(intent, filters) {
|
||||
});
|
||||
}
|
||||
function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupContext) {
|
||||
const normalizedMessage = String(userMessage ?? "");
|
||||
const hasSelectedObjectReference = hasSelectedObjectInventorySignal(normalizedMessage);
|
||||
if (!followupContext || (!followupContext.previous_intent && !followupContext.target_intent)) {
|
||||
if (hasSelectedObjectReference && hasInventoryPurchaseDocumentsFollowupCue(normalizedMessage)) {
|
||||
if (detectedIntent.intent === "unknown" ||
|
||||
detectedIntent.intent === "list_documents_by_counterparty" ||
|
||||
detectedIntent.intent === "list_documents_by_contract" ||
|
||||
detectedIntent.intent === "inventory_on_hand_as_of_date") {
|
||||
return {
|
||||
intent: "inventory_purchase_documents_for_item",
|
||||
confidence: "low",
|
||||
reasons: [...detectedIntent.reasons, "intent_adjusted_to_inline_selected_object_inventory_documents"]
|
||||
};
|
||||
}
|
||||
}
|
||||
if (hasSelectedObjectReference && hasInventorySupplierFollowupCue(normalizedMessage)) {
|
||||
if (detectedIntent.intent === "unknown" ||
|
||||
detectedIntent.intent === "list_documents_by_counterparty" ||
|
||||
detectedIntent.intent === "list_documents_by_contract" ||
|
||||
detectedIntent.intent === "inventory_on_hand_as_of_date") {
|
||||
return {
|
||||
intent: "inventory_purchase_provenance_for_item",
|
||||
confidence: "low",
|
||||
reasons: [...detectedIntent.reasons, "intent_adjusted_to_inline_selected_object_inventory_supplier"]
|
||||
};
|
||||
}
|
||||
}
|
||||
if (hasSelectedObjectReference && hasInventoryPurchaseDateFollowupCue(normalizedMessage)) {
|
||||
if (detectedIntent.intent === "unknown" || detectedIntent.intent === "inventory_on_hand_as_of_date") {
|
||||
return {
|
||||
intent: "inventory_purchase_provenance_for_item",
|
||||
confidence: "low",
|
||||
reasons: [...detectedIntent.reasons, "intent_adjusted_to_inline_selected_object_inventory_purchase_date"]
|
||||
};
|
||||
}
|
||||
}
|
||||
return detectedIntent;
|
||||
}
|
||||
const normalizedMessage = String(userMessage ?? "");
|
||||
const genericFollowupSignal = hasAddressFollowupContextSignal(normalizedMessage);
|
||||
const previousFilters = followupContext.previous_filters ?? {};
|
||||
const purchaseBridgeContinuationSignal = followupContext.previous_intent === "vat_liability_confirmed_for_tax_period" &&
|
||||
Boolean(toNonEmptyString(previousFilters.purchase_date_bridge_selected)) &&
|
||||
hasInventoryPurchaseDateVatBridgeContinuationCue(normalizedMessage);
|
||||
const hasFollowupSignal = genericFollowupSignal || purchaseBridgeContinuationSignal;
|
||||
const hasFollowupSignal = genericFollowupSignal || purchaseBridgeContinuationSignal || hasSelectedObjectReference;
|
||||
if (!hasFollowupSignal) {
|
||||
return detectedIntent;
|
||||
}
|
||||
@@ -1481,7 +1544,6 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
||||
followupContext.root_anchor_type === "item" ||
|
||||
followupContext.current_frame_kind === "inventory_root" ||
|
||||
followupContext.current_frame_kind === "inventory_drilldown";
|
||||
const hasSelectedObjectReference = hasSelectedObjectInventorySignal(normalizedMessage);
|
||||
const inventorySelectedObjectFollowup = inventoryLineageActive && (hasSelectedObjectReference || (previousIsInventoryFamily && hasFollowupSignal));
|
||||
const previousCounterpartyLaneActive = hasPreviousCounterparty &&
|
||||
(followupContext.previous_anchor_type === "counterparty" ||
|
||||
|
||||
+191
-30
@@ -57,6 +57,53 @@ function inventoryRequestedPartyMatches(requested, actualParties) {
|
||||
function inventoryPartyListOrUnknown(parties) {
|
||||
return parties.length > 0 ? parties.slice(0, 4).join("; ") : "не выделен отдельным полем";
|
||||
}
|
||||
function inventoryPartyLeadList(parties, limit = 6) {
|
||||
const visible = parties.slice(0, limit);
|
||||
const remaining = parties.length - visible.length;
|
||||
const suffix = remaining > 0 ? `; и еще ${remaining}` : "";
|
||||
return `${visible.join("; ")}${suffix}`;
|
||||
}
|
||||
function inventoryItemLeadList(items, limit = 6) {
|
||||
const visible = items.slice(0, limit);
|
||||
const remaining = items.length - visible.length;
|
||||
const suffix = remaining > 0 ? `; и еще ${remaining}` : "";
|
||||
return `${visible.join("; ")}${suffix}`;
|
||||
}
|
||||
function collectInventoryTraceItems(rows) {
|
||||
const result = [];
|
||||
const seen = new Set();
|
||||
for (const row of rows) {
|
||||
const item = String(row.item ?? "").trim();
|
||||
if (!item) {
|
||||
continue;
|
||||
}
|
||||
const comparable = normalizeInventoryReplyEntityToken(item);
|
||||
if (!comparable || seen.has(comparable)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(comparable);
|
||||
result.push(item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function extractInventoryCounterpartiesFromEvidenceLines(lines) {
|
||||
const counterparties = [];
|
||||
for (const line of lines) {
|
||||
const match = /\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442:\s*([^|]+)/iu.exec(line);
|
||||
const value = String(match?.[1] ?? "").replace(/\s+/gu, " ").trim();
|
||||
if (value) {
|
||||
counterparties.push(value);
|
||||
}
|
||||
}
|
||||
return counterparties;
|
||||
}
|
||||
function hasSupplierScopedItemListQuestion(options) {
|
||||
if (!String(options.counterpartyHint ?? "").trim()) {
|
||||
return false;
|
||||
}
|
||||
const text = `${options.rawUserMessage ?? ""} ${options.userMessage ?? ""}`;
|
||||
return /(?:какие?\s+товар|товар(?:ы|ов)?\s+(?:от|у)\s+поставщик|позици(?:и|я|ю)|номенклатур|лежат|остат(?:ок|ки|ка|ков))/iu.test(text);
|
||||
}
|
||||
function omitAmountFromInventoryTraceEvidence(lines) {
|
||||
return lines.map((line) => String(line ?? "").replace(/\s+\|\s+сумма:\s+[^|]+(?=\s+\||$)/gu, ""));
|
||||
}
|
||||
@@ -131,6 +178,36 @@ function sumInventoryRowAmount(rows) {
|
||||
function sumInventoryRowQuantity(rows) {
|
||||
return rows.reduce((sum, row) => sum + (typeof row.quantity === "number" && Number.isFinite(row.quantity) ? row.quantity : 0), 0);
|
||||
}
|
||||
function cleanInventoryScopeHint(value) {
|
||||
const cleaned = String(value ?? "").replace(/\s+/gu, " ").trim();
|
||||
return cleaned.length > 0 ? cleaned : null;
|
||||
}
|
||||
function inventoryOnHandAccountScopeLabel(options) {
|
||||
const accountHint = cleanInventoryScopeHint(options.accountHint);
|
||||
if (accountHint) {
|
||||
return `по счету ${accountHint}`;
|
||||
}
|
||||
const text = `${options.rawUserMessage ?? ""} ${options.userMessage ?? ""}`;
|
||||
return /(?:^|[^\d])41(?:[.,]\d+)?(?:[^\d]|$)|41\s*сч[её]т/iu.test(text) ? "по счету 41" : null;
|
||||
}
|
||||
function inventoryOnHandWarehouseScopeLabel(options, uniqueWarehouses) {
|
||||
const warehouseHint = cleanInventoryScopeHint(options.warehouseHint);
|
||||
if (warehouseHint) {
|
||||
return `по складу ${warehouseHint}`;
|
||||
}
|
||||
const text = `${options.rawUserMessage ?? ""} ${options.userMessage ?? ""}`;
|
||||
if (uniqueWarehouses.length === 1 && /(?:по|на)\s+склад[уе]?/iu.test(text)) {
|
||||
return `по складу ${uniqueWarehouses[0]}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function inventoryOnHandScopePhrase(options, uniqueWarehouses) {
|
||||
const scopes = [
|
||||
inventoryOnHandAccountScopeLabel(options),
|
||||
inventoryOnHandWarehouseScopeLabel(options, uniqueWarehouses)
|
||||
].filter((item) => Boolean(item));
|
||||
return scopes.length > 0 ? scopes.join(" и ") : "на складе";
|
||||
}
|
||||
function formatInventoryPercent(value, formatNumberWithDots) {
|
||||
return value === null || !Number.isFinite(value) ? "не подтверждена" : `${formatNumberWithDots(value, 2)}%`;
|
||||
}
|
||||
@@ -222,9 +299,10 @@ function composeInventoryReply(intent, rows, options, deps) {
|
||||
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 totalAmount = positions.reduce((sum, item) => sum + item.amount, 0);
|
||||
const scopePhrase = inventoryOnHandScopePhrase(options, uniqueWarehouses);
|
||||
const directAnswerLine = positions.length > 0
|
||||
? `На ${deps.formatDateRu(asOfDate)} на складе подтверждено ${deps.formatNumberWithDots(positions.length)} позиций на ${deps.formatMoneyRub(totalAmount)}.`
|
||||
: `На ${deps.formatDateRu(asOfDate)} подтвержденных товарных остатков по счету 41.01 не найдено.`;
|
||||
? `На ${deps.formatDateRu(asOfDate)} ${scopePhrase} подтверждено ${deps.formatNumberWithDots(positions.length)} позиций на ${deps.formatMoneyRub(totalAmount)}.`
|
||||
: `На ${deps.formatDateRu(asOfDate)} ${scopePhrase} подтвержденных товарных остатков не найдено.`;
|
||||
const lines = [directAnswerLine];
|
||||
if (positions.length > 0) {
|
||||
const visiblePositionsLimit = 6;
|
||||
@@ -250,13 +328,22 @@ function composeInventoryReply(intent, rows, options, deps) {
|
||||
"- На дату среза товары с ненулевым остатком не найдены."
|
||||
]);
|
||||
}
|
||||
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Сводка:", [
|
||||
const summaryBullets = [
|
||||
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
||||
`Позиции с остатком: ${deps.formatNumberWithDots(positions.length)}.`,
|
||||
`Уникальных товаров: ${deps.formatNumberWithDots(uniqueItems.length)}.`,
|
||||
`Уникальных складов: ${deps.formatNumberWithDots(uniqueWarehouses.length)}.`,
|
||||
"Общее количество не свожу в один управленческий показатель, потому что в остатках смешаны разнородные позиции."
|
||||
]);
|
||||
];
|
||||
const accountScope = inventoryOnHandAccountScopeLabel(options);
|
||||
const warehouseScope = inventoryOnHandWarehouseScopeLabel(options, uniqueWarehouses);
|
||||
if (accountScope) {
|
||||
summaryBullets.splice(1, 0, `Срез ${accountScope}.`);
|
||||
}
|
||||
if (warehouseScope) {
|
||||
summaryBullets.splice(accountScope ? 2 : 1, 0, `Срез ${warehouseScope}.`);
|
||||
}
|
||||
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Сводка:", summaryBullets);
|
||||
if (rows.length !== positions.length) {
|
||||
lines.push(`- Проверенных строк движения: ${deps.formatNumberWithDots(rows.length)}.`);
|
||||
}
|
||||
@@ -398,49 +485,111 @@ function composeInventoryReply(intent, rows, options, deps) {
|
||||
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
|
||||
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
|
||||
normalizeInventoryTraceSummaryCounterparties(summary);
|
||||
const unresolvedRows = purchaseRows.filter((row) => deps.extractInventoryCounterpartyCandidates(row).length === 0);
|
||||
const unresolvedSupplierQuestion = /(?:\u0431\u0435\u0437\s+\u043f\u043e\u043d\u044f\u0442\u043d[^\s]*\s+\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0431\u0435\u0437\s+(?:\u044f\u0432\u043d[^\s]*\s+)?\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043d\u0435\s+\u0438\u043c\u0435\u044e\u0442\s+\u044f\u0432\u043d[^\s]*\s+\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043d\u0435\u0442\s+\u044f\u0432\u043d[^\s]*\s+\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|unresolved\s+supplier\s+link)/iu.test(String(options.userMessage ?? ""));
|
||||
const purchaseRowCounterparties = purchaseRows.map((row) => deps.uniqueStrings([
|
||||
...deps.extractInventoryCounterpartyCandidates(row),
|
||||
...extractInventoryCounterpartiesFromEvidenceLines(deps.formatInventoryTraceRows([row], 1))
|
||||
]));
|
||||
const observedCounterparties = deps.uniqueStrings([
|
||||
...summary.counterparties,
|
||||
...purchaseRowCounterparties.flat()
|
||||
]);
|
||||
const unresolvedRows = purchaseRows.filter((_, index) => (purchaseRowCounterparties[index]?.length ?? 0) === 0);
|
||||
const unresolvedSupplierQuestionText = `${String(options.userMessage ?? "")}\n${String(options.rawUserMessage ?? "")}`;
|
||||
const unresolvedSupplierQuestion = /(?:\u0431\u0435\u0437\s+\u043f\u043e\u043d\u044f\u0442\u043d[^\s]*\s+\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0431\u0435\u0437\s+(?:\u044f\u0432\u043d[^\s]*\s+)?\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043d\u0435\s+\u0438\u043c\u0435\u044e\u0442\s+\u044f\u0432\u043d[^\s]*\s+\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043d\u0435\u0442\s+\u044f\u0432\u043d[^\s]*\s+\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|unresolved\s+supplier\s+link)/iu.test(unresolvedSupplierQuestionText);
|
||||
if (unresolvedSupplierQuestion) {
|
||||
const unresolvedItems = collectInventoryTraceItems(unresolvedRows);
|
||||
const unresolvedItemPreviewLimit = 3;
|
||||
const unresolvedDocumentPreviewLimit = 2;
|
||||
const directAnswerLine = unresolvedRows.length > 0
|
||||
? `В текущем складском срезе найдено операций без явно выделенного поставщика: ${deps.formatNumberWithDots(unresolvedRows.length)}.`
|
||||
? unresolvedItems.length > 0
|
||||
? `В текущем складском срезе на ${deps.formatDateRu(asOfDate)} найдено ${deps.formatNumberWithDots(unresolvedItems.length)} позиций без надежной привязки к поставщику; первые примеры ниже.`
|
||||
: `В текущем складском срезе на ${deps.formatDateRu(asOfDate)} найдено операций без явно выделенного поставщика: ${deps.formatNumberWithDots(unresolvedRows.length)}.`
|
||||
: "В текущем складском срезе товары без явно выделенной привязки к поставщику в доступных данных не найдены.";
|
||||
const lines = [directAnswerLine];
|
||||
if (unresolvedItems.length > 0) {
|
||||
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Позиции без надежной привязки к поставщику:", unresolvedItems.slice(0, unresolvedItemPreviewLimit).map((item, index) => `${index + 1}. ${item}`));
|
||||
if (unresolvedItems.length > unresolvedItemPreviewLimit) {
|
||||
lines.push(`- Показаны первые ${unresolvedItemPreviewLimit} из ${deps.formatNumberWithDots(unresolvedItems.length)} таких позиций.`);
|
||||
}
|
||||
}
|
||||
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Что проверили:", [
|
||||
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
||||
`Закупочных операций в выборке: ${deps.formatNumberWithDots(purchaseRows.length)}.`,
|
||||
`Операций без явно выделенного поставщика: ${deps.formatNumberWithDots(unresolvedRows.length)}.`,
|
||||
`Поставщиков, выделенных в остальных операциях: ${deps.formatNumberWithDots(summary.counterparties.length)}.`
|
||||
`Поставщиков, выделенных в остальных операциях: ${deps.formatNumberWithDots(observedCounterparties.length)}.`
|
||||
]);
|
||||
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Ограничения:", [
|
||||
"Без партионного учета это проверка доступного закупочного следа по складскому срезу, а не юридическое доказательство владельца каждой партии."
|
||||
]);
|
||||
if (unresolvedRows.length > 0) {
|
||||
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Позиции без явно выделенного поставщика:", deps.formatInventoryTraceRows(unresolvedRows, 12));
|
||||
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Опорные документы:", deps.formatInventoryTraceRows(unresolvedRows, unresolvedDocumentPreviewLimit));
|
||||
if (unresolvedRows.length > unresolvedDocumentPreviewLimit) {
|
||||
lines.push(`- Показаны первые ${unresolvedDocumentPreviewLimit} из ${deps.formatNumberWithDots(unresolvedRows.length)} строк без выделенного поставщика.`);
|
||||
}
|
||||
}
|
||||
else if (summary.counterparties.length > 0) {
|
||||
lines.push(`- В доступном закупочном следе встречаются поставщики: ${summary.counterparties.slice(0, 6).join("; ")}.`);
|
||||
else if (observedCounterparties.length > 0) {
|
||||
lines.push(`- В доступном закупочном следе встречаются поставщики: ${observedCounterparties.slice(0, 6).join("; ")}.`);
|
||||
}
|
||||
return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(unresolvedRows.length > 0 ? "medium" : "strong", true));
|
||||
}
|
||||
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} поставщик в доступных данных не выделен.`;
|
||||
const purchasePeriodLabel = summary.firstPeriod && summary.lastPeriod
|
||||
? `${deps.inventoryTraceDateLabel(summary.firstPeriod)}..${deps.inventoryTraceDateLabel(summary.lastPeriod)}`
|
||||
: "даты закупки не выделены";
|
||||
const supplierScopedItems = collectInventoryTraceItems(purchaseRows);
|
||||
const supplierScopedItemQuestion = hasSupplierScopedItemListQuestion(options);
|
||||
if (supplierScopedItemQuestion) {
|
||||
const requestedSupplierLabel = summary.counterparties[0] ?? String(options.counterpartyHint ?? "").trim();
|
||||
const directAnswerLine = supplierScopedItems.length > 0
|
||||
? `По поставщику ${requestedSupplierLabel} в складском срезе ${warehouseLabel} на ${deps.formatDateRu(asOfDate)} подтверждены позиции: ${inventoryItemLeadList(supplierScopedItems)}.`
|
||||
: `По поставщику ${requestedSupplierLabel} в складском срезе ${warehouseLabel} на ${deps.formatDateRu(asOfDate)} позиции не подтверждены в доступном закупочном следе.`;
|
||||
const lines = [directAnswerLine];
|
||||
if (supplierScopedItems.length > 0) {
|
||||
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Позиции:", supplierScopedItems.slice(0, 8).map((item, index) => `${index + 1}. ${item}`));
|
||||
if (supplierScopedItems.length > 8) {
|
||||
lines.push(`- Показаны первые 8 из ${deps.formatNumberWithDots(supplierScopedItems.length)} позиций.`);
|
||||
}
|
||||
}
|
||||
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Что проверили:", [
|
||||
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
||||
`Период найденного закупочного следа: ${purchasePeriodLabel}.`,
|
||||
`Закупочных документов / операций в выборке: ${deps.formatNumberWithDots(summary.documents.length)} / ${deps.formatNumberWithDots(purchaseRows.length)}.`
|
||||
]);
|
||||
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Ограничения:", [
|
||||
"Без партионного учета этот ответ показывает закупочный след текущего остатка, но не доказывает владельца каждой конкретной партии."
|
||||
]);
|
||||
if (observedCounterparties.length > 0) {
|
||||
lines.push(`- Найденные поставщики: ${inventoryPartyLeadList(observedCounterparties)}.`);
|
||||
}
|
||||
if (purchaseRows.length > 0) {
|
||||
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Опорные документы:", deps.formatInventoryTraceRows(purchaseRows, INVENTORY_TRACE_EVIDENCE_ROW_LIMIT));
|
||||
if (purchaseRows.length > INVENTORY_TRACE_EVIDENCE_ROW_LIMIT) {
|
||||
lines.push(`- Показаны первые ${INVENTORY_TRACE_EVIDENCE_ROW_LIMIT} из ${deps.formatNumberWithDots(purchaseRows.length)} найденных строк; полный след остается в подтвержденном срезе.`);
|
||||
}
|
||||
}
|
||||
return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(purchaseRows.length > 0 ? (supplierScopedItems.length > 0 ? "strong" : "medium") : "medium", purchaseRows.length > 0));
|
||||
}
|
||||
const directAnswerLine = observedCounterparties.length === 1
|
||||
? `По складскому остатку ${warehouseLabel} на ${deps.formatDateRu(asOfDate)} выявлен поставщик закупочного следа: ${observedCounterparties[0]}.`
|
||||
: observedCounterparties.length > 1
|
||||
? `По складскому остатку ${warehouseLabel} на ${deps.formatDateRu(asOfDate)} найдено несколько поставщиков закупочного следа: ${inventoryPartyLeadList(observedCounterparties)}.`
|
||||
: unresolvedRows.length > 0
|
||||
? `По складскому остатку ${warehouseLabel} на ${deps.formatDateRu(asOfDate)} однозначный поставщик текущего остатка не подтвержден: в ${deps.formatNumberWithDots(unresolvedRows.length)} закупочных операциях поставщик не выделен.`
|
||||
: `По складскому остатку ${warehouseLabel} на ${deps.formatDateRu(asOfDate)} поставщик в доступных данных не выделен.`;
|
||||
const lines = [directAnswerLine];
|
||||
if (unresolvedRows.length > 0) {
|
||||
lines.push(`По складскому остатку ${warehouseLabel} на ${deps.formatDateRu(asOfDate)} однозначная атрибуция части текущего остатка не подтверждена: ${deps.formatNumberWithDots(unresolvedRows.length)} закупочных операций без явно выделенного поставщика.`);
|
||||
}
|
||||
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Что проверили:", [
|
||||
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
||||
`Первая найденная дата закупки: ${deps.inventoryTraceDateLabel(summary.firstPeriod)}.`,
|
||||
`Последняя найденная дата закупки: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`,
|
||||
`Закупочных документов в выборке: ${deps.formatNumberWithDots(summary.documents.length)}.`,
|
||||
`Закупочных операций в выборке: ${deps.formatNumberWithDots(purchaseRows.length)}.`
|
||||
`Период найденного закупочного следа: ${purchasePeriodLabel}.`,
|
||||
`Закупочных документов / операций в выборке: ${deps.formatNumberWithDots(summary.documents.length)} / ${deps.formatNumberWithDots(purchaseRows.length)}.`
|
||||
]);
|
||||
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Ограничения:", [
|
||||
"Без партионного учета этот ответ показывает закупочный след текущего остатка, но не доказывает владельца каждой конкретной партии."
|
||||
]);
|
||||
if (summary.counterparties.length > 0) {
|
||||
lines.push(`- Найденные поставщики: ${summary.counterparties.slice(0, 6).join("; ")}.`);
|
||||
if (observedCounterparties.length > 0) {
|
||||
lines.push(`- Найденные поставщики: ${inventoryPartyLeadList(observedCounterparties)}.`);
|
||||
}
|
||||
else if (purchaseRows.length > 0) {
|
||||
lines.push("- Закупочные движения найдены, но поставщик не выделен отдельным полем в доступных данных.");
|
||||
@@ -448,11 +597,11 @@ function composeInventoryReply(intent, rows, options, deps) {
|
||||
else {
|
||||
lines.push("- В доступных данных не найдено закупочных движений по выбранному складскому срезу.");
|
||||
}
|
||||
if (unresolvedRows.length > 0) {
|
||||
lines.push(`- Операций без явно выделенного поставщика: ${deps.formatNumberWithDots(unresolvedRows.length)}.`);
|
||||
}
|
||||
if (purchaseRows.length > 0) {
|
||||
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Опорные документы:", deps.formatInventoryTraceRows(purchaseRows, 10));
|
||||
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Опорные документы:", deps.formatInventoryTraceRows(purchaseRows, INVENTORY_TRACE_EVIDENCE_ROW_LIMIT));
|
||||
if (purchaseRows.length > INVENTORY_TRACE_EVIDENCE_ROW_LIMIT) {
|
||||
lines.push(`- Показаны первые ${INVENTORY_TRACE_EVIDENCE_ROW_LIMIT} из ${deps.formatNumberWithDots(purchaseRows.length)} найденных строк; полный след остается в подтвержденном срезе.`);
|
||||
}
|
||||
}
|
||||
return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(purchaseRows.length > 0 ? (summary.counterparties.length > 0 ? "strong" : "medium") : "medium", purchaseRows.length > 0));
|
||||
}
|
||||
@@ -770,17 +919,29 @@ function composeInventoryReply(intent, rows, options, deps) {
|
||||
const supplierMatches = inventoryRequestedPartyMatches(requestedSupplier, purchaseSummary.counterparties);
|
||||
const buyerMatches = inventoryRequestedPartyMatches(requestedBuyer, saleSummary.counterparties);
|
||||
const mismatchParts = [];
|
||||
if (requestedSupplier && purchaseRows.length > 0 && !supplierMatches) {
|
||||
if (requestedSupplier && purchaseRows.length === 0) {
|
||||
mismatchParts.push(`закупка у запрошенного поставщика ${requestedSupplier} не найдена`);
|
||||
}
|
||||
else if (requestedSupplier && purchaseRows.length > 0 && !supplierMatches) {
|
||||
mismatchParts.push(`запрошенный поставщик ${requestedSupplier} не совпал с найденным поставщиком: ${inventoryPartyListOrUnknown(purchaseSummary.counterparties)}`);
|
||||
}
|
||||
if (requestedBuyer && saleRows.length > 0 && !buyerMatches) {
|
||||
if (requestedBuyer && saleRows.length === 0) {
|
||||
mismatchParts.push(`продажа/выбытие запрошенному покупателю ${requestedBuyer} не найдены`);
|
||||
}
|
||||
else if (requestedBuyer && saleRows.length > 0 && !buyerMatches) {
|
||||
mismatchParts.push(`запрошенный покупатель ${requestedBuyer} не совпал с найденным покупателем: ${inventoryPartyListOrUnknown(saleSummary.counterparties)}`);
|
||||
}
|
||||
const directAnswerLine = mismatchParts.length > 0
|
||||
? `Запрошенная цепочка по товару ${itemLabel} полностью не подтверждена: ${mismatchParts.join("; ")}.`
|
||||
: purchaseSummary.counterparties.length === 1 && saleSummary.counterparties.length === 1
|
||||
: purchaseRows.length > 0 && saleRows.length > 0 && purchaseSummary.counterparties.length === 1 && saleSummary.counterparties.length === 1
|
||||
? `По товару ${itemLabel} цепочка поставки и продажи связана с поставщиком ${purchaseSummary.counterparties[0]} и покупателем ${saleSummary.counterparties[0]}.`
|
||||
: `По товару ${itemLabel} цепочка поставки и продажи подтверждена частично или разнообразно: детали идут следом.`;
|
||||
: purchaseRows.length > 0 && saleRows.length > 0
|
||||
? `По товару ${itemLabel} найдены обе стороны цепочки поставки и продажи; участников несколько или они неоднозначны, детали ниже.`
|
||||
: purchaseRows.length > 0
|
||||
? `По товару ${itemLabel} закупочная часть цепочки подтверждена, но продажа/выбытие со счета 41.01 в доступных данных не найдены.`
|
||||
: saleRows.length > 0
|
||||
? `По товару ${itemLabel} найдена продажа/выбытие со счета 41.01, но закупочная часть цепочки в доступных данных не подтверждена.`
|
||||
: `По товару ${itemLabel} цепочка поставки и продажи не подтверждена: движений по счету 41.01 в доступных данных не найдено.`;
|
||||
const lines = [directAnswerLine, "", "Подтверждение:"];
|
||||
lines.push(`- Строк закупки на 41.01: ${deps.formatNumberWithDots(purchaseRows.length)}.`);
|
||||
lines.push(`- Строк продажи со счета 41.01: ${deps.formatNumberWithDots(saleRows.length)}.`);
|
||||
|
||||
+16
-5
@@ -133,11 +133,17 @@ function applyAddressLlmSemanticHintsToExtraction(extraction, semanticHintsInput
|
||||
semanticFrame.anchor_value = scopeTargetText;
|
||||
}
|
||||
if (semanticHints.scope_target_kind === "counterparty" && scopeTargetText) {
|
||||
extractedFilters.counterparty = scopeTargetText;
|
||||
pushWarning(warnings, "counterparty_from_llm_semantics");
|
||||
semanticFrame.scope_kind = "explicit_anchor";
|
||||
semanticFrame.anchor_kind = "counterparty";
|
||||
semanticFrame.anchor_value = scopeTargetText;
|
||||
if ((0, addressFilterExtractor_1.isLowQualityCounterpartyAnchorValue)(scopeTargetText)) {
|
||||
delete extractedFilters.counterparty;
|
||||
pushWarning(warnings, "counterparty_cleared_low_quality_llm_semantics");
|
||||
}
|
||||
else {
|
||||
extractedFilters.counterparty = scopeTargetText;
|
||||
pushWarning(warnings, "counterparty_from_llm_semantics");
|
||||
semanticFrame.scope_kind = "explicit_anchor";
|
||||
semanticFrame.anchor_kind = "counterparty";
|
||||
semanticFrame.anchor_value = scopeTargetText;
|
||||
}
|
||||
}
|
||||
if (semanticHints.scope_target_kind === "contract" && scopeTargetText) {
|
||||
extractedFilters.contract = scopeTargetText;
|
||||
@@ -159,6 +165,11 @@ function applyAddressLlmSemanticHintsToExtraction(extraction, semanticHintsInput
|
||||
pushWarning(warnings, "item_llm_semantics_ignored");
|
||||
}
|
||||
}
|
||||
if (toNonEmptyString(extractedFilters.counterparty) &&
|
||||
(0, addressFilterExtractor_1.isLowQualityCounterpartyAnchorValue)(String(extractedFilters.counterparty))) {
|
||||
delete extractedFilters.counterparty;
|
||||
pushWarning(warnings, "counterparty_cleared_low_quality_llm_semantics");
|
||||
}
|
||||
return {
|
||||
...extraction,
|
||||
extracted_filters: extractedFilters,
|
||||
|
||||
Reference in New Issue
Block a user