ДОМЕНЫ - ВОПРОСЫ - Этап 4: точный маршрут confirmed payables на дату без эвристического фолбэка
This commit is contained in:
@@ -837,6 +837,9 @@ function requiredFiltersByIntent(intent: AddressIntent): Array<keyof AddressFilt
|
||||
if (intent === "account_balance_snapshot" || intent === "documents_forming_balance") {
|
||||
return ["account", "as_of_date"];
|
||||
}
|
||||
if (intent === "payables_confirmed_as_of_date") {
|
||||
return ["as_of_date"];
|
||||
}
|
||||
if (
|
||||
intent === "list_documents_by_counterparty" ||
|
||||
intent === "bank_operations_by_counterparty" ||
|
||||
@@ -851,7 +854,11 @@ function requiredFiltersByIntent(intent: AddressIntent): Array<keyof AddressFilt
|
||||
}
|
||||
|
||||
function usesAsOfPrimaryWindow(intent: AddressIntent): boolean {
|
||||
return intent === "open_items_by_counterparty_or_contract" || intent === "list_open_contracts";
|
||||
return (
|
||||
intent === "open_items_by_counterparty_or_contract" ||
|
||||
intent === "list_open_contracts" ||
|
||||
intent === "payables_confirmed_as_of_date"
|
||||
);
|
||||
}
|
||||
|
||||
export function extractAddressFilters(userMessage: string, intent: AddressIntent): AddressFilterExtraction {
|
||||
@@ -1035,7 +1042,12 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
|
||||
// - explicit as_of has priority;
|
||||
// - else use period_to boundary when provided;
|
||||
// - else default to today.
|
||||
if ((intent === "account_balance_snapshot" || intent === "documents_forming_balance") && !filters.as_of_date) {
|
||||
if (
|
||||
(intent === "account_balance_snapshot" ||
|
||||
intent === "documents_forming_balance" ||
|
||||
intent === "payables_confirmed_as_of_date") &&
|
||||
!filters.as_of_date
|
||||
) {
|
||||
if (filters.period_to) {
|
||||
filters.as_of_date = filters.period_to;
|
||||
warnings.push("as_of_date_derived_from_period_to");
|
||||
|
||||
@@ -1432,11 +1432,12 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
|
||||
|
||||
if (hasAny(text, PAYABLES_STRONG)) {
|
||||
const reasons = ["payables_signal_detected"];
|
||||
if (hasPayablesDebtLifecycleSignal(text)) {
|
||||
const payablesDebtLifecycleSignal = hasPayablesDebtLifecycleSignal(text);
|
||||
if (payablesDebtLifecycleSignal) {
|
||||
reasons.push("payables_debt_lifecycle_signal_detected");
|
||||
}
|
||||
return {
|
||||
intent: "list_payables_counterparties",
|
||||
intent: payablesDebtLifecycleSignal ? "payables_confirmed_as_of_date" : "list_payables_counterparties",
|
||||
confidence: "high",
|
||||
reasons
|
||||
};
|
||||
|
||||
@@ -419,6 +419,7 @@ function shouldAttemptCounterpartyCatalogResolution(intent: AddressIntent, filte
|
||||
intent === "bank_operations_by_counterparty" ||
|
||||
intent === "open_items_by_counterparty_or_contract" ||
|
||||
intent === "list_payables_counterparties" ||
|
||||
intent === "payables_confirmed_as_of_date" ||
|
||||
intent === "list_receivables_counterparties"
|
||||
);
|
||||
}
|
||||
@@ -745,6 +746,7 @@ function isCounterpartyRiskIntent(intent: AddressIntent): boolean {
|
||||
return (
|
||||
intent === "list_receivables_counterparties" ||
|
||||
intent === "list_payables_counterparties" ||
|
||||
intent === "payables_confirmed_as_of_date" ||
|
||||
intent === "list_open_contracts" ||
|
||||
intent === "open_items_by_counterparty_or_contract"
|
||||
);
|
||||
@@ -760,7 +762,11 @@ function isHeuristicCandidatesIntent(intent: AddressIntent): boolean {
|
||||
}
|
||||
|
||||
function isConfirmedBalanceIntent(intent: AddressIntent): boolean {
|
||||
return intent === "account_balance_snapshot" || intent === "documents_forming_balance";
|
||||
return (
|
||||
intent === "account_balance_snapshot" ||
|
||||
intent === "documents_forming_balance" ||
|
||||
intent === "payables_confirmed_as_of_date"
|
||||
);
|
||||
}
|
||||
|
||||
function resolveAsOfDateBasis(filters: AddressFilterSet): AddressAsOfDateBasis | null {
|
||||
@@ -845,11 +851,12 @@ function deriveAddressResultSemantics(input: {
|
||||
};
|
||||
}
|
||||
if (isConfirmedBalanceIntent(input.intent)) {
|
||||
const balanceConfirmed = input.responseType !== "LIMITED_WITH_REASON";
|
||||
return {
|
||||
requested_result_mode: requestedResultMode,
|
||||
result_mode: "confirmed_balance",
|
||||
evidence_strength: deriveAddressEvidenceStrength(input),
|
||||
balance_confirmed: true,
|
||||
balance_confirmed: balanceConfirmed,
|
||||
as_of_date_basis: asOfDateBasis ?? "period_end"
|
||||
};
|
||||
}
|
||||
@@ -1463,6 +1470,8 @@ function buildLimitedOffers(input: {
|
||||
|
||||
if (input.intent === "list_receivables_counterparties") {
|
||||
offers.push("показать контрагентов с максимальными хвостами дебиторки по 62/76");
|
||||
} else if (input.intent === "payables_confirmed_as_of_date") {
|
||||
offers.push("показать подтвержденный реестр открытых обязательств на дату среза по 60/76");
|
||||
} else if (input.intent === "list_payables_counterparties") {
|
||||
offers.push("показать контрагентов с максимальными хвостами кредиторки по 60/76");
|
||||
} else if (input.intent === "open_items_by_counterparty_or_contract" || input.intent === "list_open_contracts") {
|
||||
@@ -1512,7 +1521,8 @@ function buildLimitedIntentSignalLine(input: {
|
||||
open_items_by_counterparty_or_contract: "Сигнал запроса: нужен контроль незакрытых взаиморасчетов.",
|
||||
list_open_contracts: "Сигнал запроса: нужен список незакрытых договоров на дату.",
|
||||
list_receivables_counterparties: "Сигнал запроса: нужен ранжированный список должников.",
|
||||
list_payables_counterparties: "Сигнал запроса: нужен ранжированный список кредиторов."
|
||||
list_payables_counterparties: "Сигнал запроса: нужен ранжированный список кредиторов.",
|
||||
payables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез обязательств к оплате на дату."
|
||||
};
|
||||
|
||||
const byShape: Partial<Record<AddressQueryShapeDetection["shape"], string>> = {
|
||||
@@ -1655,7 +1665,7 @@ function composeLimitedReply(input: {
|
||||
lines.push(`Что могу сделать сейчас: ${offers.join("; ")}.`);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
return lines.join("\n\n");
|
||||
}
|
||||
|
||||
function buildLimitedExecutionResult(input: {
|
||||
@@ -1701,12 +1711,17 @@ function buildLimitedExecutionResult(input: {
|
||||
rowsMatched: input.rowsMatched
|
||||
});
|
||||
const requestedResultMode = resolveRequestedResultMode(input.intent.intent, input.filters);
|
||||
const reasons = withConfirmedBalanceFallbackReason(
|
||||
const reasonsWithConfirmedFallback = withConfirmedBalanceFallbackReason(
|
||||
input.reasons,
|
||||
requestedResultMode,
|
||||
undefined,
|
||||
resultSemantics.result_mode
|
||||
);
|
||||
const reasons =
|
||||
input.intent.intent === "payables_confirmed_as_of_date" &&
|
||||
!reasonsWithConfirmedFallback.includes("exact_payables_mode_limited_response")
|
||||
? [...reasonsWithConfirmedFallback, "exact_payables_mode_limited_response"]
|
||||
: reasonsWithConfirmedFallback;
|
||||
return {
|
||||
handled: true,
|
||||
reply_text: composeLimitedReply({
|
||||
@@ -1795,7 +1810,8 @@ export class AddressQueryService {
|
||||
}
|
||||
const requestedResultMode = resolveRequestedResultMode(intent.intent, filters.extracted_filters);
|
||||
const payablesConfirmedExecution =
|
||||
intent.intent === "list_payables_counterparties" && requestedResultMode === "confirmed_balance"
|
||||
(intent.intent === "list_payables_counterparties" || intent.intent === "payables_confirmed_as_of_date") &&
|
||||
requestedResultMode === "confirmed_balance"
|
||||
? resolveExecutionFiltersForPayablesConfirmedBalance(filters.extracted_filters, analysisDate)
|
||||
: null;
|
||||
const executionFilters = payablesConfirmedExecution?.executionFilters ?? filters.extracted_filters;
|
||||
@@ -1846,6 +1862,9 @@ export class AddressQueryService {
|
||||
if (preferConfirmedBalanceForPayablesLifecycle && !baseReasons.includes("confirmed_balance_attempt_for_payables_debt_lifecycle")) {
|
||||
baseReasons.push("confirmed_balance_attempt_for_payables_debt_lifecycle");
|
||||
}
|
||||
if (intent.intent === "payables_confirmed_as_of_date" && !baseReasons.includes("confirmed_balance_exact_payables_intent")) {
|
||||
baseReasons.push("confirmed_balance_exact_payables_intent");
|
||||
}
|
||||
if (
|
||||
requestedResultMode === "confirmed_balance" &&
|
||||
recipeIntent === "open_items_by_counterparty_or_contract" &&
|
||||
@@ -1982,11 +2001,13 @@ export class AddressQueryService {
|
||||
query: plan.query,
|
||||
limit: plan.limit
|
||||
});
|
||||
const allowOpenItemsFallbackForMissingSubconto = intent.intent !== "payables_confirmed_as_of_date";
|
||||
if (
|
||||
mcp.error &&
|
||||
(plan.recipe.recipe_id === "address_movements_receivables_v1" ||
|
||||
plan.recipe.recipe_id === "address_movements_payables_v1") &&
|
||||
isMissingSubcontoFieldError(mcp.error)
|
||||
isMissingSubcontoFieldError(mcp.error) &&
|
||||
allowOpenItemsFallbackForMissingSubconto
|
||||
) {
|
||||
const fallbackSelection = selectAddressRecipe("open_items_by_counterparty_or_contract", executionFilters);
|
||||
if (fallbackSelection.selected_recipe && fallbackSelection.missing_required_filters.length === 0) {
|
||||
@@ -2019,11 +2040,21 @@ export class AddressQueryService {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!baseReasons.includes("mcp_missing_subconto_field_auto_fallback_unavailable")) {
|
||||
baseReasons.push("mcp_missing_subconto_field_auto_fallback_unavailable");
|
||||
}
|
||||
if (!baseReasons.includes("mcp_missing_subconto_field_auto_fallback_unavailable")) {
|
||||
baseReasons.push("mcp_missing_subconto_field_auto_fallback_unavailable");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
mcp.error &&
|
||||
(plan.recipe.recipe_id === "address_movements_receivables_v1" ||
|
||||
plan.recipe.recipe_id === "address_movements_payables_v1") &&
|
||||
isMissingSubcontoFieldError(mcp.error) &&
|
||||
!allowOpenItemsFallbackForMissingSubconto &&
|
||||
!baseReasons.includes("confirmed_payables_exact_mode_missing_subconto_no_heuristic_fallback")
|
||||
) {
|
||||
baseReasons.push("confirmed_payables_exact_mode_missing_subconto_no_heuristic_fallback");
|
||||
}
|
||||
|
||||
if (mcp.error) {
|
||||
const errorScopeAudit = buildDefaultAccountScopeAudit(filters.extracted_filters);
|
||||
@@ -2846,6 +2877,36 @@ export class AddressQueryService {
|
||||
}),
|
||||
factual.semantics
|
||||
);
|
||||
if (intent.intent === "payables_confirmed_as_of_date" && factualResultSemantics.balance_confirmed !== true) {
|
||||
return buildLimitedExecutionResult({
|
||||
mode,
|
||||
shape,
|
||||
intent,
|
||||
filters: filters.extracted_filters,
|
||||
missingRequiredFilters: [],
|
||||
selectedRecipe: effectiveRecipeId,
|
||||
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: "exact payables mode: confirmed balance was not proven for the requested as-of slice",
|
||||
nextStep: "specify as_of_date/counterparty or enable detailed settlement registers for exact confirmed balance",
|
||||
limitations: ["exact_payables_mode_unconfirmed_output_blocked"],
|
||||
reasons: [...baseReasons, "exact_payables_mode_unconfirmed_output_blocked"]
|
||||
});
|
||||
}
|
||||
return {
|
||||
handled: true,
|
||||
reply_text: factual.text,
|
||||
|
||||
@@ -540,6 +540,16 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [
|
||||
account_scope: ["60", "76"],
|
||||
account_scope_mode: "strict"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_payables_confirmed_as_of_date_v1",
|
||||
intent: "payables_confirmed_as_of_date",
|
||||
purpose: "Build confirmed payables snapshot as-of date from movements on accounts 60/76",
|
||||
required_filters: ["as_of_date"],
|
||||
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
|
||||
default_limit: 200,
|
||||
account_scope: ["60", "76"],
|
||||
account_scope_mode: "strict"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_movements_receivables_v1",
|
||||
intent: "list_receivables_counterparties",
|
||||
|
||||
@@ -858,7 +858,7 @@ function buildPayablesConfirmedBalanceAggregate(
|
||||
continue;
|
||||
}
|
||||
const amount = row.amount;
|
||||
if (!Number.isFinite(amount)) {
|
||||
if (typeof amount !== "number" || !Number.isFinite(amount)) {
|
||||
continue;
|
||||
}
|
||||
const absAmount = Math.abs(amount);
|
||||
@@ -2252,6 +2252,82 @@ export function composeFactualReply(
|
||||
};
|
||||
}
|
||||
|
||||
if (intent === "payables_confirmed_as_of_date") {
|
||||
const payablesAsOfDate = resolvePayablesAsOfDate(options);
|
||||
const confirmedBalances = buildPayablesConfirmedBalanceAggregate(rows, payablesAsOfDate);
|
||||
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
|
||||
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
|
||||
const periodTo = normalizeIsoDateOnly(options.periodTo);
|
||||
const scopeLine = asOfDate
|
||||
? `- Дата среза: ${formatDateRu(asOfDate)}.`
|
||||
: periodFrom || periodTo
|
||||
? `- Период анализа: ${formatDateRu(periodFrom ?? "...")}..${formatDateRu(periodTo ?? "...")}.`
|
||||
: null;
|
||||
const carryoverLine =
|
||||
asOfDate || periodFrom || periodTo
|
||||
? "- В срез могут входить обязательства, возникшие до периода, если они оставались открытыми на дату среза."
|
||||
: null;
|
||||
const categoryCounts = confirmedBalances.reduce<Record<PayablesLiabilityCategory, number>>(
|
||||
(acc, item) => {
|
||||
acc[item.category] += 1;
|
||||
return acc;
|
||||
},
|
||||
{ supplier_or_contractor: 0, bank_or_credit: 0, tax_or_state: 0, other: 0 }
|
||||
);
|
||||
|
||||
const lines: string[] = [
|
||||
"Блок 1. Статус результата",
|
||||
"- Режим результата: подтвержденный срез обязательств к оплате (exact route).",
|
||||
"- Эвристический shortlist в этом режиме не используется."
|
||||
];
|
||||
|
||||
lines.push("");
|
||||
lines.push("Блок 2. Что учтено");
|
||||
lines.push(`- Дата среза: ${formatDateRu(payablesAsOfDate)}.`);
|
||||
if (scopeLine) {
|
||||
lines.push(scopeLine);
|
||||
}
|
||||
lines.push("- Контур: обязательства по счетам 60/76.");
|
||||
if (carryoverLine) {
|
||||
lines.push(carryoverLine);
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
lines.push("Блок 3. Сводка");
|
||||
lines.push(`- Строк в выборке: ${rows.length}.`);
|
||||
lines.push(`- Контрагентов с подтвержденным остатком к оплате: ${confirmedBalances.length}.`);
|
||||
|
||||
lines.push("");
|
||||
lines.push("Блок 4. Категории обязательств");
|
||||
lines.push(`- ${liabilityCategoryLabel("supplier_or_contractor")}: ${categoryCounts.supplier_or_contractor}`);
|
||||
lines.push(`- ${liabilityCategoryLabel("bank_or_credit")}: ${categoryCounts.bank_or_credit}`);
|
||||
lines.push(`- ${liabilityCategoryLabel("tax_or_state")}: ${categoryCounts.tax_or_state}`);
|
||||
lines.push(`- ${liabilityCategoryLabel("other")}: ${categoryCounts.other}`);
|
||||
|
||||
lines.push("");
|
||||
lines.push("Блок 5. Подтвержденные позиции к оплате");
|
||||
if (confirmedBalances.length > 0) {
|
||||
lines.push(
|
||||
...confirmedBalances.slice(0, 10).map(
|
||||
(item, index) =>
|
||||
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoney(item.outstandingAmount)} | операций: ${item.operations}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}`
|
||||
)
|
||||
);
|
||||
} else {
|
||||
lines.push("- Подтвержденных открытых обязательств к оплате на дату среза не найдено.");
|
||||
}
|
||||
|
||||
return {
|
||||
responseType: confirmedBalances.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
|
||||
text: lines.join("\n"),
|
||||
semantics: {
|
||||
result_mode: "confirmed_balance",
|
||||
evidence_strength: confirmedBalances.length > 0 ? "strong" : "medium",
|
||||
balance_confirmed: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (intent === "list_payables_counterparties") {
|
||||
const counterparties = buildPayablesCounterpartyRiskAggregate(rows);
|
||||
const payablesAsOfDate = resolvePayablesAsOfDate(options);
|
||||
|
||||
@@ -439,7 +439,11 @@ function mergeFollowupFilters(
|
||||
}
|
||||
}
|
||||
|
||||
if (intent === "open_items_by_counterparty_or_contract" || intent === "list_open_contracts") {
|
||||
if (
|
||||
intent === "open_items_by_counterparty_or_contract" ||
|
||||
intent === "list_open_contracts" ||
|
||||
intent === "payables_confirmed_as_of_date"
|
||||
) {
|
||||
const inheritedContract = previousContract ?? (followupContext.previous_anchor_type === "contract" ? previousAnchorValue : null);
|
||||
const currentContract = toNonEmptyString(merged.contract);
|
||||
const shouldInheritContract =
|
||||
@@ -462,6 +466,13 @@ function mergeFollowupFilters(
|
||||
merged.counterparty = inheritedCounterparty;
|
||||
reasons.push(currentCounterparty ? "counterparty_replaced_from_followup_context" : "counterparty_from_followup_context");
|
||||
}
|
||||
if (sameDateRequested) {
|
||||
const inheritedAsOfDate = previousAsOfDate ?? previousPeriodTo ?? previousPeriodFrom;
|
||||
if (inheritedAsOfDate && merged.as_of_date !== inheritedAsOfDate) {
|
||||
merged.as_of_date = inheritedAsOfDate;
|
||||
reasons.push("as_of_date_from_followup_context");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allTimeRequested) {
|
||||
@@ -525,6 +536,7 @@ function resolveMissingRequiredFilters(intent: AddressIntent, filters: AddressFi
|
||||
const requiredByIntent: Record<string, Array<keyof AddressFilterSet>> = {
|
||||
account_balance_snapshot: ["account", "as_of_date"],
|
||||
documents_forming_balance: ["account", "as_of_date"],
|
||||
payables_confirmed_as_of_date: ["as_of_date"],
|
||||
list_documents_by_counterparty: ["counterparty"],
|
||||
bank_operations_by_counterparty: ["counterparty"],
|
||||
list_contracts_by_counterparty: ["counterparty"],
|
||||
|
||||
@@ -189,7 +189,11 @@ function inferAggregationProfile(intent: AddressIntent, shape: AddressQueryShape
|
||||
return "management_profile";
|
||||
}
|
||||
|
||||
if (intent === "account_balance_snapshot" || intent === "documents_forming_balance") {
|
||||
if (
|
||||
intent === "account_balance_snapshot" ||
|
||||
intent === "documents_forming_balance" ||
|
||||
intent === "payables_confirmed_as_of_date"
|
||||
) {
|
||||
return "balance_snapshot";
|
||||
}
|
||||
|
||||
|
||||
@@ -3652,6 +3652,7 @@ function hasOpenContractsAddressSignal(text) {
|
||||
const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([
|
||||
"list_open_contracts",
|
||||
"open_items_by_counterparty_or_contract",
|
||||
"payables_confirmed_as_of_date",
|
||||
"list_documents_by_contract",
|
||||
"bank_operations_by_contract",
|
||||
"list_documents_by_counterparty",
|
||||
|
||||
@@ -13,6 +13,7 @@ export type AddressIntent =
|
||||
| "list_contracts_by_counterparty"
|
||||
| "list_open_contracts"
|
||||
| "list_payables_counterparties"
|
||||
| "payables_confirmed_as_of_date"
|
||||
| "list_receivables_counterparties"
|
||||
| "account_balance_snapshot"
|
||||
| "open_items_by_counterparty_or_contract"
|
||||
|
||||
Reference in New Issue
Block a user