ДОМЕНЫ - ВОПРОСЫ - СКЛАД - Систематизировать ответы по поставщику без поставок через анализ оплат и возвратов

This commit is contained in:
2026-04-17 02:08:02 +03:00
parent a3a61b3a0f
commit 44f1c1e11e
33 changed files with 22512 additions and 173 deletions
@@ -2440,6 +2440,12 @@ function findRecentAddressFilterValue(items, key) {
if (!isAddressLaneDebugPayload(debug)) {
continue;
}
const replyType = toNonEmptyString(item.reply_type);
const limitedReasonCategory = toNonEmptyString(debug.limited_reason_category);
if ((replyType && replyType !== "factual" && replyType !== "factual_with_explanation") ||
limitedReasonCategory) {
continue;
}
const directFilterValue = readAddressFilterString(debug, key);
if (directFilterValue) {
return directFilterValue;
@@ -2735,8 +2741,10 @@ function hasShortDebtMirrorFollowupSignal(userMessage) {
return false;
}
return samples.some((sample) => /^(?:а|a|и|i)\s+(?:нам\s+)?кто(?=$|[\s,.;:!?])/iu.test(sample) ||
/^(?:а|a|и|i)\s+нам(?=$|[\s,.;:!?])/iu.test(sample) ||
/^(?:а|a|и|i)\s+(?:мы\s+)?кому(?=$|[\s,.;:!?])/iu.test(sample) ||
/^(?:р°|a|рё|i)\s+(?:рЅр°рј\s+)?рєс‚рѕ(?=$|[\s,.;:!?])/iu.test(sample) ||
/^(?:р°|a|рё|i)\s+рЅр°рј(?=$|[\s,.;:!?])/iu.test(sample) ||
/^(?:р°|a|рё|i)\s+(?:рјс‹\s+)?рєрѕрјсѓ(?=$|[\s,.;:!?])/iu.test(sample));
}
function isInventorySelectedObjectIntent(intent) {
@@ -2854,6 +2862,7 @@ function resolveDebtRoleSwapFollowupIntent(userMessage, previousIntent) {
}
const hasReceivablesCue = /(?:нам\s+кто\s+долж|кто\s+нам\s+долж|кто\s+долж[а-яё]*\s+нам|дебитор|к\s+получению|к\s+взысканию|receivable)/iu.test(normalized) ||
/^(?:а|a|и|i)\s+(?:нам\s+)?кто(?=$|[\s,.;:!?])/iu.test(normalized) ||
/^(?:а|a|и|i)\s+нам(?=$|[\s,.;:!?])/iu.test(normalized) ||
/^(?:р°|a|рё|i)\s+(?:рЅр°рј\s+)?рєс‚рѕ(?=$|[\s,.;:!?])/iu.test(normalized);
const hasPayablesCue = /(?:мы\s+кому\s+долж|кому\s+мы\s+долж|кому\s+долж[а-яё]*\s+мы|кредитор|к\s+уплате|payable)/iu.test(normalized) ||
/^(?:а|a|и|i)\s+(?:мы\s+)?кому(?=$|[\s,.;:!?])/iu.test(normalized) ||
@@ -2868,6 +2877,49 @@ function resolveDebtRoleSwapFollowupIntent(userMessage, previousIntent) {
}
return null;
}
function hasExplicitOrganizationScopeCue(text) {
return /(?:(?:^|[\s"'«»„“()\\\/])(?:ооо|ао|пао|зао|оао|ип|гку|муп|гуп|llc|ltd|inc|corp)(?=$|[\s"'«»„“()\\\/.,;:]))|организац|компан|контор|фирм/u.test(String(text ?? "").toLowerCase());
}
function looksLikeBareCounterpartyScopeTarget(text) {
const normalized = compactWhitespace(String(text ?? "").toLowerCase());
if (!normalized) {
return false;
}
if (hasExplicitOrganizationScopeCue(normalized)) {
return false;
}
if (/[0-9]/u.test(normalized)) {
return false;
}
const tokens = normalized.split(/\s+/u).filter(Boolean);
if (tokens.length === 0 || tokens.length > 4) {
return false;
}
return tokens.every((token) => /^[a-zа-яё._-]{2,}$/iu.test(token));
}
function shouldDowngradeOrganizationSemanticHint(scopeTargetText, fragmentText) {
const target = toNonEmptyString(scopeTargetText);
if (!target) {
return false;
}
if (hasExplicitOrganizationScopeCue(target) || hasExplicitOrganizationScopeCue(fragmentText)) {
return false;
}
return looksLikeBareCounterpartyScopeTarget(target);
}
function shouldKeepPreviousIntentForShortCounterpartyRetarget(userMessage, sourceIntent) {
const normalized = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase());
if (!normalized || countTokens(normalized) > 4) {
return false;
}
if (sourceIntent !== "list_documents_by_counterparty" && sourceIntent !== "list_documents_by_contract") {
return false;
}
if (/(?:банк|выписк|плат[её]ж|оплат|списан|поступлен|bank|payment|wire|statement)/iu.test(normalized)) {
return false;
}
return /^(?:а|и|ну)?\s*по\s+[a-zа-яё0-9._-]{2,}(?:\s+[a-zа-яё0-9._-]{2,})?$/iu.test(normalized);
}
function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMessage = null, llmPreDecomposeMeta = null, addressNavigationState = null) {
const previousAddressItem = findLastAddressAssistantItem(items);
const previousAddressDebug = previousAddressItem?.debug ?? null;
@@ -2988,7 +3040,8 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
const suggestedIntent = Array.isArray(followupOffer?.suggested_intents)
? toNonEmptyString(followupOffer.suggested_intents[0])
: null;
if (suggestedIntent) {
const keepPreviousIntent = shouldKeepPreviousIntentForShortCounterpartyRetarget(userMessage, sourceIntent);
if (suggestedIntent && !keepPreviousIntent) {
previousIntent = suggestedIntent;
followupSelectionMode = "switch_to_suggested_intent";
}
@@ -3086,13 +3139,20 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
let previousFilters = previousFiltersRaw && typeof previousFiltersRaw === "object"
? { ...previousFiltersRaw }
: {};
if (!toNonEmptyString(previousFilters.contract)) {
const shouldBackfillHistoricalPartyAnchors =
sourceIntentHint === "list_contracts_by_counterparty" ||
sourceIntentHint === "list_documents_by_counterparty" ||
sourceIntentHint === "bank_operations_by_counterparty" ||
sourceIntentHint === "list_documents_by_contract" ||
sourceIntentHint === "bank_operations_by_contract" ||
sourceIntentHint === "open_items_by_counterparty_or_contract";
if (shouldBackfillHistoricalPartyAnchors && !toNonEmptyString(previousFilters.contract)) {
const historicalContract = findRecentAddressFilterValue(items, "contract");
if (historicalContract) {
previousFilters.contract = historicalContract;
}
}
if (!toNonEmptyString(previousFilters.counterparty)) {
if (shouldBackfillHistoricalPartyAnchors && !toNonEmptyString(previousFilters.counterparty)) {
const historicalCounterparty = findRecentAddressFilterValue(items, "counterparty");
if (historicalCounterparty) {
previousFilters.counterparty = historicalCounterparty;
@@ -3138,7 +3198,10 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
const rootContextOnlyPivot = Boolean((isInventorySelectedObjectIntent(sourceIntentHint) || currentFrameKind === "inventory_drilldown") &&
hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage));
const inventoryRootTemporalPivot = Boolean(inventoryRootFrame &&
(isInventorySelectedObjectIntent(sourceIntentHint) || currentFrameKind === "inventory_drilldown") &&
(isInventorySelectedObjectIntent(sourceIntentHint) ||
isInventoryRootFrameIntent(sourceIntentHint) ||
currentFrameKind === "inventory_drilldown" ||
currentFrameKind === "inventory_root") &&
(hasInventoryRootTemporalFollowupPrimary || hasInventoryRootTemporalFollowupAlternate) &&
!hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage));
const rootScopedPivot = rootContextOnlyPivot || inventoryRootTemporalPivot;
@@ -3212,19 +3275,34 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
if (!previousIntent && !previousAnchor && Object.keys(previousFilters).length === 0) {
return null;
}
const shouldAttachInventoryRootFrame = Boolean(inventoryRootFrame &&
(rootScopedPivot ||
isInventoryRootFrameIntent(sourceIntentHint) ||
isInventorySelectedObjectIntent(sourceIntentHint) ||
hasNavigationInventoryItemFocusHint ||
inventoryShortFollowupPrimary ||
inventoryShortFollowupAlternate ||
hasInventoryRootTemporalFollowupPrimary ||
hasInventoryRootTemporalFollowupAlternate ||
hasSelectedObjectInventorySignalPrimary ||
hasSelectedObjectInventorySignalAlternate));
const carryoverTargetIntent = followupSelectionMode === "carry_root_context"
? inventoryRootFrame?.intent ?? explicitIntent ?? previousIntent ?? undefined
: explicitIntent ?? previousIntent ?? undefined;
return {
followupContext: {
previous_intent: previousIntent ?? undefined,
target_intent: carryoverTargetIntent,
previous_filters: previousFilters,
previous_anchor_type: previousAnchorType ?? undefined,
previous_anchor_value: previousAnchor,
resolved_counterparty_from_display: resolvedCounterpartyFromDisplay || undefined,
root_context_only: rootScopedPivot || undefined,
root_intent: inventoryRootFrame?.intent ?? undefined,
root_filters: inventoryRootFrame?.filters ?? undefined,
root_anchor_type: inventoryRootFrame?.anchorType ?? undefined,
root_anchor_value: inventoryRootFrame?.anchorValue ?? undefined,
current_frame_kind: currentFrameKind ?? undefined
root_intent: shouldAttachInventoryRootFrame ? inventoryRootFrame?.intent ?? undefined : undefined,
root_filters: shouldAttachInventoryRootFrame ? inventoryRootFrame?.filters ?? undefined : undefined,
root_anchor_type: shouldAttachInventoryRootFrame ? inventoryRootFrame?.anchorType ?? undefined : undefined,
root_anchor_value: shouldAttachInventoryRootFrame ? inventoryRootFrame?.anchorValue ?? undefined : undefined,
current_frame_kind: shouldAttachInventoryRootFrame ? currentFrameKind ?? undefined : undefined
},
previousAddressIntent: previousIntent,
previousAddressAnchor: previousAnchor,
@@ -3298,7 +3376,7 @@ function isRetryableAddressLimitedResult(addressLane) {
return false;
}
const category = String(addressLane?.debug?.limited_reason_category ?? "").trim().toLowerCase();
return category === "missing_anchor" || category === "empty_match";
return category === "missing_anchor" || category === "empty_match" || category === "unsupported";
}
function isAddressLlmPreDecomposeCandidate(userMessage) {
const repaired = repairAddressMojibake(String(userMessage ?? ""));
@@ -3317,13 +3395,19 @@ function normalizeAddressSemanticHintsFromFragment(fragment) {
return null;
}
const scopeTargetKind = toNonEmptyString(hints.scope_target_kind);
const scopeTargetText = toNonEmptyString(hints.scope_target_text);
const fragmentText = toNonEmptyString(fragment.raw_fragment_text) ?? toNonEmptyString(fragment.normalized_fragment_text) ?? "";
const normalizedScopeTargetKind = scopeTargetKind === "organization" &&
shouldDowngradeOrganizationSemanticHint(scopeTargetText, fragmentText)
? "counterparty"
: scopeTargetKind;
const dateScopeKind = toNonEmptyString(hints.date_scope_kind);
return {
scope_target_kind: scopeTargetKind ?? "none",
scope_target_text: toNonEmptyString(hints.scope_target_text),
scope_target_kind: normalizedScopeTargetKind ?? "none",
scope_target_text: scopeTargetText,
date_scope_kind: dateScopeKind ?? "missing",
self_scope_detected: hints.self_scope_detected === true || scopeTargetKind === "self_scope",
selected_object_scope_detected: hints.selected_object_scope_detected === true || scopeTargetKind === "selected_object"
self_scope_detected: hints.self_scope_detected === true || normalizedScopeTargetKind === "self_scope",
selected_object_scope_detected: hints.selected_object_scope_detected === true || normalizedScopeTargetKind === "selected_object"
};
}
function extractAddressPredecomposeCandidateFromFragments(fragments) {