АРЧ - Склад: сохранять полный item-anchor в selected-object sale trace и закрепить buyer follow-up регрессиями

This commit is contained in:
2026-04-15 17:57:05 +03:00
parent 7a6d8eb070
commit f911f9893b
27 changed files with 1539 additions and 180 deletions
@@ -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 &&