АДРЕСНЫЙ РЕЖИМ -ADDRESS: убран ложный factual fallback в open_items при неподтвержденном якоре + добавлен регресс-тест
This commit is contained in:
+114
-7
@@ -323,12 +323,27 @@ function cleanupAnchorValue(value) {
|
||||
.replace(/\s+(?:с|по|за)(?:\s+|$)[\s\S]*$/iu, "")
|
||||
.trim();
|
||||
}
|
||||
function cleanupContractAnchorValue(value) {
|
||||
let normalized = cleanupAnchorValue(value);
|
||||
if (!normalized) {
|
||||
return normalized;
|
||||
}
|
||||
const yearQualifierTailPattern = /\s+(?:(?:за|for)\s+)?(?:year|г(?:од|ода)?\.?)\s*(?:20\d{2}|\d{2})(?:\s+|$)[\s\S]*$/iu;
|
||||
if (yearQualifierTailPattern.test(normalized)) {
|
||||
normalized = normalized.replace(yearQualifierTailPattern, "").trim();
|
||||
}
|
||||
const trailingSeparatedYearPattern = /\s+(?:20\d{2}|\d{2})\s*(?:г(?:од|ода)?\.?|year)?$/iu;
|
||||
if (trailingSeparatedYearPattern.test(normalized) && /[\/\\-]/.test(normalized)) {
|
||||
normalized = normalized.replace(trailingSeparatedYearPattern, "").trim();
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
function hasAllTimeHint(text) {
|
||||
const value = String(text ?? "");
|
||||
return /(?:за\s+вс[её]\s+время|за\s+весь\s+период|за\s+весь\s+срок|за\s+всю\s+истори(?:ю|и)|за\s+любой\s+период|за\s+любой\s+срок|for\s+all\s+time|all\s+time|for\s+entire\s+period|entire\s+period|for\s+any\s+period|any\s+period|for\s+full\s+history|full\s+history)/iu.test(value);
|
||||
}
|
||||
function extractLooseByAnchorValue(text) {
|
||||
const match = String(text ?? "").match(/(?:^|\s)по\s+([a-zа-яё][a-zа-яё0-9._-]{1,})(?=[\s,.;:!?)]|$)/iu);
|
||||
const match = String(text ?? "").match(/(?:^|\s)по\s+([\p{L}][\p{L}\p{N}._-]{1,})(?=[\s,.;:!?)]|$)/iu);
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -471,14 +486,14 @@ function isLikelyCounterpartyToken(rawToken) {
|
||||
}
|
||||
function hasDocsOrBankSignal(text) {
|
||||
const lowered = String(text ?? "").toLowerCase();
|
||||
return new RegExp(`(?:${DOC_SIGNAL_PATTERN}|банк|выписк|платеж|платёж|оплат|transactions?|bank\\s+ops|bank\\s+operations?|payment|payments?|platezh|oplata)`, "iu").test(lowered);
|
||||
return new RegExp(`(?:${DOC_SIGNAL_PATTERN}|банк|выписк|списан|поступлен|платеж|платёж|оплат|transactions?|bank\\s+ops|bank\\s+operations?|payment|payments?|platezh|oplata)`, "iu").test(lowered);
|
||||
}
|
||||
function extractCounterpartyFromFreeTextHeuristic(text) {
|
||||
if (!hasDocsOrBankSignal(text)) {
|
||||
return undefined;
|
||||
}
|
||||
const tokens = String(text ?? "")
|
||||
.split(/[^a-zа-яё0-9._-]+/iu)
|
||||
.split(/[^\p{L}\p{N}._-]+/u)
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item.length > 0);
|
||||
if (tokens.length === 0) {
|
||||
@@ -531,7 +546,7 @@ function extractCounterpartyFromFreeTextHeuristic(text) {
|
||||
}
|
||||
function extractImplicitCounterpartyValue(text) {
|
||||
const input = String(text ?? "");
|
||||
const beforeDocsPattern = new RegExp(`(?:^|\\s)([a-zа-яё][a-zа-яё0-9._-]{1,})\\s+${DOC_SIGNAL_PATTERN}(?=[\\s,.;:!?)]|$)`, "iu");
|
||||
const beforeDocsPattern = new RegExp(`(?:^|\\s)([\\p{L}][\\p{L}\\p{N}._-]{1,})\\s+${DOC_SIGNAL_PATTERN}(?=[\\s,.;:!?)]|$)`, "iu");
|
||||
const beforeDocsMatch = input.match(beforeDocsPattern);
|
||||
if (beforeDocsMatch) {
|
||||
const candidate = String(beforeDocsMatch[1] ?? "").trim();
|
||||
@@ -539,7 +554,7 @@ function extractImplicitCounterpartyValue(text) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
const afterDocsPattern = new RegExp(`${DOC_SIGNAL_PATTERN}\\s+(?:по\\s+)?([a-zа-яё][a-zа-яё0-9._-]{1,})(?=[\\s,.;:!?)]|$)`, "iu");
|
||||
const afterDocsPattern = new RegExp(`${DOC_SIGNAL_PATTERN}\\s+(?:по\\s+)?([\\p{L}][\\p{L}\\p{N}._-]{1,})(?=[\\s,.;:!?)]|$)`, "iu");
|
||||
const afterDocsMatch = input.match(afterDocsPattern);
|
||||
if (afterDocsMatch) {
|
||||
const candidate = String(afterDocsMatch[1] ?? "").trim();
|
||||
@@ -549,6 +564,77 @@ function extractImplicitCounterpartyValue(text) {
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
function extractLeadingCounterpartyTokenHeuristic(text) {
|
||||
if (!hasDocsOrBankSignal(text)) {
|
||||
return undefined;
|
||||
}
|
||||
const tokens = String(text ?? "")
|
||||
.split(/[^\p{L}\p{N}._-]+/u)
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item.length > 0);
|
||||
if (tokens.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const monthTokens = [
|
||||
"янв",
|
||||
"фев",
|
||||
"мар",
|
||||
"апр",
|
||||
"май",
|
||||
"июн",
|
||||
"июл",
|
||||
"авг",
|
||||
"сен",
|
||||
"сент",
|
||||
"окт",
|
||||
"ноя",
|
||||
"дек",
|
||||
"january",
|
||||
"february",
|
||||
"march",
|
||||
"april",
|
||||
"may",
|
||||
"june",
|
||||
"july",
|
||||
"august",
|
||||
"september",
|
||||
"october",
|
||||
"november",
|
||||
"december"
|
||||
];
|
||||
for (const token of tokens.slice(0, 3)) {
|
||||
const lowered = token.toLowerCase();
|
||||
if (!isLikelyCounterpartyToken(lowered)) {
|
||||
continue;
|
||||
}
|
||||
if (/^\d{2}$/.test(lowered) || /^\d{4}$/.test(lowered)) {
|
||||
continue;
|
||||
}
|
||||
if (monthTokens.some((prefix) => lowered.startsWith(prefix))) {
|
||||
continue;
|
||||
}
|
||||
if (/(?:^за$|^for$|^from$|^to$|^по$|^с$|^год$|^года$|^г$|^year$|^на$|^in$)/iu.test(lowered)) {
|
||||
continue;
|
||||
}
|
||||
return token;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
function hasExplicitAccountCue(text) {
|
||||
return /(?:сч[её]т|счет|account|acct)/iu.test(String(text ?? ""));
|
||||
}
|
||||
function extractAccountTokenHeuristic(text) {
|
||||
const source = String(text ?? "");
|
||||
const dotted = source.match(/(?:^|[^\d])(\d{2}[.,]\d{1,2})(?!\d)/u);
|
||||
if (dotted) {
|
||||
return String(dotted[1]).replace(",", ".");
|
||||
}
|
||||
const contextual = source.match(/(?:^|[\s,.;:!?()\-])(?:по|на|for|account)\s+(\d{2})(?!\d)/iu);
|
||||
if (contextual) {
|
||||
return String(contextual[1]);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
function shiftDaysIso(baseIso, deltaDays) {
|
||||
const date = new Date(`${baseIso}T00:00:00.000Z`);
|
||||
date.setUTCDate(date.getUTCDate() + deltaDays);
|
||||
@@ -581,6 +667,13 @@ function extractAddressFilters(userMessage, intent) {
|
||||
if (accountMatch) {
|
||||
filters.account = String(accountMatch[1]).replace(",", ".");
|
||||
}
|
||||
if (!filters.account && (intent === "account_balance_snapshot" || intent === "documents_forming_balance")) {
|
||||
const heuristicAccount = extractAccountTokenHeuristic(text);
|
||||
if (heuristicAccount) {
|
||||
filters.account = heuristicAccount;
|
||||
warnings.push("account_anchor_derived_from_heuristic_token");
|
||||
}
|
||||
}
|
||||
const limitMatch = text.match(LIMIT_PATTERN);
|
||||
if (limitMatch) {
|
||||
const parsed = Number(limitMatch[1]);
|
||||
@@ -613,14 +706,21 @@ function extractAddressFilters(userMessage, intent) {
|
||||
warnings.push("counterparty_anchor_derived_from_free_text_heuristic");
|
||||
}
|
||||
}
|
||||
if (!filters.counterparty && (intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty")) {
|
||||
const leadingCounterparty = extractLeadingCounterpartyTokenHeuristic(text);
|
||||
if (leadingCounterparty) {
|
||||
filters.counterparty = cleanupAnchorValue(leadingCounterparty);
|
||||
warnings.push("counterparty_anchor_derived_from_leading_token");
|
||||
}
|
||||
}
|
||||
const contractMatch = text.match(CONTRACT_PATTERN);
|
||||
if (contractMatch) {
|
||||
filters.contract = cleanupAnchorValue(String(contractMatch[1]));
|
||||
filters.contract = cleanupContractAnchorValue(String(contractMatch[1]));
|
||||
}
|
||||
if (!filters.contract && (intent === "list_documents_by_contract" || intent === "bank_operations_by_contract")) {
|
||||
const heuristicContract = extractContractTokenHeuristic(text);
|
||||
if (heuristicContract) {
|
||||
filters.contract = cleanupAnchorValue(heuristicContract);
|
||||
filters.contract = cleanupContractAnchorValue(heuristicContract);
|
||||
warnings.push("contract_anchor_derived_from_heuristic_token");
|
||||
}
|
||||
}
|
||||
@@ -690,6 +790,13 @@ function extractAddressFilters(userMessage, intent) {
|
||||
warnings.push("as_of_date_defaulted_today");
|
||||
}
|
||||
}
|
||||
if (filters.account &&
|
||||
intent !== "account_balance_snapshot" &&
|
||||
intent !== "documents_forming_balance" &&
|
||||
!hasExplicitAccountCue(text)) {
|
||||
delete filters.account;
|
||||
warnings.push("account_anchor_dropped_without_explicit_cue_for_non_account_intent");
|
||||
}
|
||||
if (filters.counterparty && filters.counterparty.length < 2) {
|
||||
warnings.push("counterparty_filter_too_short");
|
||||
}
|
||||
|
||||
+20
-79
@@ -367,11 +367,13 @@ function applyIntentSpecificFilter(intent, rows) {
|
||||
}
|
||||
if (intent === "list_documents_by_counterparty" || intent === "list_documents_by_contract") {
|
||||
const documentPattern = /(?:документ|реализац|поступлен|счет[-\s]?фактур|акт|накладн|payment|invoice|document|sale|purchase|bank)/i;
|
||||
return rows.filter((row) => documentPattern.test(row.registrator.toLowerCase()) || row.analytics.length > 0);
|
||||
const matched = rows.filter((row) => documentPattern.test(row.registrator.toLowerCase()) || row.analytics.length > 0);
|
||||
return matched.length > 0 ? matched : rows;
|
||||
}
|
||||
if (intent === "documents_forming_balance") {
|
||||
const documentPattern = /(?:документ|реализац|поступлен|счет[-\s]?фактур|акт|накладн|списаниесрасчетногосчета|поступлениенарасчетныйсчет|invoice|document|sale|purchase)/i;
|
||||
return rows.filter((row) => documentPattern.test(row.registrator.toLowerCase()) || row.analytics.length > 0);
|
||||
const matched = rows.filter((row) => documentPattern.test(row.registrator.toLowerCase()) || row.analytics.length > 0);
|
||||
return matched.length > 0 ? matched : rows;
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
@@ -396,6 +398,12 @@ function isAnchorRecoveryIntent(intent) {
|
||||
intent === "open_items_by_counterparty_or_contract" ||
|
||||
intent === "list_open_contracts");
|
||||
}
|
||||
function isDocumentOrBankAnchorIntent(intent) {
|
||||
return (intent === "list_documents_by_counterparty" ||
|
||||
intent === "bank_operations_by_counterparty" ||
|
||||
intent === "list_documents_by_contract" ||
|
||||
intent === "bank_operations_by_contract");
|
||||
}
|
||||
function toIsoDatePrefix(value) {
|
||||
if (!value) {
|
||||
return null;
|
||||
@@ -967,74 +975,6 @@ class AddressQueryService {
|
||||
}
|
||||
};
|
||||
}
|
||||
if (intent.intent === "open_items_by_counterparty_or_contract" &&
|
||||
expandedFilteredRows.length === 0 &&
|
||||
expandedRowsByAnchor.length === 0 &&
|
||||
expandedNormalizedRows.length > 0) {
|
||||
const expandedFallbackRows = applyIntentSpecificFilter(intent.intent, expandedNormalizedRows);
|
||||
if (expandedFallbackRows.length > 0) {
|
||||
const expandedRowDiagnostics = deriveRowStageDiagnostics(expandedMcp.raw_rows, expandedNormalizedRows.length, expandedNormalizedRows.length);
|
||||
const expandedFactual = (0, composeStage_1.composeFactualReply)(intent.intent, expandedFallbackRows);
|
||||
const expandedLimitations = [
|
||||
...filters.warnings,
|
||||
"query_limit_auto_expanded_for_anchor_recovery",
|
||||
"open_items_anchor_not_matched_fallback_rows"
|
||||
];
|
||||
const expandedReasons = [
|
||||
...baseReasons,
|
||||
"query_limit_auto_expanded_for_anchor_recovery",
|
||||
"open_items_anchor_not_matched_fallback_rows"
|
||||
];
|
||||
return {
|
||||
handled: true,
|
||||
reply_text: `Точный якорь не подтвердился в live-строках даже после расширения до ${expandedPlan.limit}; ` +
|
||||
"показаны ближайшие доступные позиции по счетам 60/62/76.\n" +
|
||||
expandedFactual.text,
|
||||
reply_type: (0, composeStage_1.inferReplyType)(expandedFactual.responseType),
|
||||
response_type: expandedFactual.responseType,
|
||||
debug: {
|
||||
detected_mode: mode.mode,
|
||||
detected_mode_confidence: mode.confidence,
|
||||
query_shape: shape.shape,
|
||||
query_shape_confidence: shape.confidence,
|
||||
detected_intent: intent.intent,
|
||||
detected_intent_confidence: intent.confidence,
|
||||
extracted_filters: filters.extracted_filters,
|
||||
missing_required_filters: [],
|
||||
selected_recipe: expandedSelection.selected_recipe.recipe_id,
|
||||
mcp_call_status_legacy: "matched_non_empty",
|
||||
account_scope_mode: expandedPlan.account_scope_mode,
|
||||
account_scope_fallback_applied: expandedAccountScopeFallbackApplied,
|
||||
anchor_type: expandedAnchor.anchor_type,
|
||||
anchor_value_raw: expandedAnchor.anchor_value_raw,
|
||||
anchor_value_resolved: expandedAnchor.anchor_value_resolved,
|
||||
resolver_confidence: expandedAnchor.resolver_confidence,
|
||||
ambiguity_count: expandedAnchor.ambiguity_count,
|
||||
match_failure_stage: "materialized_but_not_anchor_matched",
|
||||
match_failure_reason: expandedAnchorFilter.mismatchReason ?? "anchor_not_matched_after_query_limit_expansion",
|
||||
mcp_call_status: "matched_non_empty",
|
||||
rows_fetched: expandedMcp.fetched_rows,
|
||||
raw_rows_received: expandedMcp.raw_rows.length,
|
||||
rows_after_account_scope: expandedNormalizedRows.length,
|
||||
rows_after_recipe_filter: expandedRowsByAnchor.length,
|
||||
rows_materialized: expandedNormalizedRows.length,
|
||||
rows_matched: expandedFallbackRows.length,
|
||||
raw_row_keys_sample: expandedRowDiagnostics.rawRowKeysSample,
|
||||
materialization_drop_reason: expandedRowDiagnostics.materializationDropReason,
|
||||
account_token_raw: expandedAccountScopeAudit.accountTokenRaw,
|
||||
account_token_normalized: expandedAccountScopeAudit.accountTokenNormalized,
|
||||
account_scope_fields_checked: expandedAccountScopeAudit.accountScopeFieldsChecked,
|
||||
account_scope_match_strategy: expandedAccountScopeAudit.accountScopeMatchStrategy,
|
||||
account_scope_drop_reason: expandedAccountScopeAudit.accountScopeDropReason,
|
||||
runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS",
|
||||
limited_reason_category: null,
|
||||
response_type: expandedFactual.responseType,
|
||||
limitations: expandedLimitations,
|
||||
reasons: expandedReasons
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1140,16 +1080,17 @@ class AddressQueryService {
|
||||
}
|
||||
}
|
||||
if (filteredRows.length === 0 &&
|
||||
intent.intent === "open_items_by_counterparty_or_contract" &&
|
||||
normalizedRows.length > 0) {
|
||||
const openItemsFallbackRows = applyIntentSpecificFilter(intent.intent, normalizedRows);
|
||||
if (openItemsFallbackRows.length > 0) {
|
||||
const fallbackFactual = (0, composeStage_1.composeFactualReply)(intent.intent, openItemsFallbackRows);
|
||||
const fallbackLimitations = [...filters.warnings, "open_items_anchor_not_matched_fallback_rows"];
|
||||
const fallbackReasons = [...baseReasons, "open_items_anchor_not_matched_fallback_rows"];
|
||||
isDocumentOrBankAnchorIntent(intent.intent) &&
|
||||
normalizedRows.length > 0 &&
|
||||
(stageStatus === "materialized_but_not_anchor_matched" || stageStatus === "materialized_but_filtered_out_by_recipe")) {
|
||||
const documentBankFallbackRows = applyIntentSpecificFilter(intent.intent, normalizedRows);
|
||||
if (documentBankFallbackRows.length > 0) {
|
||||
const fallbackFactual = (0, composeStage_1.composeFactualReply)(intent.intent, documentBankFallbackRows);
|
||||
const fallbackLimitations = [...filters.warnings, "anchor_not_matched_fallback_rows"];
|
||||
const fallbackReasons = [...baseReasons, "anchor_not_matched_fallback_rows"];
|
||||
return {
|
||||
handled: true,
|
||||
reply_text: "Точный якорь не подтвердился в текущем окне live-данных; показаны ближайшие доступные позиции по счетам 60/62/76.\n" +
|
||||
reply_text: "Точный якорь не подтвердился в текущем окне live-данных; показаны ближайшие доступные документы/операции по выбранному типу.\n" +
|
||||
fallbackFactual.text,
|
||||
reply_type: (0, composeStage_1.inferReplyType)(fallbackFactual.responseType),
|
||||
response_type: fallbackFactual.responseType,
|
||||
@@ -1179,7 +1120,7 @@ class AddressQueryService {
|
||||
rows_after_account_scope: normalizedRows.length,
|
||||
rows_after_recipe_filter: filterByAnchors.length,
|
||||
rows_materialized: normalizedRows.length,
|
||||
rows_matched: openItemsFallbackRows.length,
|
||||
rows_matched: documentBankFallbackRows.length,
|
||||
raw_row_keys_sample: rowDiagnostics.rawRowKeysSample,
|
||||
materialization_drop_reason: rowDiagnostics.materializationDropReason,
|
||||
account_token_raw: accountScopeAudit.accountTokenRaw,
|
||||
|
||||
Reference in New Issue
Block a user