ДОМЕНЫ - ВОПРОСЫ - кто должен нам

This commit is contained in:
2026-04-12 20:20:43 +03:00
parent ae9daa50e7
commit 040a55aaea
31 changed files with 1005 additions and 89 deletions
@@ -4,7 +4,12 @@ exports.resolveAddressCapabilityRouteDecision = resolveAddressCapabilityRouteDec
exports.isCapabilityRouteBlocked = isCapabilityRouteBlocked;
exports.resolveShadowRouteIntent = resolveShadowRouteIntent;
const config_1 = require("../config");
const COMPUTE_EXACT_INTENTS = new Set(["account_balance_snapshot", "documents_forming_balance", "payables_confirmed_as_of_date"]);
const COMPUTE_EXACT_INTENTS = new Set([
"account_balance_snapshot",
"documents_forming_balance",
"payables_confirmed_as_of_date",
"receivables_confirmed_as_of_date"
]);
const NAVIGATION_INTENTS = new Set([
"list_documents_by_counterparty",
"bank_operations_by_counterparty",
@@ -31,6 +36,9 @@ function defaultCapabilityId(intent) {
if (intent === "payables_confirmed_as_of_date") {
return "confirmed_payables_as_of_date";
}
if (intent === "receivables_confirmed_as_of_date") {
return "confirmed_receivables_as_of_date";
}
if (intent === "list_payables_counterparties") {
return "payables_candidates_list";
}
@@ -58,6 +66,14 @@ function resolveCapabilityEnabled(intent) {
reason: config_1.FEATURE_ASSISTANT_ROUTE_PAYABLES_CONFIRMED_V1 ? "payables_confirmed_route_enabled" : "payables_confirmed_route_disabled_by_flag"
};
}
if (intent === "receivables_confirmed_as_of_date") {
return {
enabled: config_1.FEATURE_ASSISTANT_ROUTE_RECEIVABLES_CONFIRMED_V1,
reason: config_1.FEATURE_ASSISTANT_ROUTE_RECEIVABLES_CONFIRMED_V1
? "receivables_confirmed_route_enabled"
: "receivables_confirmed_route_disabled_by_flag"
};
}
if (intent === "list_payables_counterparties") {
return {
enabled: config_1.FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1,
@@ -748,6 +748,9 @@ function requiredFiltersByIntent(intent) {
if (intent === "payables_confirmed_as_of_date") {
return ["as_of_date"];
}
if (intent === "receivables_confirmed_as_of_date") {
return ["as_of_date"];
}
if (intent === "list_documents_by_counterparty" ||
intent === "bank_operations_by_counterparty" ||
intent === "list_contracts_by_counterparty") {
@@ -761,7 +764,8 @@ function requiredFiltersByIntent(intent) {
function usesAsOfPrimaryWindow(intent) {
return (intent === "open_items_by_counterparty_or_contract" ||
intent === "list_open_contracts" ||
intent === "payables_confirmed_as_of_date");
intent === "payables_confirmed_as_of_date" ||
intent === "receivables_confirmed_as_of_date");
}
function extractAddressFilters(userMessage, intent) {
const rawText = String(userMessage ?? "").trim();
@@ -923,7 +927,8 @@ function extractAddressFilters(userMessage, intent) {
// - else default to today.
if ((intent === "account_balance_snapshot" ||
intent === "documents_forming_balance" ||
intent === "payables_confirmed_as_of_date") &&
intent === "payables_confirmed_as_of_date" ||
intent === "receivables_confirmed_as_of_date") &&
!filters.as_of_date) {
if (filters.period_to) {
filters.as_of_date = filters.period_to;
@@ -3,7 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveAddressIntent = resolveAddressIntent;
const RECEIVABLES_STRONG = [
"кто должен нам",
"кто нам должен",
"кто нам должэн",
"нам должны",
"нам должен",
"нам должэны",
"who owes us",
"receivable",
"receivables",
@@ -14,7 +18,11 @@ const RECEIVABLES_STRONG = [
];
const PAYABLES_STRONG = [
"кому должны мы",
"кому должэны мы",
"кому мы должны",
"кому мы должэны",
"мы должны",
"мы должэны",
"who we owe",
"payable",
"payables",
@@ -865,6 +873,18 @@ function hasPayablesDebtLifecycleSignal(text) {
}
return true;
}
function hasReceivablesDebtLifecycleSignal(text) {
const hasOweUsSignal = /(?:кто\s+нам\s+долж(?:ен|ны)?|кто\s+долж(?:ен|ны)?\s+нам|нам\s+долж(?:ен|ны)|должник(?:и|ов|а)?|дебитор(?:ы|ов|ск)?|задолж|долг(?:и|ов|а|у)?|к\s+получению|на\s+поступление|к\s+взысканию|who\s+owes\s+us|receivables?)/iu.test(text);
if (!hasOweUsSignal) {
return false;
}
const hasPastInflowSignal = /(?:прин[её]с|зан[её]с|поступил|приход|inflow|paid\s+us|already\s+paid)/iu.test(text);
const hasTopRankingSignal = /(?:топ|top|больше\s+всего|сам(?:ый|ая|ое|ые)|наибольш|максимальн)/iu.test(text);
if (hasPastInflowSignal && hasTopRankingSignal) {
return false;
}
return true;
}
function hasReceivablesLatencyRiskSignal(text) {
const hasBuyer = /(?:покупател|клиент|заказчик|customer|buyer)/iu.test(text);
const hasCounterparty = /(?:контрагент|counterparty|partner)/iu.test(text);
@@ -1239,10 +1259,15 @@ function resolveAddressIntent(userMessage) {
};
}
if (hasAny(text, RECEIVABLES_STRONG)) {
const receivablesDebtLifecycleSignal = hasReceivablesDebtLifecycleSignal(text);
const reasons = ["receivables_signal_detected"];
if (receivablesDebtLifecycleSignal) {
reasons.push("receivables_debt_lifecycle_signal_detected");
}
return {
intent: "list_receivables_counterparties",
intent: receivablesDebtLifecycleSignal ? "receivables_confirmed_as_of_date" : "list_receivables_counterparties",
confidence: "high",
reasons: ["receivables_signal_detected"]
reasons
};
}
if (hasAny(text, PAYABLES_STRONG)) {
@@ -14,6 +14,7 @@ const DISPLAY_ENTITY_TYPE_BY_INTENT = {
customer_revenue_and_payments: "counterparty",
supplier_payouts_profile: "counterparty",
list_payables_counterparties: "counterparty",
receivables_confirmed_as_of_date: "counterparty",
list_receivables_counterparties: "counterparty",
list_contracts_by_counterparty: "contract",
list_documents_by_counterparty: "document_ref",
@@ -28,6 +29,7 @@ const RESULT_SET_TYPE_BY_INTENT = {
supplier_payouts_profile: "counterparty_list",
list_payables_counterparties: "counterparty_list",
payables_confirmed_as_of_date: "balance_snapshot",
receivables_confirmed_as_of_date: "balance_snapshot",
list_receivables_counterparties: "counterparty_list",
list_contracts_by_counterparty: "contract_list",
list_documents_by_counterparty: "document_list",
+62 -14
View File
@@ -349,6 +349,7 @@ function shouldAttemptCounterpartyCatalogResolution(intent, filters) {
intent === "open_items_by_counterparty_or_contract" ||
intent === "list_payables_counterparties" ||
intent === "payables_confirmed_as_of_date" ||
intent === "receivables_confirmed_as_of_date" ||
intent === "list_receivables_counterparties");
}
async function resolveCounterpartyViaCatalog(anchorRaw) {
@@ -633,6 +634,7 @@ function isCounterpartyRiskIntent(intent) {
return (intent === "list_receivables_counterparties" ||
intent === "list_payables_counterparties" ||
intent === "payables_confirmed_as_of_date" ||
intent === "receivables_confirmed_as_of_date" ||
intent === "list_open_contracts" ||
intent === "open_items_by_counterparty_or_contract");
}
@@ -645,7 +647,8 @@ function isHeuristicCandidatesIntent(intent) {
function isConfirmedBalanceIntent(intent) {
return (intent === "account_balance_snapshot" ||
intent === "documents_forming_balance" ||
intent === "payables_confirmed_as_of_date");
intent === "payables_confirmed_as_of_date" ||
intent === "receivables_confirmed_as_of_date");
}
function resolveAsOfDateBasis(filters) {
const asOfDate = normalizeAnalysisDateHint(filters.as_of_date);
@@ -1296,6 +1299,9 @@ function buildLimitedOffers(input) {
if (input.intent === "list_receivables_counterparties") {
offers.push("показать контрагентов с максимальными хвостами дебиторки по 62/76");
}
else if (input.intent === "receivables_confirmed_as_of_date") {
offers.push("показать подтвержденный реестр открытой дебиторской задолженности на дату среза по 62/76");
}
else if (input.intent === "payables_confirmed_as_of_date") {
offers.push("показать подтвержденный реестр открытых обязательств на дату среза по 60/76");
}
@@ -1341,6 +1347,7 @@ function buildLimitedIntentSignalLine(input) {
list_open_contracts: "Сигнал запроса: нужен список незакрытых договоров на дату.",
list_receivables_counterparties: "Сигнал запроса: нужен ранжированный список должников.",
list_payables_counterparties: "Сигнал запроса: нужен ранжированный список кредиторов.",
receivables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез дебиторской задолженности на дату.",
payables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез обязательств к оплате на дату."
};
const byShape = {
@@ -1467,9 +1474,16 @@ function buildLimitedExecutionResult(input) {
});
const requestedResultMode = resolveRequestedResultMode(input.intent.intent, input.filters);
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"]
const reasons = (input.intent.intent === "payables_confirmed_as_of_date" || input.intent.intent === "receivables_confirmed_as_of_date") &&
!reasonsWithConfirmedFallback.includes(input.intent.intent === "payables_confirmed_as_of_date"
? "exact_payables_mode_limited_response"
: "exact_receivables_mode_limited_response")
? [
...reasonsWithConfirmedFallback,
input.intent.intent === "payables_confirmed_as_of_date"
? "exact_payables_mode_limited_response"
: "exact_receivables_mode_limited_response"
]
: reasonsWithConfirmedFallback;
const routeExpectationAudit = input.routeExpectationAudit ??
buildRouteExpectationAudit({
@@ -1574,11 +1588,16 @@ class AddressQueryService {
}
}
const requestedResultMode = resolveRequestedResultMode(intent.intent, filters.extracted_filters);
const payablesConfirmedExecution = (intent.intent === "list_payables_counterparties" || intent.intent === "payables_confirmed_as_of_date") &&
requestedResultMode === "confirmed_balance"
const confirmedBalancePayablesIntent = (intent.intent === "list_payables_counterparties" || intent.intent === "payables_confirmed_as_of_date") &&
requestedResultMode === "confirmed_balance";
const confirmedBalanceReceivablesIntent = intent.intent === "receivables_confirmed_as_of_date" && requestedResultMode === "confirmed_balance";
const payablesConfirmedExecution = confirmedBalancePayablesIntent
? resolveExecutionFiltersForPayablesConfirmedBalance(filters.extracted_filters, analysisDate)
: null;
const executionFilters = payablesConfirmedExecution?.executionFilters ?? filters.extracted_filters;
const receivablesConfirmedExecution = confirmedBalanceReceivablesIntent
? resolveExecutionFiltersForPayablesConfirmedBalance(filters.extracted_filters, analysisDate)
: null;
const executionFilters = payablesConfirmedExecution?.executionFilters ?? receivablesConfirmedExecution?.executionFilters ?? filters.extracted_filters;
if (payablesConfirmedExecution?.asOfDerived &&
!(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)) {
if (!filters.warnings.includes("as_of_date_derived_for_confirmed_payables")) {
@@ -1588,6 +1607,15 @@ class AddressQueryService {
baseReasons.push("as_of_date_derived_for_confirmed_payables");
}
}
if (receivablesConfirmedExecution?.asOfDerived &&
!(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)) {
if (!filters.warnings.includes("as_of_date_derived_for_confirmed_receivables")) {
filters.warnings.push("as_of_date_derived_for_confirmed_receivables");
}
if (!baseReasons.includes("as_of_date_derived_for_confirmed_receivables")) {
baseReasons.push("as_of_date_derived_for_confirmed_receivables");
}
}
const capabilityDecision = (0, addressCapabilityPolicy_1.resolveAddressCapabilityRouteDecision)(intent.intent);
const capabilityAudit = buildCapabilityAudit(intent.intent);
const shadowRouteAudit = buildShadowRouteAudit({
@@ -1654,6 +1682,10 @@ class AddressQueryService {
if (intent.intent === "payables_confirmed_as_of_date" && !baseReasons.includes("confirmed_balance_exact_payables_intent")) {
baseReasons.push("confirmed_balance_exact_payables_intent");
}
if (intent.intent === "receivables_confirmed_as_of_date" &&
!baseReasons.includes("confirmed_balance_exact_receivables_intent")) {
baseReasons.push("confirmed_balance_exact_receivables_intent");
}
if (requestedResultMode === "confirmed_balance" &&
recipeIntent === "open_items_by_counterparty_or_contract" &&
!baseReasons.includes("confirmed_balance_unavailable_fallback_to_heuristic_candidates")) {
@@ -1789,7 +1821,8 @@ class AddressQueryService {
});
const missingSubcontoFallbackEligible = plan.recipe.recipe_id === "address_movements_receivables_v1" ||
plan.recipe.recipe_id === "address_movements_payables_v1" ||
plan.recipe.recipe_id === "address_payables_confirmed_as_of_date_v1";
plan.recipe.recipe_id === "address_payables_confirmed_as_of_date_v1" ||
plan.recipe.recipe_id === "address_receivables_confirmed_as_of_date_v1";
const missingSubcontoErrorDetected = Boolean(mcp.error && missingSubcontoFallbackEligible && isMissingSubcontoFieldError(mcp.error));
if (missingSubcontoErrorDetected) {
let missingSubcontoResolvedByComposite = false;
@@ -1814,6 +1847,11 @@ class AddressQueryService {
baseReasons.push("confirmed_payables_exact_mode_missing_subconto_axis_fallback_to_composite_subconto");
}
}
else if (intent.intent === "receivables_confirmed_as_of_date") {
if (!baseReasons.includes("confirmed_receivables_exact_mode_missing_subconto_axis_fallback_to_composite_subconto")) {
baseReasons.push("confirmed_receivables_exact_mode_missing_subconto_axis_fallback_to_composite_subconto");
}
}
}
else if (!baseReasons.includes("mcp_missing_subconto_axis_auto_fallback_to_composite_subconto_failed")) {
baseReasons.push("mcp_missing_subconto_axis_auto_fallback_to_composite_subconto_failed");
@@ -1847,6 +1885,13 @@ class AddressQueryService {
baseReasons.push("confirmed_payables_exact_mode_missing_subconto_fallback_to_open_items");
}
}
else if (intent.intent === "receivables_confirmed_as_of_date") {
composeIntent = "list_receivables_counterparties";
routeExpectationIntent = "list_receivables_counterparties";
if (!baseReasons.includes("confirmed_receivables_exact_mode_missing_subconto_fallback_to_open_items")) {
baseReasons.push("confirmed_receivables_exact_mode_missing_subconto_fallback_to_open_items");
}
}
}
else {
if (!baseReasons.includes("mcp_missing_subconto_field_auto_fallback_failed")) {
@@ -1864,8 +1909,8 @@ class AddressQueryService {
if (mcp.error &&
missingSubcontoFallbackEligible &&
isMissingSubcontoFieldError(mcp.error) &&
!baseReasons.includes("confirmed_payables_exact_mode_missing_subconto_no_heuristic_fallback")) {
baseReasons.push("confirmed_payables_exact_mode_missing_subconto_no_heuristic_fallback");
!baseReasons.includes("confirmed_exact_mode_missing_subconto_no_heuristic_fallback")) {
baseReasons.push("confirmed_exact_mode_missing_subconto_no_heuristic_fallback");
}
if (mcp.error) {
const errorScopeAudit = buildDefaultAccountScopeAudit(filters.extracted_filters);
@@ -2613,7 +2658,10 @@ class AddressQueryService {
routeExpectationAudit: finalRouteExpectationAudit
});
}
if (intent.intent === "payables_confirmed_as_of_date" && composeIntent === "payables_confirmed_as_of_date" && factualResultSemantics.balance_confirmed !== true) {
if (((intent.intent === "payables_confirmed_as_of_date" && composeIntent === "payables_confirmed_as_of_date") ||
(intent.intent === "receivables_confirmed_as_of_date" && composeIntent === "receivables_confirmed_as_of_date")) &&
factualResultSemantics.balance_confirmed !== true) {
const exactModeName = intent.intent === "payables_confirmed_as_of_date" ? "payables" : "receivables";
return buildLimitedExecutionResult({
mode,
shape,
@@ -2637,10 +2685,10 @@ class AddressQueryService {
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",
reasonText: `exact ${exactModeName} 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"],
limitations: [`exact_${exactModeName}_mode_unconfirmed_output_blocked`],
reasons: [...baseReasons, `exact_${exactModeName}_mode_unconfirmed_output_blocked`],
capabilityAudit,
shadowRouteAudit,
routeExpectationAudit: finalRouteExpectationAudit
+62 -11
View File
@@ -44,6 +44,28 @@ const PAYABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
УПОРЯДОЧИТЬ ПО
Сумма __ORDER_DIRECTION__
`;
const RECEIVABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
__AS_OF_EXPR__ КАК Период,
"Остатки на дату" КАК Регистратор,
ПРЕДСТАВЛЕНИЕ(Остатки.Счет) КАК СчетДт,
"" КАК СчетКт,
Остатки.СуммаРазвернутыйОстатокДт КАК Сумма,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоДт1,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоДт2,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоДт3,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоКт1,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоКт2,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоКт3,
ПРЕДСТАВЛЕНИЕ(Остатки.Организация) КАК Организация
ИЗ
РегистрБухгалтерии.Хозрасчетный.Остатки(__AS_OF_EXPR__, , , ) КАК Остатки
ГДЕ
Остатки.СуммаРазвернутыйОстатокДт > 0
И (__RECEIVABLE_ACCOUNTS_MATCH__)
УПОРЯДОЧИТЬ ПО
Сумма __ORDER_DIRECTION__
`;
const BANK_DOCS_QUERY_TEMPLATE = `
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
БанкСписание.Дата КАК Период,
@@ -558,6 +580,17 @@ const BASE_RECIPES = [
account_scope_mode: "strict",
query_template: "payables_confirmed_as_of_balance_profile"
},
{
recipe_id: "address_receivables_confirmed_as_of_date_v1",
intent: "receivables_confirmed_as_of_date",
purpose: "Build confirmed receivables snapshot as-of date from movements on accounts 62/76",
required_filters: ["as_of_date"],
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
default_limit: 200,
account_scope: ["62", "76"],
account_scope_mode: "strict",
query_template: "receivables_confirmed_as_of_balance_profile"
},
{
recipe_id: "address_movements_receivables_v1",
intent: "list_receivables_counterparties",
@@ -947,17 +980,35 @@ function buildAddressRecipePlan(recipe, filters) {
.replaceAll("__PAYABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"]))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
})()
: MOVEMENTS_QUERY_TEMPLATE
.replace("__LIMIT__", String(resolvedLimit))
.replace("__WHERE_CLAUSE__", (() => {
const extraConditions = [];
const accountCondition = buildMovementAccountCondition(filters);
if (accountCondition) {
extraConditions.push(accountCondition);
}
return buildWhereClause(filters, "Движения.Период", extraConditions);
})())
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
: recipe.query_template === "receivables_confirmed_as_of_balance_profile"
? (() => {
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
? toDateTimeExpr(filters.as_of_date, true)
: null) ??
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
? toDateTimeExpr(filters.period_to, true)
: null) ??
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
? toDateTimeExpr(filters.period_from, true)
: null) ??
"ТЕКУЩАЯДАТА()";
return RECEIVABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
.replaceAll("__LIMIT__", String(resolvedLimit))
.replaceAll("__AS_OF_EXPR__", asOfExpr)
.replaceAll("__RECEIVABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["62", "76"]))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
})()
: MOVEMENTS_QUERY_TEMPLATE
.replace("__LIMIT__", String(resolvedLimit))
.replace("__WHERE_CLAUSE__", (() => {
const extraConditions = [];
const accountCondition = buildMovementAccountCondition(filters);
if (accountCondition) {
extraConditions.push(accountCondition);
}
return buildWhereClause(filters, "Движения.Период", extraConditions);
})())
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
return {
recipe,
query,
@@ -102,10 +102,7 @@ function formatMoneyRub(value) {
return `${formatNumberWithDots(value, 2)} ₽`;
}
function emphasizeNumericTokens(line) {
if (!line) {
return line;
}
return line.replace(/(?<!\*)\d(?:[\d.,:/-]*\d)?(?!\*)/g, (token) => `**${token}**`);
return line;
}
function parseIsoDateToken(value) {
const source = String(value ?? "").trim();
@@ -514,6 +511,18 @@ function liabilityCategoryLabel(category) {
}
return "прочие";
}
function receivablesCategoryLabel(category) {
if (category === "supplier_or_contractor") {
return "покупатели/заказчики";
}
if (category === "bank_or_credit") {
return "банки/финансовые";
}
if (category === "tax_or_state") {
return "бюджет/госорганы";
}
return "прочие";
}
function classifyPayablesLiabilityCategory(row, counterparty) {
const scores = {
supplier_or_contractor: 0,
@@ -582,6 +591,10 @@ function hasPayablesSectionPrefix(account) {
const section = extractAccountSectionCode(account);
return section === "60" || section === "76";
}
function hasReceivablesSectionPrefix(account) {
const section = extractAccountSectionCode(account);
return section === "62" || section === "76";
}
function resolvePayablesAsOfDate(options) {
const explicit = normalizeIsoDateOnly(options.asOfDate);
if (explicit) {
@@ -762,6 +775,105 @@ function buildPayablesConfirmedBalanceAggregate(rows, asOfDate) {
return left.name.localeCompare(right.name);
});
}
function buildReceivablesConfirmedBalanceAggregate(rows, asOfDate) {
const byCounterparty = new Map();
const asOfTimestamp = toUtcDayTimestamp(asOfDate);
for (const row of rows) {
const name = extractCounterpartyName(row);
if (!name) {
continue;
}
const rowTimestamp = toUtcDayTimestamp(row.period);
if (asOfTimestamp !== null && rowTimestamp !== null && rowTimestamp > asOfTimestamp) {
continue;
}
const amount = row.amount;
if (typeof amount !== "number" || !Number.isFinite(amount)) {
continue;
}
const absAmount = Math.abs(amount);
let delta = 0;
if (hasReceivablesSectionPrefix(row.account_dt)) {
delta += absAmount;
}
if (hasReceivablesSectionPrefix(row.account_kt)) {
delta -= absAmount;
}
if (Math.abs(delta) <= 0.0000001) {
continue;
}
const classified = classifyPayablesLiabilityCategory(row, name);
const contract = extractContractName(row);
const sourceRefs = extractPayablesSourceRefs(row, name, contract);
const current = byCounterparty.get(name);
if (!current) {
byCounterparty.set(name, {
outstandingAmount: delta,
operations: 1,
firstPeriod: row.period,
lastPeriod: row.period,
categoryScores: {
supplier_or_contractor: classified.scores.supplier_or_contractor,
bank_or_credit: classified.scores.bank_or_credit,
tax_or_state: classified.scores.tax_or_state,
other: classified.scores.other
},
reasons: new Set(classified.reasons),
contracts: new Set(contract ? [contract] : []),
documents: new Set(row.registrator ? [row.registrator] : []),
sourceRefs: new Set(sourceRefs)
});
continue;
}
current.outstandingAmount += delta;
current.operations += 1;
if ((row.period ?? "") < (current.firstPeriod ?? "")) {
current.firstPeriod = row.period;
}
if ((row.period ?? "") > (current.lastPeriod ?? "")) {
current.lastPeriod = row.period;
}
current.categoryScores.supplier_or_contractor += classified.scores.supplier_or_contractor;
current.categoryScores.bank_or_credit += classified.scores.bank_or_credit;
current.categoryScores.tax_or_state += classified.scores.tax_or_state;
current.categoryScores.other += classified.scores.other;
for (const reason of classified.reasons) {
current.reasons.add(reason);
}
if (contract) {
current.contracts.add(contract);
}
if (row.registrator) {
current.documents.add(row.registrator);
}
for (const ref of sourceRefs) {
current.sourceRefs.add(ref);
}
}
return Array.from(byCounterparty.entries())
.map(([name, item]) => ({
name,
outstandingAmount: item.outstandingAmount,
operations: item.operations,
firstPeriod: item.firstPeriod,
lastPeriod: item.lastPeriod,
category: resolvePayablesLiabilityCategory(item.categoryScores),
categoryReasons: Array.from(item.reasons).slice(0, 2),
contracts: Array.from(item.contracts).slice(0, 2),
documents: Array.from(item.documents).slice(0, 2),
sourceRefs: Array.from(item.sourceRefs).slice(0, 3)
}))
.filter((item) => item.outstandingAmount > 0.005)
.sort((left, right) => {
if (right.outstandingAmount !== left.outstandingAmount) {
return right.outstandingAmount - left.outstandingAmount;
}
if (right.operations !== left.operations) {
return right.operations - left.operations;
}
return left.name.localeCompare(right.name);
});
}
function buildCounterpartyRiskAggregate(rows) {
const byCounterparty = new Map();
for (const row of rows) {
@@ -1932,7 +2044,7 @@ function composeFactualReply(intent, rows, options = {}) {
`Итого подтвержденный долг на ${formatDateRu(payablesAsOfDate)}: ${formatMoneyRub(totalOutstandingAmount)}.`,
"",
"Блок 1. Статус результата",
"- Режим результата: подтвержденный срез обязательств к оплате (exact route)."
"- Результат: подтвержденный срез обязательств к оплате."
];
lines.push("");
lines.push("Блок 2. Что учтено");
@@ -1957,7 +2069,13 @@ function composeFactualReply(intent, rows, options = {}) {
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)} | остаток: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`));
lines.push(...confirmedBalances.slice(0, 10).flatMap((item, index) => [
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`,
""
]));
if (lines[lines.length - 1] === "") {
lines.pop();
}
}
else {
lines.push("- Подтвержденных открытых обязательств к оплате на дату среза не найдено.");
@@ -1972,6 +2090,73 @@ function composeFactualReply(intent, rows, options = {}) {
}
};
}
if (intent === "receivables_confirmed_as_of_date") {
const receivablesAsOfDate = resolveReceivablesAsOfDate(options);
const confirmedBalances = buildReceivablesConfirmedBalanceAggregate(rows, receivablesAsOfDate);
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
const periodTo = normalizeIsoDateOnly(options.periodTo);
const totalOutstandingAmount = confirmedBalances.reduce((sum, item) => sum + item.outstandingAmount, 0);
const periodScopeLine = !asOfDate && (periodFrom || periodTo)
? `- Период анализа: ${formatDateRu(periodFrom ?? "...")}..${formatDateRu(periodTo ?? "...")}.`
: null;
const carryoverLine = asOfDate || periodFrom || periodTo
? "- В срез могут входить задолженности, возникшие до периода, если они оставались открытыми на дату среза."
: null;
const categoryCounts = confirmedBalances.reduce((acc, item) => {
acc[item.category] += 1;
return acc;
}, { supplier_or_contractor: 0, bank_or_credit: 0, tax_or_state: 0, other: 0 });
const lines = [
`Итого подтвержденная дебиторская задолженность на ${formatDateRu(receivablesAsOfDate)}: ${formatMoneyRub(totalOutstandingAmount)}.`,
"",
"Блок 1. Статус результата",
"- Результат: подтвержденный срез дебиторской задолженности."
];
lines.push("");
lines.push("Блок 2. Что учтено");
lines.push(`- Дата среза: ${formatDateRu(receivablesAsOfDate)}.`);
if (periodScopeLine) {
lines.push(periodScopeLine);
}
lines.push("- Контур: дебиторская задолженность по счетам 62/76.");
if (carryoverLine) {
lines.push(carryoverLine);
}
lines.push("");
lines.push("Блок 3. Сводка");
lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`);
lines.push(`- Контрагентов с подтвержденным остатком к получению: ${formatNumberWithDots(confirmedBalances.length)}.`);
lines.push("");
lines.push("Блок 4. Категории дебиторской задолженности");
lines.push(`- ${receivablesCategoryLabel("supplier_or_contractor")}: ${formatNumberWithDots(categoryCounts.supplier_or_contractor)}.`);
lines.push(`- ${receivablesCategoryLabel("bank_or_credit")}: ${formatNumberWithDots(categoryCounts.bank_or_credit)}.`);
lines.push(`- ${receivablesCategoryLabel("tax_or_state")}: ${formatNumberWithDots(categoryCounts.tax_or_state)}.`);
lines.push(`- ${receivablesCategoryLabel("other")}: ${formatNumberWithDots(categoryCounts.other)}.`);
lines.push("");
lines.push("Блок 5. Подтвержденные позиции к получению");
if (confirmedBalances.length > 0) {
lines.push(...confirmedBalances.slice(0, 10).flatMap((item, index) => [
`${index + 1}. ${item.name} | категория: ${receivablesCategoryLabel(item.category)} | остаток к получению: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`,
""
]));
if (lines[lines.length - 1] === "") {
lines.pop();
}
}
else {
lines.push("- Подтвержденной открытой дебиторской задолженности на дату среза не найдено.");
}
return {
responseType: confirmedBalances.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
text: lines.map(emphasizeNumericTokens).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);
@@ -352,7 +352,8 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
}
if (intent === "open_items_by_counterparty_or_contract" ||
intent === "list_open_contracts" ||
intent === "payables_confirmed_as_of_date") {
intent === "payables_confirmed_as_of_date" ||
intent === "receivables_confirmed_as_of_date") {
const inheritedContract = previousContract ?? (followupContext.previous_anchor_type === "contract" ? previousAnchorValue : null);
const currentContract = toNonEmptyString(merged.contract);
const shouldInheritContract = !currentContract ||
@@ -434,6 +435,7 @@ function resolveMissingRequiredFilters(intent, filters) {
account_balance_snapshot: ["account", "as_of_date"],
documents_forming_balance: ["account", "as_of_date"],
payables_confirmed_as_of_date: ["as_of_date"],
receivables_confirmed_as_of_date: ["as_of_date"],
list_documents_by_counterparty: ["counterparty"],
bank_operations_by_counterparty: ["counterparty"],
list_contracts_by_counterparty: ["counterparty"],
@@ -93,7 +93,8 @@ function inferAggregationProfile(intent, shape) {
}
if (intent === "account_balance_snapshot" ||
intent === "documents_forming_balance" ||
intent === "payables_confirmed_as_of_date") {
intent === "payables_confirmed_as_of_date" ||
intent === "receivables_confirmed_as_of_date") {
return "balance_snapshot";
}
if (intent === "open_items_by_counterparty_or_contract" ||
+34 -9
View File
@@ -568,7 +568,7 @@ function scrubRawTechnicalRefs(value) {
.replace(RAW_REF_TOKEN_PATTERN, "reference")
.replace(/\(\s*\[id\]\s*\)/g, "")
.replace(/\[\s*id\s*\](?:\s*,\s*\[\s*id\s*\])+/g, "[id]")
.replace(/\s{2,}/g, " ")
.replace(/[ \t]{2,}/g, " ")
.trim();
}
function stripSyntheticPlaceholders(value) {
@@ -582,23 +582,48 @@ function stripSyntheticPlaceholders(value) {
}
function sanitizeUserFacingReply(value) {
const raw = String(value ?? "");
const hardCutMatch = raw.match(/(?:^|\n)\s*#{0,6}\s*(?:debug_payload_json|technical_breakdown_json)\b/i);
const hardCutMatch = raw.match(/(?:^|\n)\s*#{0,6}\s*(?:debug_payload_json|technical_breakdown_json|technical_debug_payload_json)\b/i);
const preCut = hardCutMatch ? raw.slice(0, hardCutMatch.index) : raw;
const withoutDebugBlocks = preCut
.replace(/###\s*debug_payload_json[\s\S]*?(?:```[\s\S]*?```|$)/gi, "")
.replace(/###\s*technical_debug_payload_json[\s\S]*?(?:```[\s\S]*?```|$)/gi, "")
.replace(/###\s*technical_breakdown_json[\s\S]*?(?:```[\s\S]*?```|$)/gi, "")
.replace(/(?:^|\n)\s*#{0,6}\s*(?:debug_payload_json|technical_breakdown_json)\b[\s\S]*$/gi, "")
.replace(/(?:^|\n)\s*#{0,6}\s*(?:debug_payload_json|technical_breakdown_json|technical_debug_payload_json)\b[\s\S]*$/gi, "")
.replace(/```json[\s\S]*?```/gi, "");
const normalized = scrubRawTechnicalRefs(withoutDebugBlocks).replace(/[ \t]+\n/g, "\n");
const cleanedLines = normalized
const preparedLines = normalized
.split(/\r?\n/g)
.map((line) => stripSyntheticPlaceholders(line))
.map((line) => stripMojibakeFragments(line))
.map((line) => line.trim())
.filter((line) => line.length > 0)
.filter((line) => !/^(?:-\s*)?(?:action|clarify|open|limit|note):\s*$/i.test(line))
.filter((line) => !hasUserFacingLeakage(line))
.filter((line) => !looksLikeMojibake(line));
.map((line) => line.trim());
const cleanedLines = [];
let previousWasBlank = false;
for (const line of preparedLines) {
if (line.length === 0) {
if (!previousWasBlank && cleanedLines.length > 0) {
cleanedLines.push("");
}
previousWasBlank = true;
continue;
}
if (/^(?:-\s*)?(?:action|clarify|open|limit|note):\s*$/i.test(line)) {
continue;
}
if (hasUserFacingLeakage(line)) {
continue;
}
if (looksLikeMojibake(line)) {
continue;
}
cleanedLines.push(line);
previousWasBlank = false;
}
while (cleanedLines.length > 0 && cleanedLines[0] === "") {
cleanedLines.shift();
}
while (cleanedLines.length > 0 && cleanedLines[cleanedLines.length - 1] === "") {
cleanedLines.pop();
}
const cleaned = cleanedLines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
return cleaned || "Available data requires clarification for a reliable user-facing answer.";
}
@@ -3714,7 +3714,10 @@ const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([
"supplier_payouts_profile",
"list_open_contracts",
"open_items_by_counterparty_or_contract",
"list_payables_counterparties",
"list_receivables_counterparties",
"payables_confirmed_as_of_date",
"receivables_confirmed_as_of_date",
"list_documents_by_contract",
"bank_operations_by_contract",
"list_documents_by_counterparty",