АДРЕСНЫЙ РЕЖИМ -
This commit is contained in:
@@ -323,6 +323,11 @@ function cleanupAnchorValue(value: string): string {
|
||||
if (asOfTruncatedTailPattern.test(normalized)) {
|
||||
return normalized.replace(asOfTruncatedTailPattern, "").trim();
|
||||
}
|
||||
const periodEndTailPattern =
|
||||
/\s+на\s+конец(?:\s+период[а-яё]*)?\s+(?:\d{1,4}[./-]\d{1,2}(?:[./-]\d{1,4})?|\d{4}|[a-zа-яё]+\s+\d{4})(?:\s+|$)[\s\S]*$/iu;
|
||||
if (periodEndTailPattern.test(normalized)) {
|
||||
return normalized.replace(periodEndTailPattern, "").trim();
|
||||
}
|
||||
|
||||
// Remove trailing period qualifiers that can be swallowed by broad anchor regexes:
|
||||
// "<counterparty> с 2020-07-01 по 2020-07-31", "<counterparty> from 2020-07-01 to 2020-07-31"
|
||||
@@ -618,6 +623,10 @@ function requiredFiltersByIntent(intent: AddressIntent): Array<keyof AddressFilt
|
||||
return [];
|
||||
}
|
||||
|
||||
function usesAsOfPrimaryWindow(intent: AddressIntent): boolean {
|
||||
return intent === "open_items_by_counterparty_or_contract" || intent === "list_open_contracts";
|
||||
}
|
||||
|
||||
export function extractAddressFilters(userMessage: string, intent: AddressIntent): AddressFilterExtraction {
|
||||
const rawText = String(userMessage ?? "").trim();
|
||||
const text = normalizeMojibakeString(rawText);
|
||||
@@ -713,8 +722,22 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
|
||||
}
|
||||
}
|
||||
|
||||
const explicitAsOfDate = extractAsOfDate(text);
|
||||
if (usesAsOfPrimaryWindow(intent) && explicitAsOfDate) {
|
||||
filters.as_of_date = explicitAsOfDate;
|
||||
const periodWasDerivedHeuristically =
|
||||
warnings.includes("period_derived_from_month_phrase") ||
|
||||
warnings.includes("period_derived_from_year_range_phrase") ||
|
||||
warnings.includes("period_derived_from_year_phrase");
|
||||
if (periodWasDerivedHeuristically && !periodRange.period_from && !periodRange.period_to) {
|
||||
delete filters.period_from;
|
||||
delete filters.period_to;
|
||||
warnings.push("period_window_cleared_for_as_of_intent");
|
||||
}
|
||||
}
|
||||
|
||||
// If explicit period window exists, do not infer as_of_date from one of its boundary dates.
|
||||
if (!filters.period_from && !filters.period_to) {
|
||||
if (!filters.period_from && !filters.period_to && !filters.as_of_date) {
|
||||
const asOfDate = extractAsOfDate(text);
|
||||
if (asOfDate) {
|
||||
filters.as_of_date = asOfDate;
|
||||
|
||||
@@ -224,6 +224,23 @@ function hasAccountBalanceSignal(text: string): boolean {
|
||||
return hasAccountLexeme && hasBalanceLexeme;
|
||||
}
|
||||
|
||||
function hasOpenContractsListSignal(text: string): boolean {
|
||||
const hasContractLexeme = text.includes("договор") || text.includes("contract") || text.includes("dogovor");
|
||||
const hasOpenLexeme = /(?:незакрыт|не\s+закрыт|открыт|open|unclosed)/iu.test(text);
|
||||
if (!hasContractLexeme || !hasOpenLexeme) {
|
||||
return false;
|
||||
}
|
||||
// Query about a specific contract should stay in open-items lane.
|
||||
if (hasContractNumberLikeToken(text)) {
|
||||
return false;
|
||||
}
|
||||
// Debt/tail wording indicates open-items intent, not contract list.
|
||||
if (/(?:долг|задолж|хвост|позиц|open\s+items|unclosed\s+items|взаиморасчет|взаиморасчёт)/iu.test(text)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function isLikelyCounterpartyToken(rawToken: string): boolean {
|
||||
const token = String(rawToken ?? "").trim().toLowerCase();
|
||||
if (!token || token.length < 2) {
|
||||
@@ -517,6 +534,14 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
|
||||
};
|
||||
}
|
||||
|
||||
if (hasOpenContractsListSignal(text)) {
|
||||
return {
|
||||
intent: "list_open_contracts",
|
||||
confidence: "medium",
|
||||
reasons: ["open_contract_signal_detected"]
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
hasAny(text, OPEN_ITEMS_HINTS) &&
|
||||
(text.includes("контраг") || text.includes("договор") || text.includes("counterparty") || text.includes("contract"))
|
||||
|
||||
@@ -17,7 +17,7 @@ import { buildAddressRecipePlan, selectAddressRecipe } from "./addressRecipeCata
|
||||
import { executeAddressMcpQuery } from "./addressMcpClient";
|
||||
import { runAddressDecomposeStage, type AddressFollowupContext } from "./address_runtime/decomposeStage";
|
||||
import { resolvePrimaryAnchor, refineAnchorFromRows, type AnchorResolutionDebug } from "./address_runtime/resolveStage";
|
||||
import { composeFactualReply, contractCandidatesFromRows, inferReplyType } from "./address_runtime/composeStage";
|
||||
import { composeFactualReply, inferReplyType } from "./address_runtime/composeStage";
|
||||
|
||||
interface NormalizedAddressRow {
|
||||
period: string | null;
|
||||
@@ -464,12 +464,14 @@ function canAutoBroadenPeriodWindow(intent: AddressIntent, filters: AddressFilte
|
||||
);
|
||||
}
|
||||
|
||||
function isDocumentOrBankIntent(intent: AddressIntent): boolean {
|
||||
function isAnchorRecoveryIntent(intent: AddressIntent): boolean {
|
||||
return (
|
||||
intent === "list_documents_by_counterparty" ||
|
||||
intent === "bank_operations_by_counterparty" ||
|
||||
intent === "list_documents_by_contract" ||
|
||||
intent === "bank_operations_by_contract"
|
||||
intent === "bank_operations_by_contract" ||
|
||||
intent === "open_items_by_counterparty_or_contract" ||
|
||||
intent === "list_open_contracts"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1058,40 +1060,9 @@ export class AddressQueryService {
|
||||
}
|
||||
}
|
||||
|
||||
if (intent.intent === "list_open_contracts" && filteredRows.length > 0 && contractCandidatesFromRows(filteredRows).length === 0) {
|
||||
return buildLimitedExecutionResult({
|
||||
mode,
|
||||
shape,
|
||||
intent,
|
||||
filters: filters.extracted_filters,
|
||||
missingRequiredFilters: [],
|
||||
selectedRecipe: recipeSelection.selected_recipe.recipe_id,
|
||||
accountScopeMode: plan.account_scope_mode,
|
||||
accountScopeFallbackApplied,
|
||||
accountScopeAudit,
|
||||
anchor,
|
||||
matchFailureStage,
|
||||
matchFailureReason,
|
||||
mcpCallStatus: stageStatus,
|
||||
rowsFetched: mcp.fetched_rows,
|
||||
rawRowsReceived: mcp.raw_rows.length,
|
||||
rowsAfterAccountScope: normalizedRows.length,
|
||||
rowsAfterRecipeFilter: filterByAnchors.length,
|
||||
rowsMaterialized: normalizedRows.length,
|
||||
rowsMatched: filteredRows.length,
|
||||
rawRowKeysSample: rowDiagnostics.rawRowKeysSample,
|
||||
materializationDropReason: rowDiagnostics.materializationDropReason,
|
||||
category: "recipe_visibility_gap",
|
||||
reasonText: "в live строках нет договорных якорей для уверенного списка незакрытых договоров",
|
||||
nextStep: "сузьте запрос по контрагенту или добавьте номер договора",
|
||||
limitations: ["no_contract_anchors_in_live_rows"],
|
||||
reasons: baseReasons
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
filteredRows.length === 0 &&
|
||||
isDocumentOrBankIntent(intent.intent) &&
|
||||
isAnchorRecoveryIntent(intent.intent) &&
|
||||
(stageStatus === "materialized_but_not_anchor_matched" || stageStatus === "materialized_but_filtered_out_by_recipe")
|
||||
) {
|
||||
const currentLimit =
|
||||
@@ -1201,6 +1172,82 @@ export 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 = 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: 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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1313,6 +1360,67 @@ export 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 = 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"];
|
||||
return {
|
||||
handled: true,
|
||||
reply_text:
|
||||
"Точный якорь не подтвердился в текущем окне live-данных; показаны ближайшие доступные позиции по счетам 60/62/76.\n" +
|
||||
fallbackFactual.text,
|
||||
reply_type: inferReplyType(fallbackFactual.responseType),
|
||||
response_type: fallbackFactual.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: recipeSelection.selected_recipe.recipe_id,
|
||||
mcp_call_status_legacy: "matched_non_empty",
|
||||
account_scope_mode: plan.account_scope_mode,
|
||||
account_scope_fallback_applied: accountScopeFallbackApplied,
|
||||
anchor_type: anchor.anchor_type,
|
||||
anchor_value_raw: anchor.anchor_value_raw,
|
||||
anchor_value_resolved: anchor.anchor_value_resolved,
|
||||
resolver_confidence: anchor.resolver_confidence,
|
||||
ambiguity_count: anchor.ambiguity_count,
|
||||
match_failure_stage: matchFailureStage,
|
||||
match_failure_reason: matchFailureReason,
|
||||
mcp_call_status: "matched_non_empty",
|
||||
rows_fetched: mcp.fetched_rows,
|
||||
raw_rows_received: mcp.raw_rows.length,
|
||||
rows_after_account_scope: normalizedRows.length,
|
||||
rows_after_recipe_filter: filterByAnchors.length,
|
||||
rows_materialized: normalizedRows.length,
|
||||
rows_matched: openItemsFallbackRows.length,
|
||||
raw_row_keys_sample: rowDiagnostics.rawRowKeysSample,
|
||||
materialization_drop_reason: rowDiagnostics.materializationDropReason,
|
||||
account_token_raw: accountScopeAudit.accountTokenRaw,
|
||||
account_token_normalized: accountScopeAudit.accountTokenNormalized,
|
||||
account_scope_fields_checked: accountScopeAudit.accountScopeFieldsChecked,
|
||||
account_scope_match_strategy: accountScopeAudit.accountScopeMatchStrategy,
|
||||
account_scope_drop_reason: accountScopeAudit.accountScopeDropReason,
|
||||
runtime_readiness: "LIVE_QUERYABLE_WITH_LIMITS",
|
||||
limited_reason_category: null,
|
||||
response_type: fallbackFactual.responseType,
|
||||
limitations: fallbackLimitations,
|
||||
reasons: fallbackReasons
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (filteredRows.length === 0) {
|
||||
const hadBaseRows = normalizedRows.length > 0 || mcp.fetched_rows > 0;
|
||||
const hadAnchorMatchedRows = filterByAnchors.length > 0;
|
||||
|
||||
@@ -280,7 +280,9 @@ function maxLimitForIntent(intent: AddressIntent): number {
|
||||
intent === "list_documents_by_counterparty" ||
|
||||
intent === "bank_operations_by_counterparty" ||
|
||||
intent === "list_documents_by_contract" ||
|
||||
intent === "bank_operations_by_contract"
|
||||
intent === "bank_operations_by_contract" ||
|
||||
intent === "open_items_by_counterparty_or_contract" ||
|
||||
intent === "list_open_contracts"
|
||||
) {
|
||||
return ADDRESS_MAX_LIMIT_EXTENDED;
|
||||
}
|
||||
|
||||
@@ -85,7 +85,12 @@ export function composeFactualReply(
|
||||
`Строк движения: ${rows.length}.`,
|
||||
`Договорных кандидатов: ${contracts.length}.`
|
||||
];
|
||||
lines.push(...contracts.slice(0, 8).map((item, index) => `${index + 1}. ${item}`));
|
||||
if (contracts.length > 0) {
|
||||
lines.push(...contracts.slice(0, 8).map((item, index) => `${index + 1}. ${item}`));
|
||||
} else {
|
||||
lines.push("Договорные якоря в live-строках не выделены; показаны связанные движения как fallback.");
|
||||
lines.push(...formatTopRows(rows, 6));
|
||||
}
|
||||
return {
|
||||
responseType: "FACTUAL_LIST",
|
||||
text: lines.join("\n")
|
||||
|
||||
Reference in New Issue
Block a user