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

This commit is contained in:
dctouch 2026-04-12 20:20:43 +03:00
parent ae9daa50e7
commit 040a55aaea
31 changed files with 1005 additions and 89 deletions

View File

@ -8,6 +8,12 @@
"capability_layer": "compute", "capability_layer": "compute",
"capability_route_mode": "exact" "capability_route_mode": "exact"
}, },
{
"intent": "receivables_confirmed_as_of_date",
"capability_id": "confirmed_receivables_as_of_date",
"capability_layer": "compute",
"capability_route_mode": "exact"
},
{ {
"intent": "list_payables_counterparties", "intent": "list_payables_counterparties",
"capability_id": "payables_candidates_list", "capability_id": "payables_candidates_list",

View File

@ -8,6 +8,12 @@
"expected_requested_result_modes": ["confirmed_balance"], "expected_requested_result_modes": ["confirmed_balance"],
"expected_result_modes": ["confirmed_balance"] "expected_result_modes": ["confirmed_balance"]
}, },
{
"intent": "receivables_confirmed_as_of_date",
"expected_selected_recipes": ["address_receivables_confirmed_as_of_date_v1"],
"expected_requested_result_modes": ["confirmed_balance"],
"expected_result_modes": ["confirmed_balance"]
},
{ {
"intent": "list_payables_counterparties", "intent": "list_payables_counterparties",
"expected_selected_recipes": ["address_movements_payables_v1", "address_open_items_by_party_or_contract_v1"], "expected_selected_recipes": ["address_movements_payables_v1", "address_open_items_by_party_or_contract_v1"],

View File

@ -3,8 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod }; return (mod && mod.__esModule) ? mod : { "default": mod };
}; };
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.PRESETS_DIR = exports.TRACES_DIR = exports.DATA_DIR = exports.VAT_PAYABLE_19_PREFIXES = exports.VAT_PAYABLE_68_PREFIXES = exports.ASSISTANT_MCP_LIVE_LIMIT = exports.ASSISTANT_MCP_TIMEOUT_MS = exports.ASSISTANT_MCP_CHANNEL = exports.ASSISTANT_MCP_PROXY_URL = exports.FEATURE_ASSISTANT_LIVING_CHAT_ROUTER_V1 = exports.FEATURE_ASSISTANT_ROUTE_EXPECTATION_HARD_GUARD_V1 = exports.FEATURE_ASSISTANT_ROUTE_EXPECTATION_AUDIT_V1 = exports.FEATURE_ASSISTANT_ROUTE_SHADOW_PAYABLES_EXACT_V1 = exports.FEATURE_ASSISTANT_ROUTE_RECEIVABLES_HEURISTIC_V1 = exports.FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1 = exports.FEATURE_ASSISTANT_ROUTE_PAYABLES_CONFIRMED_V1 = exports.FEATURE_ASSISTANT_ROUTE_BALANCE_EXACT_V1 = exports.FEATURE_ASSISTANT_ROUTE_DRILLDOWN_V1 = exports.FEATURE_ASSISTANT_ROUTE_ADDRESS_GENERIC_V1 = exports.FEATURE_ASSISTANT_CAPABILITY_ROUTE_GUARD_V1 = exports.FEATURE_ASSISTANT_ADDRESS_NAVIGATION_STATE_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_LIVE_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_V1 = exports.FEATURE_ASSISTANT_MCP_RUNTIME_V1 = exports.FEATURE_ASSISTANT_GRAPH_RUNTIME_V1 = exports.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1 = exports.FEATURE_ASSISTANT_LIFECYCLE_RUNTIME_V1 = exports.FEATURE_ASSISTANT_STAGE2_EVAL_V1 = exports.FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1 = exports.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1 = exports.FEATURE_ASSISTANT_PROBLEM_UNITS_V1 = exports.FEATURE_ASSISTANT_ACCOUNTANT_EVAL_V1 = exports.FEATURE_ASSISTANT_ANSWER_POLICY_V11 = exports.FEATURE_ASSISTANT_ANTI_GENERIC_RANKING_GUARD_V1 = exports.FEATURE_ASSISTANT_MIN_EVIDENCE_GATE_V1 = exports.FEATURE_ASSISTANT_BROAD_GUARD_V1 = exports.FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1 = exports.FEATURE_ASSISTANT_STATE_FOLLOWUP_BINDING_V1 = exports.FEATURE_ASSISTANT_CONTRACTS_V11 = exports.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 = exports.DEFAULT_PROMPT_VERSION = exports.DEFAULT_MAX_OUTPUT_TOKENS = exports.DEFAULT_TEMPERATURE = exports.DEFAULT_MODEL = exports.DEFAULT_OPENAI_BASE_URL = exports.TIMEZONE = exports.PORT = exports.MODULE_ROOT = exports.BACKEND_ROOT = void 0; exports.TRACES_DIR = exports.DATA_DIR = exports.VAT_PAYABLE_19_PREFIXES = exports.VAT_PAYABLE_68_PREFIXES = exports.ASSISTANT_MCP_LIVE_LIMIT = exports.ASSISTANT_MCP_TIMEOUT_MS = exports.ASSISTANT_MCP_CHANNEL = exports.ASSISTANT_MCP_PROXY_URL = exports.FEATURE_ASSISTANT_LIVING_CHAT_ROUTER_V1 = exports.FEATURE_ASSISTANT_ROUTE_EXPECTATION_HARD_GUARD_V1 = exports.FEATURE_ASSISTANT_ROUTE_EXPECTATION_AUDIT_V1 = exports.FEATURE_ASSISTANT_ROUTE_SHADOW_PAYABLES_EXACT_V1 = exports.FEATURE_ASSISTANT_ROUTE_RECEIVABLES_HEURISTIC_V1 = exports.FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1 = exports.FEATURE_ASSISTANT_ROUTE_RECEIVABLES_CONFIRMED_V1 = exports.FEATURE_ASSISTANT_ROUTE_PAYABLES_CONFIRMED_V1 = exports.FEATURE_ASSISTANT_ROUTE_BALANCE_EXACT_V1 = exports.FEATURE_ASSISTANT_ROUTE_DRILLDOWN_V1 = exports.FEATURE_ASSISTANT_ROUTE_ADDRESS_GENERIC_V1 = exports.FEATURE_ASSISTANT_CAPABILITY_ROUTE_GUARD_V1 = exports.FEATURE_ASSISTANT_ADDRESS_NAVIGATION_STATE_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_LIVE_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_V1 = exports.FEATURE_ASSISTANT_MCP_RUNTIME_V1 = exports.FEATURE_ASSISTANT_GRAPH_RUNTIME_V1 = exports.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1 = exports.FEATURE_ASSISTANT_LIFECYCLE_RUNTIME_V1 = exports.FEATURE_ASSISTANT_STAGE2_EVAL_V1 = exports.FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1 = exports.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1 = exports.FEATURE_ASSISTANT_PROBLEM_UNITS_V1 = exports.FEATURE_ASSISTANT_ACCOUNTANT_EVAL_V1 = exports.FEATURE_ASSISTANT_ANSWER_POLICY_V11 = exports.FEATURE_ASSISTANT_ANTI_GENERIC_RANKING_GUARD_V1 = exports.FEATURE_ASSISTANT_MIN_EVIDENCE_GATE_V1 = exports.FEATURE_ASSISTANT_BROAD_GUARD_V1 = exports.FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1 = exports.FEATURE_ASSISTANT_STATE_FOLLOWUP_BINDING_V1 = exports.FEATURE_ASSISTANT_CONTRACTS_V11 = exports.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 = exports.DEFAULT_PROMPT_VERSION = exports.DEFAULT_MAX_OUTPUT_TOKENS = exports.DEFAULT_TEMPERATURE = exports.DEFAULT_MODEL = exports.DEFAULT_OPENAI_BASE_URL = exports.TIMEZONE = exports.PORT = exports.MODULE_ROOT = exports.BACKEND_ROOT = void 0;
exports.MANUAL_CASE_DECISION_SCHEMA_FILE = exports.ASSISTANT_CAPABILITIES_REGISTRY_FILE = exports.ASSISTANT_CANON_FILE = exports.ARCH_EXPORT_2020_DIR = exports.SCHEMAS_DIR = exports.EVAL_DATASETS_DIR = exports.REPORTS_DIR = exports.PROMPTS_DIR = exports.AUTORUN_GENERATOR_HISTORY_FILE = exports.AUTORUN_GENERATOR_DIR = exports.AUTORUN_ANNOTATIONS_FILE = exports.AUTORUN_ANNOTATIONS_DIR = exports.ASSISTANT_ANNOTATIONS_FILE = exports.ASSISTANT_ANNOTATIONS_DIR = exports.ASSISTANT_SESSIONS_DIR = exports.EVAL_CASES_DIR = void 0; exports.MANUAL_CASE_DECISION_SCHEMA_FILE = exports.ASSISTANT_CAPABILITIES_REGISTRY_FILE = exports.ASSISTANT_CANON_FILE = exports.ARCH_EXPORT_2020_DIR = exports.SCHEMAS_DIR = exports.EVAL_DATASETS_DIR = exports.REPORTS_DIR = exports.PROMPTS_DIR = exports.AUTORUN_GENERATOR_HISTORY_FILE = exports.AUTORUN_GENERATOR_DIR = exports.AUTORUN_ANNOTATIONS_FILE = exports.AUTORUN_ANNOTATIONS_DIR = exports.ASSISTANT_ANNOTATIONS_FILE = exports.ASSISTANT_ANNOTATIONS_DIR = exports.ASSISTANT_SESSIONS_DIR = exports.EVAL_CASES_DIR = exports.PRESETS_DIR = void 0;
const path_1 = __importDefault(require("path")); const path_1 = __importDefault(require("path"));
exports.BACKEND_ROOT = path_1.default.resolve(__dirname, ".."); exports.BACKEND_ROOT = path_1.default.resolve(__dirname, "..");
exports.MODULE_ROOT = path_1.default.resolve(exports.BACKEND_ROOT, ".."); exports.MODULE_ROOT = path_1.default.resolve(exports.BACKEND_ROOT, "..");
@ -66,6 +66,7 @@ exports.FEATURE_ASSISTANT_ROUTE_ADDRESS_GENERIC_V1 = toBooleanFlag(process.env.F
exports.FEATURE_ASSISTANT_ROUTE_DRILLDOWN_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_ROUTE_DRILLDOWN_V1, true); exports.FEATURE_ASSISTANT_ROUTE_DRILLDOWN_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_ROUTE_DRILLDOWN_V1, true);
exports.FEATURE_ASSISTANT_ROUTE_BALANCE_EXACT_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_ROUTE_BALANCE_EXACT_V1, true); exports.FEATURE_ASSISTANT_ROUTE_BALANCE_EXACT_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_ROUTE_BALANCE_EXACT_V1, true);
exports.FEATURE_ASSISTANT_ROUTE_PAYABLES_CONFIRMED_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_ROUTE_PAYABLES_CONFIRMED_V1, true); exports.FEATURE_ASSISTANT_ROUTE_PAYABLES_CONFIRMED_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_ROUTE_PAYABLES_CONFIRMED_V1, true);
exports.FEATURE_ASSISTANT_ROUTE_RECEIVABLES_CONFIRMED_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_ROUTE_RECEIVABLES_CONFIRMED_V1, true);
exports.FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1, true); exports.FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1, true);
exports.FEATURE_ASSISTANT_ROUTE_RECEIVABLES_HEURISTIC_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_ROUTE_RECEIVABLES_HEURISTIC_V1, true); exports.FEATURE_ASSISTANT_ROUTE_RECEIVABLES_HEURISTIC_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_ROUTE_RECEIVABLES_HEURISTIC_V1, true);
exports.FEATURE_ASSISTANT_ROUTE_SHADOW_PAYABLES_EXACT_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_ROUTE_SHADOW_PAYABLES_EXACT_V1, false); exports.FEATURE_ASSISTANT_ROUTE_SHADOW_PAYABLES_EXACT_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_ROUTE_SHADOW_PAYABLES_EXACT_V1, false);

View File

@ -4,7 +4,12 @@ exports.resolveAddressCapabilityRouteDecision = resolveAddressCapabilityRouteDec
exports.isCapabilityRouteBlocked = isCapabilityRouteBlocked; exports.isCapabilityRouteBlocked = isCapabilityRouteBlocked;
exports.resolveShadowRouteIntent = resolveShadowRouteIntent; exports.resolveShadowRouteIntent = resolveShadowRouteIntent;
const config_1 = require("../config"); 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([ const NAVIGATION_INTENTS = new Set([
"list_documents_by_counterparty", "list_documents_by_counterparty",
"bank_operations_by_counterparty", "bank_operations_by_counterparty",
@ -31,6 +36,9 @@ function defaultCapabilityId(intent) {
if (intent === "payables_confirmed_as_of_date") { if (intent === "payables_confirmed_as_of_date") {
return "confirmed_payables_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") { if (intent === "list_payables_counterparties") {
return "payables_candidates_list"; 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" 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") { if (intent === "list_payables_counterparties") {
return { return {
enabled: config_1.FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1, enabled: config_1.FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1,

View File

@ -748,6 +748,9 @@ function requiredFiltersByIntent(intent) {
if (intent === "payables_confirmed_as_of_date") { if (intent === "payables_confirmed_as_of_date") {
return ["as_of_date"]; return ["as_of_date"];
} }
if (intent === "receivables_confirmed_as_of_date") {
return ["as_of_date"];
}
if (intent === "list_documents_by_counterparty" || if (intent === "list_documents_by_counterparty" ||
intent === "bank_operations_by_counterparty" || intent === "bank_operations_by_counterparty" ||
intent === "list_contracts_by_counterparty") { intent === "list_contracts_by_counterparty") {
@ -761,7 +764,8 @@ function requiredFiltersByIntent(intent) {
function usesAsOfPrimaryWindow(intent) { function usesAsOfPrimaryWindow(intent) {
return (intent === "open_items_by_counterparty_or_contract" || return (intent === "open_items_by_counterparty_or_contract" ||
intent === "list_open_contracts" || 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) { function extractAddressFilters(userMessage, intent) {
const rawText = String(userMessage ?? "").trim(); const rawText = String(userMessage ?? "").trim();
@ -923,7 +927,8 @@ function extractAddressFilters(userMessage, intent) {
// - else default to today. // - else default to today.
if ((intent === "account_balance_snapshot" || if ((intent === "account_balance_snapshot" ||
intent === "documents_forming_balance" || 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) { !filters.as_of_date) {
if (filters.period_to) { if (filters.period_to) {
filters.as_of_date = filters.period_to; filters.as_of_date = filters.period_to;

View File

@ -3,7 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveAddressIntent = resolveAddressIntent; exports.resolveAddressIntent = resolveAddressIntent;
const RECEIVABLES_STRONG = [ const RECEIVABLES_STRONG = [
"кто должен нам", "кто должен нам",
"кто нам должен",
"кто нам должэн",
"нам должны", "нам должны",
"нам должен",
"нам должэны",
"who owes us", "who owes us",
"receivable", "receivable",
"receivables", "receivables",
@ -14,7 +18,11 @@ const RECEIVABLES_STRONG = [
]; ];
const PAYABLES_STRONG = [ const PAYABLES_STRONG = [
"кому должны мы", "кому должны мы",
"кому должэны мы",
"кому мы должны",
"кому мы должэны",
"мы должны", "мы должны",
"мы должэны",
"who we owe", "who we owe",
"payable", "payable",
"payables", "payables",
@ -865,6 +873,18 @@ function hasPayablesDebtLifecycleSignal(text) {
} }
return true; 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) { function hasReceivablesLatencyRiskSignal(text) {
const hasBuyer = /(?:покупател|клиент|заказчик|customer|buyer)/iu.test(text); const hasBuyer = /(?:покупател|клиент|заказчик|customer|buyer)/iu.test(text);
const hasCounterparty = /(?:контрагент|counterparty|partner)/iu.test(text); const hasCounterparty = /(?:контрагент|counterparty|partner)/iu.test(text);
@ -1239,10 +1259,15 @@ function resolveAddressIntent(userMessage) {
}; };
} }
if (hasAny(text, RECEIVABLES_STRONG)) { if (hasAny(text, RECEIVABLES_STRONG)) {
const receivablesDebtLifecycleSignal = hasReceivablesDebtLifecycleSignal(text);
const reasons = ["receivables_signal_detected"];
if (receivablesDebtLifecycleSignal) {
reasons.push("receivables_debt_lifecycle_signal_detected");
}
return { return {
intent: "list_receivables_counterparties", intent: receivablesDebtLifecycleSignal ? "receivables_confirmed_as_of_date" : "list_receivables_counterparties",
confidence: "high", confidence: "high",
reasons: ["receivables_signal_detected"] reasons
}; };
} }
if (hasAny(text, PAYABLES_STRONG)) { if (hasAny(text, PAYABLES_STRONG)) {

View File

@ -14,6 +14,7 @@ const DISPLAY_ENTITY_TYPE_BY_INTENT = {
customer_revenue_and_payments: "counterparty", customer_revenue_and_payments: "counterparty",
supplier_payouts_profile: "counterparty", supplier_payouts_profile: "counterparty",
list_payables_counterparties: "counterparty", list_payables_counterparties: "counterparty",
receivables_confirmed_as_of_date: "counterparty",
list_receivables_counterparties: "counterparty", list_receivables_counterparties: "counterparty",
list_contracts_by_counterparty: "contract", list_contracts_by_counterparty: "contract",
list_documents_by_counterparty: "document_ref", list_documents_by_counterparty: "document_ref",
@ -28,6 +29,7 @@ const RESULT_SET_TYPE_BY_INTENT = {
supplier_payouts_profile: "counterparty_list", supplier_payouts_profile: "counterparty_list",
list_payables_counterparties: "counterparty_list", list_payables_counterparties: "counterparty_list",
payables_confirmed_as_of_date: "balance_snapshot", payables_confirmed_as_of_date: "balance_snapshot",
receivables_confirmed_as_of_date: "balance_snapshot",
list_receivables_counterparties: "counterparty_list", list_receivables_counterparties: "counterparty_list",
list_contracts_by_counterparty: "contract_list", list_contracts_by_counterparty: "contract_list",
list_documents_by_counterparty: "document_list", list_documents_by_counterparty: "document_list",

View File

@ -349,6 +349,7 @@ function shouldAttemptCounterpartyCatalogResolution(intent, filters) {
intent === "open_items_by_counterparty_or_contract" || intent === "open_items_by_counterparty_or_contract" ||
intent === "list_payables_counterparties" || intent === "list_payables_counterparties" ||
intent === "payables_confirmed_as_of_date" || intent === "payables_confirmed_as_of_date" ||
intent === "receivables_confirmed_as_of_date" ||
intent === "list_receivables_counterparties"); intent === "list_receivables_counterparties");
} }
async function resolveCounterpartyViaCatalog(anchorRaw) { async function resolveCounterpartyViaCatalog(anchorRaw) {
@ -633,6 +634,7 @@ function isCounterpartyRiskIntent(intent) {
return (intent === "list_receivables_counterparties" || return (intent === "list_receivables_counterparties" ||
intent === "list_payables_counterparties" || intent === "list_payables_counterparties" ||
intent === "payables_confirmed_as_of_date" || intent === "payables_confirmed_as_of_date" ||
intent === "receivables_confirmed_as_of_date" ||
intent === "list_open_contracts" || intent === "list_open_contracts" ||
intent === "open_items_by_counterparty_or_contract"); intent === "open_items_by_counterparty_or_contract");
} }
@ -645,7 +647,8 @@ function isHeuristicCandidatesIntent(intent) {
function isConfirmedBalanceIntent(intent) { function isConfirmedBalanceIntent(intent) {
return (intent === "account_balance_snapshot" || return (intent === "account_balance_snapshot" ||
intent === "documents_forming_balance" || 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) { function resolveAsOfDateBasis(filters) {
const asOfDate = normalizeAnalysisDateHint(filters.as_of_date); const asOfDate = normalizeAnalysisDateHint(filters.as_of_date);
@ -1296,6 +1299,9 @@ function buildLimitedOffers(input) {
if (input.intent === "list_receivables_counterparties") { if (input.intent === "list_receivables_counterparties") {
offers.push("показать контрагентов с максимальными хвостами дебиторки по 62/76"); 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") { else if (input.intent === "payables_confirmed_as_of_date") {
offers.push("показать подтвержденный реестр открытых обязательств на дату среза по 60/76"); offers.push("показать подтвержденный реестр открытых обязательств на дату среза по 60/76");
} }
@ -1341,6 +1347,7 @@ function buildLimitedIntentSignalLine(input) {
list_open_contracts: "Сигнал запроса: нужен список незакрытых договоров на дату.", list_open_contracts: "Сигнал запроса: нужен список незакрытых договоров на дату.",
list_receivables_counterparties: "Сигнал запроса: нужен ранжированный список должников.", list_receivables_counterparties: "Сигнал запроса: нужен ранжированный список должников.",
list_payables_counterparties: "Сигнал запроса: нужен ранжированный список кредиторов.", list_payables_counterparties: "Сигнал запроса: нужен ранжированный список кредиторов.",
receivables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез дебиторской задолженности на дату.",
payables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез обязательств к оплате на дату." payables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез обязательств к оплате на дату."
}; };
const byShape = { const byShape = {
@ -1467,9 +1474,16 @@ function buildLimitedExecutionResult(input) {
}); });
const requestedResultMode = resolveRequestedResultMode(input.intent.intent, input.filters); const requestedResultMode = resolveRequestedResultMode(input.intent.intent, input.filters);
const reasonsWithConfirmedFallback = withConfirmedBalanceFallbackReason(input.reasons, requestedResultMode, undefined, resultSemantics.result_mode); const reasonsWithConfirmedFallback = withConfirmedBalanceFallbackReason(input.reasons, requestedResultMode, undefined, resultSemantics.result_mode);
const reasons = input.intent.intent === "payables_confirmed_as_of_date" && const reasons = (input.intent.intent === "payables_confirmed_as_of_date" || input.intent.intent === "receivables_confirmed_as_of_date") &&
!reasonsWithConfirmedFallback.includes("exact_payables_mode_limited_response") !reasonsWithConfirmedFallback.includes(input.intent.intent === "payables_confirmed_as_of_date"
? [...reasonsWithConfirmedFallback, "exact_payables_mode_limited_response"] ? "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; : reasonsWithConfirmedFallback;
const routeExpectationAudit = input.routeExpectationAudit ?? const routeExpectationAudit = input.routeExpectationAudit ??
buildRouteExpectationAudit({ buildRouteExpectationAudit({
@ -1574,11 +1588,16 @@ class AddressQueryService {
} }
} }
const requestedResultMode = resolveRequestedResultMode(intent.intent, filters.extracted_filters); const requestedResultMode = resolveRequestedResultMode(intent.intent, filters.extracted_filters);
const payablesConfirmedExecution = (intent.intent === "list_payables_counterparties" || intent.intent === "payables_confirmed_as_of_date") && const confirmedBalancePayablesIntent = (intent.intent === "list_payables_counterparties" || intent.intent === "payables_confirmed_as_of_date") &&
requestedResultMode === "confirmed_balance" requestedResultMode === "confirmed_balance";
const confirmedBalanceReceivablesIntent = intent.intent === "receivables_confirmed_as_of_date" && requestedResultMode === "confirmed_balance";
const payablesConfirmedExecution = confirmedBalancePayablesIntent
? resolveExecutionFiltersForPayablesConfirmedBalance(filters.extracted_filters, analysisDate) ? resolveExecutionFiltersForPayablesConfirmedBalance(filters.extracted_filters, analysisDate)
: null; : 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 && if (payablesConfirmedExecution?.asOfDerived &&
!(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)) { !(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")) { 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"); 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 capabilityDecision = (0, addressCapabilityPolicy_1.resolveAddressCapabilityRouteDecision)(intent.intent);
const capabilityAudit = buildCapabilityAudit(intent.intent); const capabilityAudit = buildCapabilityAudit(intent.intent);
const shadowRouteAudit = buildShadowRouteAudit({ const shadowRouteAudit = buildShadowRouteAudit({
@ -1654,6 +1682,10 @@ class AddressQueryService {
if (intent.intent === "payables_confirmed_as_of_date" && !baseReasons.includes("confirmed_balance_exact_payables_intent")) { if (intent.intent === "payables_confirmed_as_of_date" && !baseReasons.includes("confirmed_balance_exact_payables_intent")) {
baseReasons.push("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" && if (requestedResultMode === "confirmed_balance" &&
recipeIntent === "open_items_by_counterparty_or_contract" && recipeIntent === "open_items_by_counterparty_or_contract" &&
!baseReasons.includes("confirmed_balance_unavailable_fallback_to_heuristic_candidates")) { !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" || const missingSubcontoFallbackEligible = plan.recipe.recipe_id === "address_movements_receivables_v1" ||
plan.recipe.recipe_id === "address_movements_payables_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)); const missingSubcontoErrorDetected = Boolean(mcp.error && missingSubcontoFallbackEligible && isMissingSubcontoFieldError(mcp.error));
if (missingSubcontoErrorDetected) { if (missingSubcontoErrorDetected) {
let missingSubcontoResolvedByComposite = false; let missingSubcontoResolvedByComposite = false;
@ -1814,6 +1847,11 @@ class AddressQueryService {
baseReasons.push("confirmed_payables_exact_mode_missing_subconto_axis_fallback_to_composite_subconto"); 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")) { 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"); 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"); 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 { else {
if (!baseReasons.includes("mcp_missing_subconto_field_auto_fallback_failed")) { if (!baseReasons.includes("mcp_missing_subconto_field_auto_fallback_failed")) {
@ -1864,8 +1909,8 @@ class AddressQueryService {
if (mcp.error && if (mcp.error &&
missingSubcontoFallbackEligible && missingSubcontoFallbackEligible &&
isMissingSubcontoFieldError(mcp.error) && isMissingSubcontoFieldError(mcp.error) &&
!baseReasons.includes("confirmed_payables_exact_mode_missing_subconto_no_heuristic_fallback")) { !baseReasons.includes("confirmed_exact_mode_missing_subconto_no_heuristic_fallback")) {
baseReasons.push("confirmed_payables_exact_mode_missing_subconto_no_heuristic_fallback"); baseReasons.push("confirmed_exact_mode_missing_subconto_no_heuristic_fallback");
} }
if (mcp.error) { if (mcp.error) {
const errorScopeAudit = buildDefaultAccountScopeAudit(filters.extracted_filters); const errorScopeAudit = buildDefaultAccountScopeAudit(filters.extracted_filters);
@ -2613,7 +2658,10 @@ class AddressQueryService {
routeExpectationAudit: finalRouteExpectationAudit 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({ return buildLimitedExecutionResult({
mode, mode,
shape, shape,
@ -2637,10 +2685,10 @@ class AddressQueryService {
rawRowKeysSample: rowDiagnostics.rawRowKeysSample, rawRowKeysSample: rowDiagnostics.rawRowKeysSample,
materializationDropReason: rowDiagnostics.materializationDropReason, materializationDropReason: rowDiagnostics.materializationDropReason,
category: "recipe_visibility_gap", 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", nextStep: "specify as_of_date/counterparty or enable detailed settlement registers for exact confirmed balance",
limitations: ["exact_payables_mode_unconfirmed_output_blocked"], limitations: [`exact_${exactModeName}_mode_unconfirmed_output_blocked`],
reasons: [...baseReasons, "exact_payables_mode_unconfirmed_output_blocked"], reasons: [...baseReasons, `exact_${exactModeName}_mode_unconfirmed_output_blocked`],
capabilityAudit, capabilityAudit,
shadowRouteAudit, shadowRouteAudit,
routeExpectationAudit: finalRouteExpectationAudit routeExpectationAudit: finalRouteExpectationAudit

View File

@ -44,6 +44,28 @@ const PAYABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
УПОРЯДОЧИТЬ ПО УПОРЯДОЧИТЬ ПО
Сумма __ORDER_DIRECTION__ Сумма __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 = ` const BANK_DOCS_QUERY_TEMPLATE = `
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__ ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
БанкСписание.Дата КАК Период, БанкСписание.Дата КАК Период,
@ -558,6 +580,17 @@ const BASE_RECIPES = [
account_scope_mode: "strict", account_scope_mode: "strict",
query_template: "payables_confirmed_as_of_balance_profile" 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", recipe_id: "address_movements_receivables_v1",
intent: "list_receivables_counterparties", intent: "list_receivables_counterparties",
@ -947,17 +980,35 @@ function buildAddressRecipePlan(recipe, filters) {
.replaceAll("__PAYABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"])) .replaceAll("__PAYABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"]))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort)); .replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
})() })()
: MOVEMENTS_QUERY_TEMPLATE : recipe.query_template === "receivables_confirmed_as_of_balance_profile"
.replace("__LIMIT__", String(resolvedLimit)) ? (() => {
.replace("__WHERE_CLAUSE__", (() => { const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
const extraConditions = []; ? toDateTimeExpr(filters.as_of_date, true)
const accountCondition = buildMovementAccountCondition(filters); : null) ??
if (accountCondition) { (typeof filters.period_to === "string" && filters.period_to.trim().length > 0
extraConditions.push(accountCondition); ? toDateTimeExpr(filters.period_to, true)
} : null) ??
return buildWhereClause(filters, "Движения.Период", extraConditions); (typeof filters.period_from === "string" && filters.period_from.trim().length > 0
})()) ? toDateTimeExpr(filters.period_from, true)
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort)); : 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 { return {
recipe, recipe,
query, query,

View File

@ -102,10 +102,7 @@ function formatMoneyRub(value) {
return `${formatNumberWithDots(value, 2)}`; return `${formatNumberWithDots(value, 2)}`;
} }
function emphasizeNumericTokens(line) { function emphasizeNumericTokens(line) {
if (!line) { return line;
return line;
}
return line.replace(/(?<!\*)\d(?:[\d.,:/-]*\d)?(?!\*)/g, (token) => `**${token}**`);
} }
function parseIsoDateToken(value) { function parseIsoDateToken(value) {
const source = String(value ?? "").trim(); const source = String(value ?? "").trim();
@ -514,6 +511,18 @@ function liabilityCategoryLabel(category) {
} }
return "прочие"; 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) { function classifyPayablesLiabilityCategory(row, counterparty) {
const scores = { const scores = {
supplier_or_contractor: 0, supplier_or_contractor: 0,
@ -582,6 +591,10 @@ function hasPayablesSectionPrefix(account) {
const section = extractAccountSectionCode(account); const section = extractAccountSectionCode(account);
return section === "60" || section === "76"; return section === "60" || section === "76";
} }
function hasReceivablesSectionPrefix(account) {
const section = extractAccountSectionCode(account);
return section === "62" || section === "76";
}
function resolvePayablesAsOfDate(options) { function resolvePayablesAsOfDate(options) {
const explicit = normalizeIsoDateOnly(options.asOfDate); const explicit = normalizeIsoDateOnly(options.asOfDate);
if (explicit) { if (explicit) {
@ -762,6 +775,105 @@ function buildPayablesConfirmedBalanceAggregate(rows, asOfDate) {
return left.name.localeCompare(right.name); 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) { function buildCounterpartyRiskAggregate(rows) {
const byCounterparty = new Map(); const byCounterparty = new Map();
for (const row of rows) { for (const row of rows) {
@ -1932,7 +2044,7 @@ function composeFactualReply(intent, rows, options = {}) {
`Итого подтвержденный долг на ${formatDateRu(payablesAsOfDate)}: ${formatMoneyRub(totalOutstandingAmount)}.`, `Итого подтвержденный долг на ${formatDateRu(payablesAsOfDate)}: ${formatMoneyRub(totalOutstandingAmount)}.`,
"", "",
"Блок 1. Статус результата", "Блок 1. Статус результата",
"- Режим результата: подтвержденный срез обязательств к оплате (exact route)." "- Результат: подтвержденный срез обязательств к оплате."
]; ];
lines.push(""); lines.push("");
lines.push("Блок 2. Что учтено"); lines.push("Блок 2. Что учтено");
@ -1957,7 +2069,13 @@ function composeFactualReply(intent, rows, options = {}) {
lines.push(""); lines.push("");
lines.push("Блок 5. Подтвержденные позиции к оплате"); lines.push("Блок 5. Подтвержденные позиции к оплате");
if (confirmedBalances.length > 0) { 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 { else {
lines.push("- Подтвержденных открытых обязательств к оплате на дату среза не найдено."); 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") { if (intent === "list_payables_counterparties") {
const counterparties = buildPayablesCounterpartyRiskAggregate(rows); const counterparties = buildPayablesCounterpartyRiskAggregate(rows);
const payablesAsOfDate = resolvePayablesAsOfDate(options); const payablesAsOfDate = resolvePayablesAsOfDate(options);

View File

@ -352,7 +352,8 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
} }
if (intent === "open_items_by_counterparty_or_contract" || if (intent === "open_items_by_counterparty_or_contract" ||
intent === "list_open_contracts" || 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 inheritedContract = previousContract ?? (followupContext.previous_anchor_type === "contract" ? previousAnchorValue : null);
const currentContract = toNonEmptyString(merged.contract); const currentContract = toNonEmptyString(merged.contract);
const shouldInheritContract = !currentContract || const shouldInheritContract = !currentContract ||
@ -434,6 +435,7 @@ function resolveMissingRequiredFilters(intent, filters) {
account_balance_snapshot: ["account", "as_of_date"], account_balance_snapshot: ["account", "as_of_date"],
documents_forming_balance: ["account", "as_of_date"], documents_forming_balance: ["account", "as_of_date"],
payables_confirmed_as_of_date: ["as_of_date"], payables_confirmed_as_of_date: ["as_of_date"],
receivables_confirmed_as_of_date: ["as_of_date"],
list_documents_by_counterparty: ["counterparty"], list_documents_by_counterparty: ["counterparty"],
bank_operations_by_counterparty: ["counterparty"], bank_operations_by_counterparty: ["counterparty"],
list_contracts_by_counterparty: ["counterparty"], list_contracts_by_counterparty: ["counterparty"],

View File

@ -93,7 +93,8 @@ function inferAggregationProfile(intent, shape) {
} }
if (intent === "account_balance_snapshot" || if (intent === "account_balance_snapshot" ||
intent === "documents_forming_balance" || 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"; return "balance_snapshot";
} }
if (intent === "open_items_by_counterparty_or_contract" || if (intent === "open_items_by_counterparty_or_contract" ||

View File

@ -568,7 +568,7 @@ function scrubRawTechnicalRefs(value) {
.replace(RAW_REF_TOKEN_PATTERN, "reference") .replace(RAW_REF_TOKEN_PATTERN, "reference")
.replace(/\(\s*\[id\]\s*\)/g, "") .replace(/\(\s*\[id\]\s*\)/g, "")
.replace(/\[\s*id\s*\](?:\s*,\s*\[\s*id\s*\])+/g, "[id]") .replace(/\[\s*id\s*\](?:\s*,\s*\[\s*id\s*\])+/g, "[id]")
.replace(/\s{2,}/g, " ") .replace(/[ \t]{2,}/g, " ")
.trim(); .trim();
} }
function stripSyntheticPlaceholders(value) { function stripSyntheticPlaceholders(value) {
@ -582,23 +582,48 @@ function stripSyntheticPlaceholders(value) {
} }
function sanitizeUserFacingReply(value) { function sanitizeUserFacingReply(value) {
const raw = String(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 preCut = hardCutMatch ? raw.slice(0, hardCutMatch.index) : raw;
const withoutDebugBlocks = preCut const withoutDebugBlocks = preCut
.replace(/###\s*debug_payload_json[\s\S]*?(?:```[\s\S]*?```|$)/gi, "") .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(/###\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, ""); .replace(/```json[\s\S]*?```/gi, "");
const normalized = scrubRawTechnicalRefs(withoutDebugBlocks).replace(/[ \t]+\n/g, "\n"); const normalized = scrubRawTechnicalRefs(withoutDebugBlocks).replace(/[ \t]+\n/g, "\n");
const cleanedLines = normalized const preparedLines = normalized
.split(/\r?\n/g) .split(/\r?\n/g)
.map((line) => stripSyntheticPlaceholders(line)) .map((line) => stripSyntheticPlaceholders(line))
.map((line) => stripMojibakeFragments(line)) .map((line) => stripMojibakeFragments(line))
.map((line) => line.trim()) .map((line) => line.trim());
.filter((line) => line.length > 0) const cleanedLines = [];
.filter((line) => !/^(?:-\s*)?(?:action|clarify|open|limit|note):\s*$/i.test(line)) let previousWasBlank = false;
.filter((line) => !hasUserFacingLeakage(line)) for (const line of preparedLines) {
.filter((line) => !looksLikeMojibake(line)); 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(); const cleaned = cleanedLines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
return cleaned || "Available data requires clarification for a reliable user-facing answer."; return cleaned || "Available data requires clarification for a reliable user-facing answer.";
} }

View File

@ -3714,7 +3714,10 @@ const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([
"supplier_payouts_profile", "supplier_payouts_profile",
"list_open_contracts", "list_open_contracts",
"open_items_by_counterparty_or_contract", "open_items_by_counterparty_or_contract",
"list_payables_counterparties",
"list_receivables_counterparties",
"payables_confirmed_as_of_date", "payables_confirmed_as_of_date",
"receivables_confirmed_as_of_date",
"list_documents_by_contract", "list_documents_by_contract",
"bank_operations_by_contract", "bank_operations_by_contract",
"list_documents_by_counterparty", "list_documents_by_counterparty",

View File

@ -139,6 +139,10 @@ export const FEATURE_ASSISTANT_ROUTE_PAYABLES_CONFIRMED_V1 = toBooleanFlag(
process.env.FEATURE_ASSISTANT_ROUTE_PAYABLES_CONFIRMED_V1, process.env.FEATURE_ASSISTANT_ROUTE_PAYABLES_CONFIRMED_V1,
true true
); );
export const FEATURE_ASSISTANT_ROUTE_RECEIVABLES_CONFIRMED_V1 = toBooleanFlag(
process.env.FEATURE_ASSISTANT_ROUTE_RECEIVABLES_CONFIRMED_V1,
true
);
export const FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1 = toBooleanFlag( export const FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1 = toBooleanFlag(
process.env.FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1, process.env.FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1,
true true

View File

@ -5,6 +5,7 @@ import {
FEATURE_ASSISTANT_ROUTE_DRILLDOWN_V1, FEATURE_ASSISTANT_ROUTE_DRILLDOWN_V1,
FEATURE_ASSISTANT_ROUTE_PAYABLES_CONFIRMED_V1, FEATURE_ASSISTANT_ROUTE_PAYABLES_CONFIRMED_V1,
FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1, FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1,
FEATURE_ASSISTANT_ROUTE_RECEIVABLES_CONFIRMED_V1,
FEATURE_ASSISTANT_ROUTE_RECEIVABLES_HEURISTIC_V1, FEATURE_ASSISTANT_ROUTE_RECEIVABLES_HEURISTIC_V1,
FEATURE_ASSISTANT_ROUTE_SHADOW_PAYABLES_EXACT_V1 FEATURE_ASSISTANT_ROUTE_SHADOW_PAYABLES_EXACT_V1
} from "../config"; } from "../config";
@ -22,7 +23,12 @@ export interface AddressCapabilityRouteDecision {
capability_route_reason: string; capability_route_reason: string;
} }
const COMPUTE_EXACT_INTENTS = new Set<AddressIntent>(["account_balance_snapshot", "documents_forming_balance", "payables_confirmed_as_of_date"]); const COMPUTE_EXACT_INTENTS = new Set<AddressIntent>([
"account_balance_snapshot",
"documents_forming_balance",
"payables_confirmed_as_of_date",
"receivables_confirmed_as_of_date"
]);
const NAVIGATION_INTENTS = new Set<AddressIntent>([ const NAVIGATION_INTENTS = new Set<AddressIntent>([
"list_documents_by_counterparty", "list_documents_by_counterparty",
"bank_operations_by_counterparty", "bank_operations_by_counterparty",
@ -53,6 +59,9 @@ function defaultCapabilityId(intent: AddressIntent): string {
if (intent === "payables_confirmed_as_of_date") { if (intent === "payables_confirmed_as_of_date") {
return "confirmed_payables_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") { if (intent === "list_payables_counterparties") {
return "payables_candidates_list"; return "payables_candidates_list";
} }
@ -81,6 +90,14 @@ function resolveCapabilityEnabled(intent: AddressIntent): { enabled: boolean; re
reason: FEATURE_ASSISTANT_ROUTE_PAYABLES_CONFIRMED_V1 ? "payables_confirmed_route_enabled" : "payables_confirmed_route_disabled_by_flag" reason: 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: FEATURE_ASSISTANT_ROUTE_RECEIVABLES_CONFIRMED_V1,
reason: FEATURE_ASSISTANT_ROUTE_RECEIVABLES_CONFIRMED_V1
? "receivables_confirmed_route_enabled"
: "receivables_confirmed_route_disabled_by_flag"
};
}
if (intent === "list_payables_counterparties") { if (intent === "list_payables_counterparties") {
return { return {
enabled: FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1, enabled: FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1,

View File

@ -840,6 +840,9 @@ function requiredFiltersByIntent(intent: AddressIntent): Array<keyof AddressFilt
if (intent === "payables_confirmed_as_of_date") { if (intent === "payables_confirmed_as_of_date") {
return ["as_of_date"]; return ["as_of_date"];
} }
if (intent === "receivables_confirmed_as_of_date") {
return ["as_of_date"];
}
if ( if (
intent === "list_documents_by_counterparty" || intent === "list_documents_by_counterparty" ||
intent === "bank_operations_by_counterparty" || intent === "bank_operations_by_counterparty" ||
@ -857,7 +860,8 @@ function usesAsOfPrimaryWindow(intent: AddressIntent): boolean {
return ( return (
intent === "open_items_by_counterparty_or_contract" || intent === "open_items_by_counterparty_or_contract" ||
intent === "list_open_contracts" || intent === "list_open_contracts" ||
intent === "payables_confirmed_as_of_date" intent === "payables_confirmed_as_of_date" ||
intent === "receivables_confirmed_as_of_date"
); );
} }
@ -1045,7 +1049,8 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
if ( if (
(intent === "account_balance_snapshot" || (intent === "account_balance_snapshot" ||
intent === "documents_forming_balance" || 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 !filters.as_of_date
) { ) {
if (filters.period_to) { if (filters.period_to) {

View File

@ -2,7 +2,11 @@
const RECEIVABLES_STRONG = [ const RECEIVABLES_STRONG = [
"кто должен нам", "кто должен нам",
"кто нам должен",
"кто нам должэн",
"нам должны", "нам должны",
"нам должен",
"нам должэны",
"who owes us", "who owes us",
"receivable", "receivable",
"receivables", "receivables",
@ -14,7 +18,11 @@ const RECEIVABLES_STRONG = [
const PAYABLES_STRONG = [ const PAYABLES_STRONG = [
"кому должны мы", "кому должны мы",
"кому должэны мы",
"кому мы должны",
"кому мы должэны",
"мы должны", "мы должны",
"мы должэны",
"who we owe", "who we owe",
"payable", "payable",
"payables", "payables",
@ -1012,6 +1020,22 @@ function hasPayablesDebtLifecycleSignal(text: string): boolean {
return true; return true;
} }
function hasReceivablesDebtLifecycleSignal(text: string): boolean {
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: string): boolean { function hasReceivablesLatencyRiskSignal(text: string): boolean {
const hasBuyer = /(?:покупател|клиент|заказчик|customer|buyer)/iu.test(text); const hasBuyer = /(?:покупател|клиент|заказчик|customer|buyer)/iu.test(text);
const hasCounterparty = /(?:контрагент|counterparty|partner)/iu.test(text); const hasCounterparty = /(?:контрагент|counterparty|partner)/iu.test(text);
@ -1452,10 +1476,15 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
} }
if (hasAny(text, RECEIVABLES_STRONG)) { if (hasAny(text, RECEIVABLES_STRONG)) {
const receivablesDebtLifecycleSignal = hasReceivablesDebtLifecycleSignal(text);
const reasons = ["receivables_signal_detected"];
if (receivablesDebtLifecycleSignal) {
reasons.push("receivables_debt_lifecycle_signal_detected");
}
return { return {
intent: "list_receivables_counterparties", intent: receivablesDebtLifecycleSignal ? "receivables_confirmed_as_of_date" : "list_receivables_counterparties",
confidence: "high", confidence: "high",
reasons: ["receivables_signal_detected"] reasons
}; };
} }

View File

@ -22,6 +22,7 @@ const DISPLAY_ENTITY_TYPE_BY_INTENT: Partial<Record<AddressIntent, AddressFocusO
customer_revenue_and_payments: "counterparty", customer_revenue_and_payments: "counterparty",
supplier_payouts_profile: "counterparty", supplier_payouts_profile: "counterparty",
list_payables_counterparties: "counterparty", list_payables_counterparties: "counterparty",
receivables_confirmed_as_of_date: "counterparty",
list_receivables_counterparties: "counterparty", list_receivables_counterparties: "counterparty",
list_contracts_by_counterparty: "contract", list_contracts_by_counterparty: "contract",
list_documents_by_counterparty: "document_ref", list_documents_by_counterparty: "document_ref",
@ -37,6 +38,7 @@ const RESULT_SET_TYPE_BY_INTENT: Partial<Record<AddressIntent, AddressResultSetT
supplier_payouts_profile: "counterparty_list", supplier_payouts_profile: "counterparty_list",
list_payables_counterparties: "counterparty_list", list_payables_counterparties: "counterparty_list",
payables_confirmed_as_of_date: "balance_snapshot", payables_confirmed_as_of_date: "balance_snapshot",
receivables_confirmed_as_of_date: "balance_snapshot",
list_receivables_counterparties: "counterparty_list", list_receivables_counterparties: "counterparty_list",
list_contracts_by_counterparty: "contract_list", list_contracts_by_counterparty: "contract_list",
list_documents_by_counterparty: "document_list", list_documents_by_counterparty: "document_list",

View File

@ -454,6 +454,7 @@ function shouldAttemptCounterpartyCatalogResolution(intent: AddressIntent, filte
intent === "open_items_by_counterparty_or_contract" || intent === "open_items_by_counterparty_or_contract" ||
intent === "list_payables_counterparties" || intent === "list_payables_counterparties" ||
intent === "payables_confirmed_as_of_date" || intent === "payables_confirmed_as_of_date" ||
intent === "receivables_confirmed_as_of_date" ||
intent === "list_receivables_counterparties" intent === "list_receivables_counterparties"
); );
} }
@ -781,6 +782,7 @@ function isCounterpartyRiskIntent(intent: AddressIntent): boolean {
intent === "list_receivables_counterparties" || intent === "list_receivables_counterparties" ||
intent === "list_payables_counterparties" || intent === "list_payables_counterparties" ||
intent === "payables_confirmed_as_of_date" || intent === "payables_confirmed_as_of_date" ||
intent === "receivables_confirmed_as_of_date" ||
intent === "list_open_contracts" || intent === "list_open_contracts" ||
intent === "open_items_by_counterparty_or_contract" intent === "open_items_by_counterparty_or_contract"
); );
@ -799,7 +801,8 @@ function isConfirmedBalanceIntent(intent: AddressIntent): boolean {
return ( return (
intent === "account_balance_snapshot" || intent === "account_balance_snapshot" ||
intent === "documents_forming_balance" || intent === "documents_forming_balance" ||
intent === "payables_confirmed_as_of_date" intent === "payables_confirmed_as_of_date" ||
intent === "receivables_confirmed_as_of_date"
); );
} }
@ -1631,6 +1634,8 @@ function buildLimitedOffers(input: {
if (input.intent === "list_receivables_counterparties") { if (input.intent === "list_receivables_counterparties") {
offers.push("показать контрагентов с максимальными хвостами дебиторки по 62/76"); 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") { } else if (input.intent === "payables_confirmed_as_of_date") {
offers.push("показать подтвержденный реестр открытых обязательств на дату среза по 60/76"); offers.push("показать подтвержденный реестр открытых обязательств на дату среза по 60/76");
} else if (input.intent === "list_payables_counterparties") { } else if (input.intent === "list_payables_counterparties") {
@ -1683,6 +1688,7 @@ function buildLimitedIntentSignalLine(input: {
list_open_contracts: "Сигнал запроса: нужен список незакрытых договоров на дату.", list_open_contracts: "Сигнал запроса: нужен список незакрытых договоров на дату.",
list_receivables_counterparties: "Сигнал запроса: нужен ранжированный список должников.", list_receivables_counterparties: "Сигнал запроса: нужен ранжированный список должников.",
list_payables_counterparties: "Сигнал запроса: нужен ранжированный список кредиторов.", list_payables_counterparties: "Сигнал запроса: нужен ранжированный список кредиторов.",
receivables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез дебиторской задолженности на дату.",
payables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез обязательств к оплате на дату." payables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез обязательств к оплате на дату."
}; };
@ -1882,9 +1888,18 @@ function buildLimitedExecutionResult(input: {
resultSemantics.result_mode resultSemantics.result_mode
); );
const reasons = const reasons =
input.intent.intent === "payables_confirmed_as_of_date" && (input.intent.intent === "payables_confirmed_as_of_date" || input.intent.intent === "receivables_confirmed_as_of_date") &&
!reasonsWithConfirmedFallback.includes("exact_payables_mode_limited_response") !reasonsWithConfirmedFallback.includes(
? [...reasonsWithConfirmedFallback, "exact_payables_mode_limited_response"] 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; : reasonsWithConfirmedFallback;
const routeExpectationAudit = const routeExpectationAudit =
input.routeExpectationAudit ?? input.routeExpectationAudit ??
@ -1994,12 +2009,20 @@ export class AddressQueryService {
} }
} }
const requestedResultMode = resolveRequestedResultMode(intent.intent, filters.extracted_filters); const requestedResultMode = resolveRequestedResultMode(intent.intent, filters.extracted_filters);
const payablesConfirmedExecution = const confirmedBalancePayablesIntent =
(intent.intent === "list_payables_counterparties" || intent.intent === "payables_confirmed_as_of_date") && (intent.intent === "list_payables_counterparties" || intent.intent === "payables_confirmed_as_of_date") &&
requestedResultMode === "confirmed_balance" requestedResultMode === "confirmed_balance";
const confirmedBalanceReceivablesIntent =
intent.intent === "receivables_confirmed_as_of_date" && requestedResultMode === "confirmed_balance";
const payablesConfirmedExecution =
confirmedBalancePayablesIntent
? resolveExecutionFiltersForPayablesConfirmedBalance(filters.extracted_filters, analysisDate) ? resolveExecutionFiltersForPayablesConfirmedBalance(filters.extracted_filters, analysisDate)
: null; : 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 ( if (
payablesConfirmedExecution?.asOfDerived && payablesConfirmedExecution?.asOfDerived &&
!(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0) !(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)
@ -2011,6 +2034,17 @@ export class AddressQueryService {
baseReasons.push("as_of_date_derived_for_confirmed_payables"); 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 = resolveAddressCapabilityRouteDecision(intent.intent); const capabilityDecision = resolveAddressCapabilityRouteDecision(intent.intent);
const capabilityAudit = buildCapabilityAudit(intent.intent); const capabilityAudit = buildCapabilityAudit(intent.intent);
const shadowRouteAudit = buildShadowRouteAudit({ const shadowRouteAudit = buildShadowRouteAudit({
@ -2080,6 +2114,12 @@ export class AddressQueryService {
if (intent.intent === "payables_confirmed_as_of_date" && !baseReasons.includes("confirmed_balance_exact_payables_intent")) { if (intent.intent === "payables_confirmed_as_of_date" && !baseReasons.includes("confirmed_balance_exact_payables_intent")) {
baseReasons.push("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 ( if (
requestedResultMode === "confirmed_balance" && requestedResultMode === "confirmed_balance" &&
recipeIntent === "open_items_by_counterparty_or_contract" && recipeIntent === "open_items_by_counterparty_or_contract" &&
@ -2229,7 +2269,8 @@ export class AddressQueryService {
const missingSubcontoFallbackEligible = const missingSubcontoFallbackEligible =
plan.recipe.recipe_id === "address_movements_receivables_v1" || plan.recipe.recipe_id === "address_movements_receivables_v1" ||
plan.recipe.recipe_id === "address_movements_payables_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( const missingSubcontoErrorDetected = Boolean(
mcp.error && missingSubcontoFallbackEligible && isMissingSubcontoFieldError(mcp.error) mcp.error && missingSubcontoFallbackEligible && isMissingSubcontoFieldError(mcp.error)
); );
@ -2255,6 +2296,10 @@ export class AddressQueryService {
if (!baseReasons.includes("confirmed_payables_exact_mode_missing_subconto_axis_fallback_to_composite_subconto")) { if (!baseReasons.includes("confirmed_payables_exact_mode_missing_subconto_axis_fallback_to_composite_subconto")) {
baseReasons.push("confirmed_payables_exact_mode_missing_subconto_axis_fallback_to_composite_subconto"); 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")) { } 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"); baseReasons.push("mcp_missing_subconto_axis_auto_fallback_to_composite_subconto_failed");
@ -2290,6 +2335,12 @@ export class AddressQueryService {
if (!baseReasons.includes("confirmed_payables_exact_mode_missing_subconto_fallback_to_open_items")) { if (!baseReasons.includes("confirmed_payables_exact_mode_missing_subconto_fallback_to_open_items")) {
baseReasons.push("confirmed_payables_exact_mode_missing_subconto_fallback_to_open_items"); 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 { } else {
if (!baseReasons.includes("mcp_missing_subconto_field_auto_fallback_failed")) { if (!baseReasons.includes("mcp_missing_subconto_field_auto_fallback_failed")) {
@ -2307,9 +2358,9 @@ export class AddressQueryService {
mcp.error && mcp.error &&
missingSubcontoFallbackEligible && missingSubcontoFallbackEligible &&
isMissingSubcontoFieldError(mcp.error) && isMissingSubcontoFieldError(mcp.error) &&
!baseReasons.includes("confirmed_payables_exact_mode_missing_subconto_no_heuristic_fallback") !baseReasons.includes("confirmed_exact_mode_missing_subconto_no_heuristic_fallback")
) { ) {
baseReasons.push("confirmed_payables_exact_mode_missing_subconto_no_heuristic_fallback"); baseReasons.push("confirmed_exact_mode_missing_subconto_no_heuristic_fallback");
} }
if (mcp.error) { if (mcp.error) {
@ -3176,7 +3227,12 @@ export class AddressQueryService {
routeExpectationAudit: finalRouteExpectationAudit 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({ return buildLimitedExecutionResult({
mode, mode,
shape, shape,
@ -3200,10 +3256,10 @@ export class AddressQueryService {
rawRowKeysSample: rowDiagnostics.rawRowKeysSample, rawRowKeysSample: rowDiagnostics.rawRowKeysSample,
materializationDropReason: rowDiagnostics.materializationDropReason, materializationDropReason: rowDiagnostics.materializationDropReason,
category: "recipe_visibility_gap", 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", nextStep: "specify as_of_date/counterparty or enable detailed settlement registers for exact confirmed balance",
limitations: ["exact_payables_mode_unconfirmed_output_blocked"], limitations: [`exact_${exactModeName}_mode_unconfirmed_output_blocked`],
reasons: [...baseReasons, "exact_payables_mode_unconfirmed_output_blocked"], reasons: [...baseReasons, `exact_${exactModeName}_mode_unconfirmed_output_blocked`],
capabilityAudit, capabilityAudit,
shadowRouteAudit, shadowRouteAudit,
routeExpectationAudit: finalRouteExpectationAudit routeExpectationAudit: finalRouteExpectationAudit

View File

@ -49,6 +49,29 @@ const PAYABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
Сумма __ORDER_DIRECTION__ Сумма __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 = ` const BANK_DOCS_QUERY_TEMPLATE = `
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__ ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
БанкСписание.Дата КАК Период, БанкСписание.Дата КАК Период,
@ -574,6 +597,17 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [
account_scope_mode: "strict", account_scope_mode: "strict",
query_template: "payables_confirmed_as_of_balance_profile" 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", recipe_id: "address_movements_receivables_v1",
intent: "list_receivables_counterparties", intent: "list_receivables_counterparties",
@ -1044,6 +1078,25 @@ export function buildAddressRecipePlan(
.replaceAll("__PAYABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"])) .replaceAll("__PAYABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"]))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort)); .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 : MOVEMENTS_QUERY_TEMPLATE
.replace("__LIMIT__", String(resolvedLimit)) .replace("__LIMIT__", String(resolvedLimit))
.replace("__WHERE_CLAUSE__", (() => { .replace("__WHERE_CLAUSE__", (() => {

View File

@ -176,10 +176,7 @@ function formatMoneyRub(value: number): string {
} }
function emphasizeNumericTokens(line: string): string { function emphasizeNumericTokens(line: string): string {
if (!line) { return line;
return line;
}
return line.replace(/(?<!\*)\d(?:[\d.,:/-]*\d)?(?!\*)/g, (token) => `**${token}**`);
} }
function parseIsoDateToken(value: string | null | undefined): { year: number; month: number; day: number } | null { function parseIsoDateToken(value: string | null | undefined): { year: number; month: number; day: number } | null {
@ -700,6 +697,19 @@ function liabilityCategoryLabel(category: PayablesLiabilityCategory): string {
return "прочие"; return "прочие";
} }
function receivablesCategoryLabel(category: PayablesLiabilityCategory): string {
if (category === "supplier_or_contractor") {
return "покупатели/заказчики";
}
if (category === "bank_or_credit") {
return "банки/финансовые";
}
if (category === "tax_or_state") {
return "бюджет/госорганы";
}
return "прочие";
}
function classifyPayablesLiabilityCategory(row: ComposeStageRow, counterparty: string): { function classifyPayablesLiabilityCategory(row: ComposeStageRow, counterparty: string): {
scores: Record<PayablesLiabilityCategory, number>; scores: Record<PayablesLiabilityCategory, number>;
reasons: string[]; reasons: string[];
@ -784,6 +794,11 @@ function hasPayablesSectionPrefix(account: string | null): boolean {
return section === "60" || section === "76"; return section === "60" || section === "76";
} }
function hasReceivablesSectionPrefix(account: string | null): boolean {
const section = extractAccountSectionCode(account);
return section === "62" || section === "76";
}
function resolvePayablesAsOfDate(options: ComposeFactualReplyOptions): string { function resolvePayablesAsOfDate(options: ComposeFactualReplyOptions): string {
const explicit = normalizeIsoDateOnly(options.asOfDate); const explicit = normalizeIsoDateOnly(options.asOfDate);
if (explicit) { if (explicit) {
@ -998,6 +1013,126 @@ function buildPayablesConfirmedBalanceAggregate(
}); });
} }
function buildReceivablesConfirmedBalanceAggregate(
rows: ComposeStageRow[],
asOfDate: string
): PayablesConfirmedBalanceAggregate[] {
const byCounterparty = new Map<
string,
{
outstandingAmount: number;
operations: number;
firstPeriod: string | null;
lastPeriod: string | null;
categoryScores: Record<PayablesLiabilityCategory, number>;
reasons: Set<string>;
contracts: Set<string>;
documents: Set<string>;
sourceRefs: Set<string>;
}
>();
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: ComposeStageRow[]): CounterpartyRiskAggregate[] { function buildCounterpartyRiskAggregate(rows: ComposeStageRow[]): CounterpartyRiskAggregate[] {
const byCounterparty = new Map<string, CounterpartyRiskAggregate>(); const byCounterparty = new Map<string, CounterpartyRiskAggregate>();
@ -2462,7 +2597,7 @@ export function composeFactualReply(
`Итого подтвержденный долг на ${formatDateRu(payablesAsOfDate)}: ${formatMoneyRub(totalOutstandingAmount)}.`, `Итого подтвержденный долг на ${formatDateRu(payablesAsOfDate)}: ${formatMoneyRub(totalOutstandingAmount)}.`,
"", "",
"Блок 1. Статус результата", "Блок 1. Статус результата",
"- Режим результата: подтвержденный срез обязательств к оплате (exact route)." "- Результат: подтвержденный срез обязательств к оплате."
]; ];
lines.push(""); lines.push("");
@ -2492,11 +2627,14 @@ export function composeFactualReply(
lines.push("Блок 5. Подтвержденные позиции к оплате"); lines.push("Блок 5. Подтвержденные позиции к оплате");
if (confirmedBalances.length > 0) { if (confirmedBalances.length > 0) {
lines.push( lines.push(
...confirmedBalances.slice(0, 10).map( ...confirmedBalances.slice(0, 10).flatMap((item, index) => [
(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)}`,
`${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 { } else {
lines.push("- Подтвержденных открытых обязательств к оплате на дату среза не найдено."); lines.push("- Подтвержденных открытых обязательств к оплате на дату среза не найдено.");
} }
@ -2512,6 +2650,86 @@ export function composeFactualReply(
}; };
} }
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<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[] = [
`Итого подтвержденная дебиторская задолженность на ${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") { if (intent === "list_payables_counterparties") {
const counterparties = buildPayablesCounterpartyRiskAggregate(rows); const counterparties = buildPayablesCounterpartyRiskAggregate(rows);
const payablesAsOfDate = resolvePayablesAsOfDate(options); const payablesAsOfDate = resolvePayablesAsOfDate(options);

View File

@ -442,7 +442,8 @@ function mergeFollowupFilters(
if ( if (
intent === "open_items_by_counterparty_or_contract" || intent === "open_items_by_counterparty_or_contract" ||
intent === "list_open_contracts" || 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 inheritedContract = previousContract ?? (followupContext.previous_anchor_type === "contract" ? previousAnchorValue : null);
const currentContract = toNonEmptyString(merged.contract); const currentContract = toNonEmptyString(merged.contract);
@ -537,6 +538,7 @@ function resolveMissingRequiredFilters(intent: AddressIntent, filters: AddressFi
account_balance_snapshot: ["account", "as_of_date"], account_balance_snapshot: ["account", "as_of_date"],
documents_forming_balance: ["account", "as_of_date"], documents_forming_balance: ["account", "as_of_date"],
payables_confirmed_as_of_date: ["as_of_date"], payables_confirmed_as_of_date: ["as_of_date"],
receivables_confirmed_as_of_date: ["as_of_date"],
list_documents_by_counterparty: ["counterparty"], list_documents_by_counterparty: ["counterparty"],
bank_operations_by_counterparty: ["counterparty"], bank_operations_by_counterparty: ["counterparty"],
list_contracts_by_counterparty: ["counterparty"], list_contracts_by_counterparty: ["counterparty"],

View File

@ -192,7 +192,8 @@ function inferAggregationProfile(intent: AddressIntent, shape: AddressQueryShape
if ( if (
intent === "account_balance_snapshot" || intent === "account_balance_snapshot" ||
intent === "documents_forming_balance" || 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"; return "balance_snapshot";
} }

View File

@ -665,7 +665,7 @@ function scrubRawTechnicalRefs(value: string): string {
.replace(RAW_REF_TOKEN_PATTERN, "reference") .replace(RAW_REF_TOKEN_PATTERN, "reference")
.replace(/\(\s*\[id\]\s*\)/g, "") .replace(/\(\s*\[id\]\s*\)/g, "")
.replace(/\[\s*id\s*\](?:\s*,\s*\[\s*id\s*\])+/g, "[id]") .replace(/\[\s*id\s*\](?:\s*,\s*\[\s*id\s*\])+/g, "[id]")
.replace(/\s{2,}/g, " ") .replace(/[ \t]{2,}/g, " ")
.trim(); .trim();
} }
@ -681,23 +681,50 @@ function stripSyntheticPlaceholders(value: string): string {
function sanitizeUserFacingReply(value: string): string { function sanitizeUserFacingReply(value: string): string {
const raw = String(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 preCut = hardCutMatch ? raw.slice(0, hardCutMatch.index) : raw;
const withoutDebugBlocks = preCut const withoutDebugBlocks = preCut
.replace(/###\s*debug_payload_json[\s\S]*?(?:```[\s\S]*?```|$)/gi, "") .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(/###\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, ""); .replace(/```json[\s\S]*?```/gi, "");
const normalized = scrubRawTechnicalRefs(withoutDebugBlocks).replace(/[ \t]+\n/g, "\n"); const normalized = scrubRawTechnicalRefs(withoutDebugBlocks).replace(/[ \t]+\n/g, "\n");
const cleanedLines = normalized const preparedLines = normalized
.split(/\r?\n/g) .split(/\r?\n/g)
.map((line) => stripSyntheticPlaceholders(line)) .map((line) => stripSyntheticPlaceholders(line))
.map((line) => stripMojibakeFragments(line)) .map((line) => stripMojibakeFragments(line))
.map((line) => line.trim()) .map((line) => line.trim());
.filter((line) => line.length > 0) const cleanedLines: string[] = [];
.filter((line) => !/^(?:-\s*)?(?:action|clarify|open|limit|note):\s*$/i.test(line)) let previousWasBlank = false;
.filter((line) => !hasUserFacingLeakage(line)) for (const line of preparedLines) {
.filter((line) => !looksLikeMojibake(line)); 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(); const cleaned = cleanedLines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
return cleaned || "Available data requires clarification for a reliable user-facing answer."; return cleaned || "Available data requires clarification for a reliable user-facing answer.";
} }

View File

@ -3672,7 +3672,10 @@ const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([
"supplier_payouts_profile", "supplier_payouts_profile",
"list_open_contracts", "list_open_contracts",
"open_items_by_counterparty_or_contract", "open_items_by_counterparty_or_contract",
"list_payables_counterparties",
"list_receivables_counterparties",
"payables_confirmed_as_of_date", "payables_confirmed_as_of_date",
"receivables_confirmed_as_of_date",
"list_documents_by_contract", "list_documents_by_contract",
"bank_operations_by_contract", "bank_operations_by_contract",
"list_documents_by_counterparty", "list_documents_by_counterparty",

View File

@ -14,6 +14,7 @@ export type AddressIntent =
| "list_open_contracts" | "list_open_contracts"
| "list_payables_counterparties" | "list_payables_counterparties"
| "payables_confirmed_as_of_date" | "payables_confirmed_as_of_date"
| "receivables_confirmed_as_of_date"
| "list_receivables_counterparties" | "list_receivables_counterparties"
| "account_balance_snapshot" | "account_balance_snapshot"
| "open_items_by_counterparty_or_contract" | "open_items_by_counterparty_or_contract"
@ -130,7 +131,8 @@ export interface AddressRecipeDefinition {
| "contract_value_profile" | "contract_value_profile"
| "contracts_by_counterparty_profile" | "contracts_by_counterparty_profile"
| "vat_payable_forecast_profile" | "vat_payable_forecast_profile"
| "payables_confirmed_as_of_balance_profile"; | "payables_confirmed_as_of_balance_profile"
| "receivables_confirmed_as_of_balance_profile";
required_filters: Array<keyof AddressFilterSet>; required_filters: Array<keyof AddressFilterSet>;
optional_filters: Array<keyof AddressFilterSet>; optional_filters: Array<keyof AddressFilterSet>;
default_limit: number; default_limit: number;

View File

@ -15,6 +15,15 @@ describe("address capability policy", () => {
expect(isCapabilityRouteBlocked(decision)).toBe(false); expect(isCapabilityRouteBlocked(decision)).toBe(false);
}); });
it("maps confirmed receivables intent to compute exact capability", () => {
const decision = resolveAddressCapabilityRouteDecision("receivables_confirmed_as_of_date");
expect(decision.capability_id).toBe("confirmed_receivables_as_of_date");
expect(decision.capability_layer).toBe("compute");
expect(decision.capability_route_mode).toBe("exact");
expect(decision.capability_route_enabled).toBe(true);
expect(isCapabilityRouteBlocked(decision)).toBe(false);
});
it("maps document drilldown intent to navigation capability", () => { it("maps document drilldown intent to navigation capability", () => {
const decision = resolveAddressCapabilityRouteDecision("list_documents_by_contract"); const decision = resolveAddressCapabilityRouteDecision("list_documents_by_contract");
expect(decision.capability_id).toBe("documents_drilldown"); expect(decision.capability_id).toBe("documents_drilldown");

View File

@ -0,0 +1,53 @@
import { describe, expect, it } from "vitest";
import { resolveAddressIntent } from "../src/services/addressIntentResolver";
import { extractAddressFilters } from "../src/services/addressFilterExtractor";
import { buildAddressRecipePlan, selectAddressRecipe } from "../src/services/addressRecipeCatalog";
import { resolveAddressCapabilityRouteDecision } from "../src/services/addressCapabilityPolicy";
import { evaluateAddressRouteExpectation } from "../src/services/addressRouteExpectations";
import { AddressQueryService } from "../src/services/addressQueryService";
describe("receivables confirmed as-of route", () => {
it("routes 'кто нам должен' wording into exact receivables intent", () => {
const result = resolveAddressIntent("кто нам должен на июль 2020");
expect(result.intent).toBe("receivables_confirmed_as_of_date");
expect(result.reasons).toContain("receivables_debt_lifecycle_signal_detected");
});
it("selects confirmed receivables recipe and builds balance query", () => {
const filters = extractAddressFilters("кто нам должен на июль 2020", "receivables_confirmed_as_of_date").extracted_filters;
const selected = selectAddressRecipe("receivables_confirmed_as_of_date", filters);
expect(selected.selected_recipe?.recipe_id).toBe("address_receivables_confirmed_as_of_date_v1");
const plan = buildAddressRecipePlan(selected.selected_recipe!, filters);
expect(plan.query).toContain("РегистрБухгалтерии.Хозрасчетный.Остатки");
expect(plan.query).toContain("СуммаРазвернутыйОстатокДт");
expect(plan.query).toContain("Остатки.Счет");
expect(plan.query).toContain("62");
expect(plan.query).toContain("76");
});
it("exposes compute exact capability and route expectation for exact receivables", () => {
const capability = resolveAddressCapabilityRouteDecision("receivables_confirmed_as_of_date");
expect(capability.capability_id).toBe("confirmed_receivables_as_of_date");
expect(capability.capability_layer).toBe("compute");
expect(capability.capability_route_mode).toBe("exact");
const expectation = evaluateAddressRouteExpectation({
intent: "receivables_confirmed_as_of_date",
selectedRecipe: "address_receivables_confirmed_as_of_date_v1",
requestedResultMode: "confirmed_balance",
resultMode: "confirmed_balance"
});
expect(expectation.status).toBe("matched");
});
it("uses exact receivables route in runtime for monthly as-of query", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("кто нам должен на июль 2020");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("receivables_confirmed_as_of_date");
expect(result?.debug.selected_recipe).toBe("address_receivables_confirmed_as_of_date_v1");
expect(result?.debug.requested_result_mode).toBe("confirmed_balance");
expect(result?.debug.route_expectation_status).toBe("matched");
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
});
});

View File

@ -23,6 +23,17 @@ describe("address route expectations contract", () => {
expect(audit.reason).toBe("route_expectation_matched"); expect(audit.reason).toBe("route_expectation_matched");
}); });
it("matches expected recipe and result mode for exact receivables route", () => {
const audit = evaluateAddressRouteExpectation({
intent: "receivables_confirmed_as_of_date",
selectedRecipe: "address_receivables_confirmed_as_of_date_v1",
requestedResultMode: "confirmed_balance",
resultMode: "confirmed_balance"
});
expect(audit.status).toBe("matched");
expect(audit.reason).toBe("route_expectation_matched");
});
it("detects selected recipe mismatch", () => { it("detects selected recipe mismatch", () => {
const audit = evaluateAddressRouteExpectation({ const audit = evaluateAddressRouteExpectation({
intent: "payables_confirmed_as_of_date", intent: "payables_confirmed_as_of_date",

View File

@ -0,0 +1,48 @@
import { describe, expect, it } from "vitest";
import { sanitizeAssistantReplyForUserFacing } from "../src/services/answerComposer";
describe("answer composer user-facing formatting", () => {
it("keeps intentional blank lines between semantic blocks", () => {
const source = [
"Блок 1. Статус результата",
"- Дата среза: 31.07.2020.",
"",
"Блок 2. Что учтено",
"- Контур: дебиторка 62/76.",
"",
"Блок 3. Сводка",
"- Строк: 24."
].join("\n");
const sanitized = sanitizeAssistantReplyForUserFacing(source);
expect(sanitized).toContain("Блок 1. Статус результата");
expect(sanitized).toContain("\n\nБлок 2. Что учтено\n");
expect(sanitized).toContain("\n\nБлок 3. Сводка\n");
});
it("removes debug payload and still preserves block separators", () => {
const source = [
"Итого подтвержденная дебиторка: 1.000,00 ₽.",
"",
"Блок 1. Статус результата",
"- Exact route.",
"",
"Блок 2. Что учтено",
"- Дата среза: 31.07.2020.",
"",
"### technical_debug_payload_json",
"```json",
"{\"trace_id\":\"t-1\"}",
"```"
].join("\n");
const sanitized = sanitizeAssistantReplyForUserFacing(source);
expect(sanitized).toContain("Блок 1. Статус результата");
expect(sanitized).toContain("\n\nБлок 2. Что учтено\n");
expect(sanitized).not.toContain("technical_debug_payload_json");
expect(sanitized).not.toContain("trace_id");
});
});