ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Спека exact-маршрута payables на дату: confirmed_balance без эвристического финала

This commit is contained in:
2026-04-12 13:46:14 +03:00
parent ca2feab893
commit 1b2ee93176
19 changed files with 2453 additions and 252 deletions
@@ -1414,6 +1414,11 @@ function buildAddressDebugPayload(addressDebug, llmPreDecomposeMeta = null) {
runtime_readiness: addressDebug.runtime_readiness,
limited_reason_category: addressDebug.limited_reason_category,
response_type: addressDebug.response_type,
requested_result_mode: addressDebug.requested_result_mode ?? undefined,
result_mode: addressDebug.result_mode ?? undefined,
evidence_strength: addressDebug.evidence_strength ?? undefined,
balance_confirmed: typeof addressDebug.balance_confirmed === "boolean" ? addressDebug.balance_confirmed : undefined,
as_of_date_basis: addressDebug.as_of_date_basis ?? undefined,
execution_lane: "address_query",
llm_decomposition_applied: Boolean(llmMeta?.applied),
llm_decomposition_attempted: Boolean(llmMeta?.attempted),
@@ -1542,6 +1547,19 @@ const ADDRESS_PREDECOMPOSE_NOISE_TOKENS = new Set([
"kakoi",
"vse",
"all",
"\u043f\u0443\u043d\u043a\u0442",
"\u043f\u0443\u043d\u043a\u0442\u0430",
"\u043f\u0443\u043d\u043a\u0442\u0443",
"\u043f\u0443\u043d\u043a\u0442\u043e\u043c",
"\u043f\u043e\u0437\u0438\u0446\u0438\u044f",
"\u043f\u043e\u0437\u0438\u0446\u0438\u0438",
"\u043f\u043e\u0437\u0438\u0446\u0438\u044e",
"\u0441\u0442\u0440\u043e\u043a\u0430",
"\u0441\u0442\u0440\u043e\u043a\u0438",
"\u0441\u0442\u0440\u043e\u043a\u0443",
"item",
"row",
"line",
"blya",
"blyat",
"епт",
@@ -1566,51 +1584,51 @@ const ADDRESS_FALLBACK_STRIP_TOKENS = new Set([
"please"
]);
const ADDRESS_MONTH_ALIAS_MAP = {
янв: "01",
январ: "01",
"\u044f\u043d\u0432": "01",
"\u044f\u043d\u0432\u0430\u0440": "01",
january: "01",
jan: "01",
фев: "02",
феврал: "02",
"\u0444\u0435\u0432": "02",
"\u0444\u0435\u0432\u0440\u0430\u043b": "02",
february: "02",
feb: "02",
мар: "03",
март: "03",
"\u043c\u0430\u0440": "03",
"\u043c\u0430\u0440\u0442": "03",
march: "03",
apr: "04",
апр: "04",
апрел: "04",
"\u0430\u043f\u0440": "04",
"\u0430\u043f\u0440\u0435\u043b": "04",
april: "04",
май: "05",
ма: "05",
"\u043c\u0430\u0439": "05",
"\u043c\u0430": "05",
may: "05",
июн: "06",
июнь: "06",
"\u0438\u044e\u043d": "06",
"\u0438\u044e\u043d\u044c": "06",
june: "06",
jun: "06",
июл: "07",
июль: "07",
"\u0438\u044e\u043b": "07",
"\u0438\u044e\u043b\u044c": "07",
july: "07",
jul: "07",
авг: "08",
август: "08",
"\u0430\u0432\u0433": "08",
"\u0430\u0432\u0433\u0443\u0441\u0442": "08",
august: "08",
aug: "08",
сен: "09",
сент: "09",
сентябр: "09",
"\u0441\u0435\u043d": "09",
"\u0441\u0435\u043d\u0442": "09",
"\u0441\u0435\u043d\u0442\u044f\u0431\u0440": "09",
september: "09",
sep: "09",
окт: "10",
октябр: "10",
"\u043e\u043a\u0442": "10",
"\u043e\u043a\u0442\u044f\u0431\u0440": "10",
october: "10",
oct: "10",
ноя: "11",
ноябр: "11",
"\u043d\u043e\u044f": "11",
"\u043d\u043e\u044f\u0431\u0440": "11",
november: "11",
nov: "11",
дек: "12",
декабр: "12",
"\u0434\u0435\u043a": "12",
"\u0434\u0435\u043a\u0430\u0431\u0440": "12",
december: "12",
dec: "12"
};
@@ -1839,6 +1857,10 @@ function resolveAddressDeterministicFallback(userMessage, sanitizedUserMessage)
const bankSignal = ADDRESS_BANK_SIGNAL_PATTERN.test(source);
const contractSignal = ADDRESS_CONTRACT_SIGNAL_PATTERN.test(source);
const balanceSignal = ADDRESS_BALANCE_SIGNAL_PATTERN.test(source);
const hasIndexPointerSignal = /(?:\u043f\u0443\u043d\u043a\u0442|\u043f\u043e\u0437\u0438\u0446|\u0441\u0442\u0440\u043e\u043a|item|row|line)/iu.test(sourceRaw);
if (hasIndexPointerSignal && extractDisplayedEntityIndexMention(sourceRaw) !== null) {
return null;
}
if (balanceSignal && account) {
let periodClause = "";
let rule = "balance_account_rewrite";
@@ -2157,6 +2179,14 @@ const FOLLOWUP_DISPLAY_COUNTERPARTY_LEGAL_TOKENS = new Set([
"company",
"group"
]);
const FOLLOWUP_DISPLAY_ENTITY_TYPE_BY_INTENT = {
counterparty_activity_lifecycle: "counterparty",
customer_revenue_and_payments: "counterparty",
supplier_payouts_profile: "counterparty",
counterparty_population_and_roles: "counterparty",
contract_usage_and_value: "contract",
list_contracts_by_counterparty: "contract"
};
function normalizeCounterpartyForFollowupMatch(value) {
return compactWhitespace(repairAddressMojibake(String(value ?? ""))
.toLowerCase()
@@ -2167,7 +2197,25 @@ function normalizeCounterpartyForFollowupMatch(value) {
function normalizeCounterpartyTokenForFollowupMatch(value) {
return normalizeCounterpartyForFollowupMatch(value).replace(/[._-]+/g, "");
}
function extractDisplayedCounterpartyCandidates(replyText) {
function normalizeCounterpartyStemForFollowupMatch(value) {
const compact = normalizeCounterpartyTokenForFollowupMatch(value);
if (!compact || !/[а-яё]/iu.test(compact)) {
return compact;
}
const stem = compact.replace(/(?:иями|ями|ами|ией|ей|ий|ов|ев|ом|ем|ах|ях|ую|юю|ая|яя|ое|ее|ые|ие|ого|его|ому|ему|ыми|ими|ым|им|ам|ям|у|ю|а|я|е|и|ы|о)$/iu, "");
return stem.length >= 3 ? stem : compact;
}
function inferDisplayedEntityTypeFromIntent(intent) {
const normalized = compactWhitespace(String(intent ?? "").toLowerCase());
if (!normalized) {
return "unknown";
}
return FOLLOWUP_DISPLAY_ENTITY_TYPE_BY_INTENT[normalized] ?? "unknown";
}
function extractDisplayedAddressEntityCandidates(replyText, entityType = "unknown") {
if (entityType === "unknown") {
return [];
}
const lines = String(replyText ?? "").split(/\r?\n/);
const candidates = [];
for (const line of lines) {
@@ -2175,10 +2223,15 @@ function extractDisplayedCounterpartyCandidates(replyText) {
if (!compactLine) {
continue;
}
if (!/^\d+\.\s+/.test(compactLine)) {
const numberedMatch = compactLine.match(/^(\d+)\.\s+(.+)$/);
if (!numberedMatch) {
continue;
}
const afterNumber = compactLine.replace(/^\d+\.\s+/, "");
const index = Number.parseInt(String(numberedMatch[1] ?? ""), 10);
if (!Number.isFinite(index) || index <= 0) {
continue;
}
const afterNumber = String(numberedMatch[2] ?? "");
const parts = afterNumber.split("|").map((item) => compactWhitespace(item));
let counterpartyCandidate = parts[0] ?? "";
if (parts.length >= 2 && /^\d{4}-\d{2}-\d{2}/.test(parts[0] ?? "")) {
@@ -2188,9 +2241,20 @@ function extractDisplayedCounterpartyCandidates(replyText) {
if (!cleanedCandidate || cleanedCandidate.length < 2) {
continue;
}
candidates.push(cleanedCandidate);
candidates.push({
index,
value: cleanedCandidate,
entityType
});
}
return Array.from(new Set(candidates));
const dedup = new Map();
for (const candidate of candidates) {
const key = `${candidate.entityType}:${candidate.index}:${normalizeCounterpartyForFollowupMatch(candidate.value)}`;
if (!dedup.has(key)) {
dedup.set(key, candidate);
}
}
return Array.from(dedup.values());
}
function buildCounterpartyAliasesForFollowupMatch(counterpartyName) {
const aliases = new Set();
@@ -2203,13 +2267,14 @@ function buildCounterpartyAliasesForFollowupMatch(counterpartyName) {
.split(/\s+/)
.map((token) => token.trim())
.filter(Boolean);
const withoutLegalTokens = normalizedTokens
const tokensForAlias = Array.from(new Set(normalizedTokens.flatMap((token) => [token, ...token.split(/-+/).map((part) => part.trim()).filter(Boolean)])));
const withoutLegalTokens = tokensForAlias
.filter((token) => !FOLLOWUP_DISPLAY_COUNTERPARTY_LEGAL_TOKENS.has(token))
.join(" ");
if (withoutLegalTokens) {
aliases.add(withoutLegalTokens);
}
for (const token of normalizedTokens) {
for (const token of tokensForAlias) {
const compactToken = normalizeCounterpartyTokenForFollowupMatch(token);
if (compactToken.length < 3) {
continue;
@@ -2221,6 +2286,10 @@ function buildCounterpartyAliasesForFollowupMatch(counterpartyName) {
continue;
}
aliases.add(compactToken);
const stemToken = normalizeCounterpartyStemForFollowupMatch(compactToken);
if (stemToken.length >= 4) {
aliases.add(stemToken);
}
}
return Array.from(aliases)
.map((alias) => compactWhitespace(alias))
@@ -2234,31 +2303,95 @@ function hasCounterpartyAliasMention(normalizedMessage, alias) {
}
const aliasPattern = escapeRegex(trimmedAlias).replace(/\s+/g, "\\s+");
const boundaryPattern = new RegExp(`(?:^|[^a-zа-я0-9])${aliasPattern}(?:$|[^a-zа-я0-9])`, "iu");
return boundaryPattern.test(normalizedMessage);
if (boundaryPattern.test(normalizedMessage)) {
return true;
}
if (trimmedAlias.length < 4 || !/[а-яё]/iu.test(trimmedAlias)) {
return false;
}
const fuzzyPattern = new RegExp(`(?:^|[^a-zа-я0-9])${aliasPattern}[а-яё]{0,3}(?:$|[^a-zа-я0-9])`, "iu");
return fuzzyPattern.test(normalizedMessage);
}
function resolveDisplayedCounterpartyMention(userMessage, displayedCounterparties) {
function extractDisplayedEntityIndexMention(userMessage) {
const normalized = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase());
if (!normalized) {
return null;
}
const tokenStart = "(?:^|[^\\p{L}\\p{N}_])";
const tokenEnd = "(?=$|[^\\p{L}\\p{N}_])";
const pointerPattern = "(?:\\u043f\\u0443\\u043d\\u043a\\u0442(?:\\u0430|\\u0443|\\u043e\\u043c)?|\\u043f\\u043e\\u0437\\u0438\\u0446\\u0438(?:\\u044f|\\u0438|\\u044e|\\u0435\\u0439)|\\u0441\\u0442\\u0440\\u043e\\u043a(?:\\u0430|\\u0438|\\u0435|\\u0443)|item|row|line)";
const pointerSignalPattern = new RegExp(`${tokenStart}${pointerPattern}${tokenEnd}`, "iu");
const directPattern = new RegExp(`${tokenStart}${pointerPattern}${tokenEnd}\\D{0,8}(\\d{1,3})(?!\\d)`, "iu");
const directMatch = normalized.match(directPattern);
if (directMatch) {
const value = Number.parseInt(String(directMatch[1] ?? ""), 10);
return Number.isFinite(value) && value > 0 ? value : null;
}
const reversePattern = new RegExp(`${tokenStart}(\\d{1,3})(?:-?(?:\\u0439|\\u044f|\\u0435|\\u0433\\u043e|\\u043c\\u0443))?\\s+${pointerPattern}${tokenEnd}`, "iu");
const reverseMatch = normalized.match(reversePattern);
if (reverseMatch) {
const value = Number.parseInt(String(reverseMatch[1] ?? ""), 10);
return Number.isFinite(value) && value > 0 ? value : null;
}
if (pointerSignalPattern.test(normalized)) {
const numericMatches = Array.from(normalized.matchAll(/(?:^|[^\p{N}])(\d{1,3})(?!\d)/gu))
.map((match) => Number.parseInt(String(match[1] ?? ""), 10))
.filter((value) => Number.isFinite(value) && value > 0);
if (numericMatches.length === 1) {
return numericMatches[0];
}
}
return null;
}
function resolveDisplayedAddressEntityMention(userMessage, displayedEntities) {
const normalizedMessage = normalizeCounterpartyForFollowupMatch(userMessage);
if (!normalizedMessage) {
return null;
}
if (!Array.isArray(displayedCounterparties) || displayedCounterparties.length === 0) {
if (!Array.isArray(displayedEntities) || displayedEntities.length === 0) {
return null;
}
const indexMention = extractDisplayedEntityIndexMention(userMessage);
if (indexMention !== null) {
const indexedCandidate = displayedEntities.find((candidate) => Number(candidate.index) === indexMention);
if (indexedCandidate) {
return {
value: indexedCandidate.value,
entityType: indexedCandidate.entityType,
matchKind: "index",
index: indexedCandidate.index
};
}
}
let bestMatch = null;
for (const candidate of displayedCounterparties) {
const aliases = buildCounterpartyAliasesForFollowupMatch(candidate);
for (const candidate of displayedEntities) {
const aliases = buildCounterpartyAliasesForFollowupMatch(candidate.value);
for (const alias of aliases) {
if (!hasCounterpartyAliasMention(normalizedMessage, alias)) {
continue;
}
const score = alias.length * 10 + (normalizeCounterpartyForFollowupMatch(candidate) === alias ? 1 : 0);
const score = alias.length * 10 + (normalizeCounterpartyForFollowupMatch(candidate.value) === alias ? 1 : 0);
if (!bestMatch || score > bestMatch.score) {
bestMatch = { value: candidate, score };
bestMatch = {
value: candidate.value,
entityType: candidate.entityType,
index: candidate.index,
matchKind: "alias",
score
};
}
break;
}
}
return bestMatch?.value ?? null;
if (!bestMatch) {
return null;
}
return {
value: bestMatch.value,
entityType: bestMatch.entityType,
matchKind: bestMatch.matchKind,
index: bestMatch.index
};
}
function findRecentAddressFilterValue(items, key) {
for (let index = items.length - 1; index >= 0; index -= 1) {
@@ -2435,12 +2568,17 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
const hasAlternateFollowupSignal = toNonEmptyString(alternateMessage)
? hasAddressFollowupContextSignal(alternateMessage)
: false;
const hasPrimaryIndexReferenceSignal = extractDisplayedEntityIndexMention(userMessage) !== null;
const hasAlternateIndexReferenceSignal = toNonEmptyString(alternateMessage)
? extractDisplayedEntityIndexMention(String(alternateMessage ?? "")) !== null
: false;
const hasIndexReferenceSignal = hasPrimaryIndexReferenceSignal || hasAlternateIndexReferenceSignal;
const hasStandaloneAddressTopic = hasStandaloneAddressTopicSignal(userMessage) ||
(toNonEmptyString(alternateMessage) ? hasStandaloneAddressTopicSignal(alternateMessage) : false);
if (hasStandaloneAddressTopic && !hasImplicitContinuationSignal) {
if (hasStandaloneAddressTopic && !hasImplicitContinuationSignal && !hasIndexReferenceSignal) {
return null;
}
if (!hasPrimaryFollowupSignal && !hasAlternateFollowupSignal && !hasImplicitContinuationSignal) {
if (!hasPrimaryFollowupSignal && !hasAlternateFollowupSignal && !hasImplicitContinuationSignal && !hasIndexReferenceSignal) {
return null;
}
if (!previousAddressDebug) {
@@ -2487,16 +2625,24 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
previousFilters.organization = historicalOrganization;
}
}
const displayedCounterparties = extractDisplayedCounterpartyCandidates(toNonEmptyString(previousAddressItem?.text) ?? "");
const counterpartyFromFollowupText = resolveDisplayedCounterpartyMention(userMessage, displayedCounterparties) ??
const displayedEntityType = inferDisplayedEntityTypeFromIntent(sourceIntent);
const displayedEntities = extractDisplayedAddressEntityCandidates(toNonEmptyString(previousAddressItem?.text) ?? "", displayedEntityType);
const resolvedEntityFromFollowup = resolveDisplayedAddressEntityMention(userMessage, displayedEntities) ??
(toNonEmptyString(alternateMessage)
? resolveDisplayedCounterpartyMention(String(alternateMessage ?? ""), displayedCounterparties)
? resolveDisplayedAddressEntityMention(String(alternateMessage ?? ""), displayedEntities)
: null);
if (counterpartyFromFollowupText) {
previousFilters.counterparty = counterpartyFromFollowupText;
previousAnchorType = "counterparty";
previousAnchor = counterpartyFromFollowupText;
resolvedCounterpartyFromDisplay = true;
if (resolvedEntityFromFollowup) {
if (resolvedEntityFromFollowup.entityType === "counterparty") {
previousFilters.counterparty = resolvedEntityFromFollowup.value;
previousAnchorType = "counterparty";
previousAnchor = resolvedEntityFromFollowup.value;
resolvedCounterpartyFromDisplay = true;
}
else if (resolvedEntityFromFollowup.entityType === "contract") {
previousFilters.contract = resolvedEntityFromFollowup.value;
previousAnchorType = "contract";
previousAnchor = resolvedEntityFromFollowup.value;
}
if (followupSelectionMode !== "switch_to_suggested_intent") {
followupSelectionMode = "carry_referenced_entity";
}
@@ -3330,7 +3476,9 @@ function resolveAddressToolGateDecision(addressInputMessage, followupContext, ll
llmContractIntent === "unknown" &&
!followupContext &&
!hasClassifierSignal &&
!strongDataSignalFromRawMessage) {
!hasIntentSignal &&
!strongDataSignalFromRawMessage &&
!strongDataSignalFromEffectiveMessage) {
return {
runAddressLane: false,
decision: "skip_address_lane",
@@ -4466,7 +4614,7 @@ function isPlausibleOrganizationName(value) {
if (/(?:справочникссылка|документссылка|плансчетовссылка|standardodata|recordtype|cmp:)/i.test(candidate)) {
return false;
}
return /[A-Za-zА-Яа-яЁё]/u.test(candidate);
return /[A-Za-z\u0400-\u04FF]/u.test(candidate);
}
function appendOrganizationFactsFromValue(value, hints, bucket, depth = 0) {
if (depth > 4 || value === null || value === undefined) {