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

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_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",
"capability_id": "payables_candidates_list",

View File

@ -8,6 +8,12 @@
"expected_requested_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",
"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 };
};
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.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.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 = exports.PRESETS_DIR = void 0;
const path_1 = __importDefault(require("path"));
exports.BACKEND_ROOT = path_1.default.resolve(__dirname, "..");
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_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_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_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);

View File

@ -4,7 +4,12 @@ exports.resolveAddressCapabilityRouteDecision = resolveAddressCapabilityRouteDec
exports.isCapabilityRouteBlocked = isCapabilityRouteBlocked;
exports.resolveShadowRouteIntent = resolveShadowRouteIntent;
const config_1 = require("../config");
const COMPUTE_EXACT_INTENTS = new Set(["account_balance_snapshot", "documents_forming_balance", "payables_confirmed_as_of_date"]);
const COMPUTE_EXACT_INTENTS = new Set([
"account_balance_snapshot",
"documents_forming_balance",
"payables_confirmed_as_of_date",
"receivables_confirmed_as_of_date"
]);
const NAVIGATION_INTENTS = new Set([
"list_documents_by_counterparty",
"bank_operations_by_counterparty",
@ -31,6 +36,9 @@ function defaultCapabilityId(intent) {
if (intent === "payables_confirmed_as_of_date") {
return "confirmed_payables_as_of_date";
}
if (intent === "receivables_confirmed_as_of_date") {
return "confirmed_receivables_as_of_date";
}
if (intent === "list_payables_counterparties") {
return "payables_candidates_list";
}
@ -58,6 +66,14 @@ function resolveCapabilityEnabled(intent) {
reason: config_1.FEATURE_ASSISTANT_ROUTE_PAYABLES_CONFIRMED_V1 ? "payables_confirmed_route_enabled" : "payables_confirmed_route_disabled_by_flag"
};
}
if (intent === "receivables_confirmed_as_of_date") {
return {
enabled: config_1.FEATURE_ASSISTANT_ROUTE_RECEIVABLES_CONFIRMED_V1,
reason: config_1.FEATURE_ASSISTANT_ROUTE_RECEIVABLES_CONFIRMED_V1
? "receivables_confirmed_route_enabled"
: "receivables_confirmed_route_disabled_by_flag"
};
}
if (intent === "list_payables_counterparties") {
return {
enabled: config_1.FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1,

View File

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

View File

@ -3,7 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveAddressIntent = resolveAddressIntent;
const RECEIVABLES_STRONG = [
"кто должен нам",
"кто нам должен",
"кто нам должэн",
"нам должны",
"нам должен",
"нам должэны",
"who owes us",
"receivable",
"receivables",
@ -14,7 +18,11 @@ const RECEIVABLES_STRONG = [
];
const PAYABLES_STRONG = [
"кому должны мы",
"кому должэны мы",
"кому мы должны",
"кому мы должэны",
"мы должны",
"мы должэны",
"who we owe",
"payable",
"payables",
@ -865,6 +873,18 @@ function hasPayablesDebtLifecycleSignal(text) {
}
return true;
}
function hasReceivablesDebtLifecycleSignal(text) {
const hasOweUsSignal = /(?:кто\s+нам\s+долж(?:ен|ны)?|кто\s+долж(?:ен|ны)?\s+нам|нам\s+долж(?:ен|ны)|должник(?:и|ов|а)?|дебитор(?:ы|ов|ск)?|задолж|долг(?:и|ов|а|у)?|к\s+получению|на\s+поступление|к\s+взысканию|who\s+owes\s+us|receivables?)/iu.test(text);
if (!hasOweUsSignal) {
return false;
}
const hasPastInflowSignal = /(?:прин[её]с|зан[её]с|поступил|приход|inflow|paid\s+us|already\s+paid)/iu.test(text);
const hasTopRankingSignal = /(?:топ|top|больше\s+всего|сам(?:ый|ая|ое|ые)|наибольш|максимальн)/iu.test(text);
if (hasPastInflowSignal && hasTopRankingSignal) {
return false;
}
return true;
}
function hasReceivablesLatencyRiskSignal(text) {
const hasBuyer = /(?:покупател|клиент|заказчик|customer|buyer)/iu.test(text);
const hasCounterparty = /(?:контрагент|counterparty|partner)/iu.test(text);
@ -1239,10 +1259,15 @@ function resolveAddressIntent(userMessage) {
};
}
if (hasAny(text, RECEIVABLES_STRONG)) {
const receivablesDebtLifecycleSignal = hasReceivablesDebtLifecycleSignal(text);
const reasons = ["receivables_signal_detected"];
if (receivablesDebtLifecycleSignal) {
reasons.push("receivables_debt_lifecycle_signal_detected");
}
return {
intent: "list_receivables_counterparties",
intent: receivablesDebtLifecycleSignal ? "receivables_confirmed_as_of_date" : "list_receivables_counterparties",
confidence: "high",
reasons: ["receivables_signal_detected"]
reasons
};
}
if (hasAny(text, PAYABLES_STRONG)) {

View File

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

View File

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

View File

@ -44,6 +44,28 @@ const PAYABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
УПОРЯДОЧИТЬ ПО
Сумма __ORDER_DIRECTION__
`;
const RECEIVABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
__AS_OF_EXPR__ КАК Период,
"Остатки на дату" КАК Регистратор,
ПРЕДСТАВЛЕНИЕ(Остатки.Счет) КАК СчетДт,
"" КАК СчетКт,
Остатки.СуммаРазвернутыйОстатокДт КАК Сумма,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоДт1,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоДт2,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоДт3,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоКт1,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоКт2,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоКт3,
ПРЕДСТАВЛЕНИЕ(Остатки.Организация) КАК Организация
ИЗ
РегистрБухгалтерии.Хозрасчетный.Остатки(__AS_OF_EXPR__, , , ) КАК Остатки
ГДЕ
Остатки.СуммаРазвернутыйОстатокДт > 0
И (__RECEIVABLE_ACCOUNTS_MATCH__)
УПОРЯДОЧИТЬ ПО
Сумма __ORDER_DIRECTION__
`;
const BANK_DOCS_QUERY_TEMPLATE = `
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
БанкСписание.Дата КАК Период,
@ -558,6 +580,17 @@ const BASE_RECIPES = [
account_scope_mode: "strict",
query_template: "payables_confirmed_as_of_balance_profile"
},
{
recipe_id: "address_receivables_confirmed_as_of_date_v1",
intent: "receivables_confirmed_as_of_date",
purpose: "Build confirmed receivables snapshot as-of date from movements on accounts 62/76",
required_filters: ["as_of_date"],
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
default_limit: 200,
account_scope: ["62", "76"],
account_scope_mode: "strict",
query_template: "receivables_confirmed_as_of_balance_profile"
},
{
recipe_id: "address_movements_receivables_v1",
intent: "list_receivables_counterparties",
@ -947,17 +980,35 @@ function buildAddressRecipePlan(recipe, filters) {
.replaceAll("__PAYABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"]))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
})()
: MOVEMENTS_QUERY_TEMPLATE
.replace("__LIMIT__", String(resolvedLimit))
.replace("__WHERE_CLAUSE__", (() => {
const extraConditions = [];
const accountCondition = buildMovementAccountCondition(filters);
if (accountCondition) {
extraConditions.push(accountCondition);
}
return buildWhereClause(filters, "Движения.Период", extraConditions);
})())
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
: recipe.query_template === "receivables_confirmed_as_of_balance_profile"
? (() => {
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
? toDateTimeExpr(filters.as_of_date, true)
: null) ??
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
? toDateTimeExpr(filters.period_to, true)
: null) ??
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
? toDateTimeExpr(filters.period_from, true)
: null) ??
"ТЕКУЩАЯДАТА()";
return RECEIVABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
.replaceAll("__LIMIT__", String(resolvedLimit))
.replaceAll("__AS_OF_EXPR__", asOfExpr)
.replaceAll("__RECEIVABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["62", "76"]))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
})()
: MOVEMENTS_QUERY_TEMPLATE
.replace("__LIMIT__", String(resolvedLimit))
.replace("__WHERE_CLAUSE__", (() => {
const extraConditions = [];
const accountCondition = buildMovementAccountCondition(filters);
if (accountCondition) {
extraConditions.push(accountCondition);
}
return buildWhereClause(filters, "Движения.Период", extraConditions);
})())
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
return {
recipe,
query,

View File

@ -102,10 +102,7 @@ function formatMoneyRub(value) {
return `${formatNumberWithDots(value, 2)}`;
}
function emphasizeNumericTokens(line) {
if (!line) {
return line;
}
return line.replace(/(?<!\*)\d(?:[\d.,:/-]*\d)?(?!\*)/g, (token) => `**${token}**`);
return line;
}
function parseIsoDateToken(value) {
const source = String(value ?? "").trim();
@ -514,6 +511,18 @@ function liabilityCategoryLabel(category) {
}
return "прочие";
}
function receivablesCategoryLabel(category) {
if (category === "supplier_or_contractor") {
return "покупатели/заказчики";
}
if (category === "bank_or_credit") {
return "банки/финансовые";
}
if (category === "tax_or_state") {
return "бюджет/госорганы";
}
return "прочие";
}
function classifyPayablesLiabilityCategory(row, counterparty) {
const scores = {
supplier_or_contractor: 0,
@ -582,6 +591,10 @@ function hasPayablesSectionPrefix(account) {
const section = extractAccountSectionCode(account);
return section === "60" || section === "76";
}
function hasReceivablesSectionPrefix(account) {
const section = extractAccountSectionCode(account);
return section === "62" || section === "76";
}
function resolvePayablesAsOfDate(options) {
const explicit = normalizeIsoDateOnly(options.asOfDate);
if (explicit) {
@ -762,6 +775,105 @@ function buildPayablesConfirmedBalanceAggregate(rows, asOfDate) {
return left.name.localeCompare(right.name);
});
}
function buildReceivablesConfirmedBalanceAggregate(rows, asOfDate) {
const byCounterparty = new Map();
const asOfTimestamp = toUtcDayTimestamp(asOfDate);
for (const row of rows) {
const name = extractCounterpartyName(row);
if (!name) {
continue;
}
const rowTimestamp = toUtcDayTimestamp(row.period);
if (asOfTimestamp !== null && rowTimestamp !== null && rowTimestamp > asOfTimestamp) {
continue;
}
const amount = row.amount;
if (typeof amount !== "number" || !Number.isFinite(amount)) {
continue;
}
const absAmount = Math.abs(amount);
let delta = 0;
if (hasReceivablesSectionPrefix(row.account_dt)) {
delta += absAmount;
}
if (hasReceivablesSectionPrefix(row.account_kt)) {
delta -= absAmount;
}
if (Math.abs(delta) <= 0.0000001) {
continue;
}
const classified = classifyPayablesLiabilityCategory(row, name);
const contract = extractContractName(row);
const sourceRefs = extractPayablesSourceRefs(row, name, contract);
const current = byCounterparty.get(name);
if (!current) {
byCounterparty.set(name, {
outstandingAmount: delta,
operations: 1,
firstPeriod: row.period,
lastPeriod: row.period,
categoryScores: {
supplier_or_contractor: classified.scores.supplier_or_contractor,
bank_or_credit: classified.scores.bank_or_credit,
tax_or_state: classified.scores.tax_or_state,
other: classified.scores.other
},
reasons: new Set(classified.reasons),
contracts: new Set(contract ? [contract] : []),
documents: new Set(row.registrator ? [row.registrator] : []),
sourceRefs: new Set(sourceRefs)
});
continue;
}
current.outstandingAmount += delta;
current.operations += 1;
if ((row.period ?? "") < (current.firstPeriod ?? "")) {
current.firstPeriod = row.period;
}
if ((row.period ?? "") > (current.lastPeriod ?? "")) {
current.lastPeriod = row.period;
}
current.categoryScores.supplier_or_contractor += classified.scores.supplier_or_contractor;
current.categoryScores.bank_or_credit += classified.scores.bank_or_credit;
current.categoryScores.tax_or_state += classified.scores.tax_or_state;
current.categoryScores.other += classified.scores.other;
for (const reason of classified.reasons) {
current.reasons.add(reason);
}
if (contract) {
current.contracts.add(contract);
}
if (row.registrator) {
current.documents.add(row.registrator);
}
for (const ref of sourceRefs) {
current.sourceRefs.add(ref);
}
}
return Array.from(byCounterparty.entries())
.map(([name, item]) => ({
name,
outstandingAmount: item.outstandingAmount,
operations: item.operations,
firstPeriod: item.firstPeriod,
lastPeriod: item.lastPeriod,
category: resolvePayablesLiabilityCategory(item.categoryScores),
categoryReasons: Array.from(item.reasons).slice(0, 2),
contracts: Array.from(item.contracts).slice(0, 2),
documents: Array.from(item.documents).slice(0, 2),
sourceRefs: Array.from(item.sourceRefs).slice(0, 3)
}))
.filter((item) => item.outstandingAmount > 0.005)
.sort((left, right) => {
if (right.outstandingAmount !== left.outstandingAmount) {
return right.outstandingAmount - left.outstandingAmount;
}
if (right.operations !== left.operations) {
return right.operations - left.operations;
}
return left.name.localeCompare(right.name);
});
}
function buildCounterpartyRiskAggregate(rows) {
const byCounterparty = new Map();
for (const row of rows) {
@ -1932,7 +2044,7 @@ function composeFactualReply(intent, rows, options = {}) {
`Итого подтвержденный долг на ${formatDateRu(payablesAsOfDate)}: ${formatMoneyRub(totalOutstandingAmount)}.`,
"",
"Блок 1. Статус результата",
"- Режим результата: подтвержденный срез обязательств к оплате (exact route)."
"- Результат: подтвержденный срез обязательств к оплате."
];
lines.push("");
lines.push("Блок 2. Что учтено");
@ -1957,7 +2069,13 @@ function composeFactualReply(intent, rows, options = {}) {
lines.push("");
lines.push("Блок 5. Подтвержденные позиции к оплате");
if (confirmedBalances.length > 0) {
lines.push(...confirmedBalances.slice(0, 10).map((item, index) => `${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`));
lines.push(...confirmedBalances.slice(0, 10).flatMap((item, index) => [
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`,
""
]));
if (lines[lines.length - 1] === "") {
lines.pop();
}
}
else {
lines.push("- Подтвержденных открытых обязательств к оплате на дату среза не найдено.");
@ -1972,6 +2090,73 @@ function composeFactualReply(intent, rows, options = {}) {
}
};
}
if (intent === "receivables_confirmed_as_of_date") {
const receivablesAsOfDate = resolveReceivablesAsOfDate(options);
const confirmedBalances = buildReceivablesConfirmedBalanceAggregate(rows, receivablesAsOfDate);
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
const periodTo = normalizeIsoDateOnly(options.periodTo);
const totalOutstandingAmount = confirmedBalances.reduce((sum, item) => sum + item.outstandingAmount, 0);
const periodScopeLine = !asOfDate && (periodFrom || periodTo)
? `- Период анализа: ${formatDateRu(periodFrom ?? "...")}..${formatDateRu(periodTo ?? "...")}.`
: null;
const carryoverLine = asOfDate || periodFrom || periodTo
? "- В срез могут входить задолженности, возникшие до периода, если они оставались открытыми на дату среза."
: null;
const categoryCounts = confirmedBalances.reduce((acc, item) => {
acc[item.category] += 1;
return acc;
}, { supplier_or_contractor: 0, bank_or_credit: 0, tax_or_state: 0, other: 0 });
const lines = [
`Итого подтвержденная дебиторская задолженность на ${formatDateRu(receivablesAsOfDate)}: ${formatMoneyRub(totalOutstandingAmount)}.`,
"",
"Блок 1. Статус результата",
"- Результат: подтвержденный срез дебиторской задолженности."
];
lines.push("");
lines.push("Блок 2. Что учтено");
lines.push(`- Дата среза: ${formatDateRu(receivablesAsOfDate)}.`);
if (periodScopeLine) {
lines.push(periodScopeLine);
}
lines.push("- Контур: дебиторская задолженность по счетам 62/76.");
if (carryoverLine) {
lines.push(carryoverLine);
}
lines.push("");
lines.push("Блок 3. Сводка");
lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`);
lines.push(`- Контрагентов с подтвержденным остатком к получению: ${formatNumberWithDots(confirmedBalances.length)}.`);
lines.push("");
lines.push("Блок 4. Категории дебиторской задолженности");
lines.push(`- ${receivablesCategoryLabel("supplier_or_contractor")}: ${formatNumberWithDots(categoryCounts.supplier_or_contractor)}.`);
lines.push(`- ${receivablesCategoryLabel("bank_or_credit")}: ${formatNumberWithDots(categoryCounts.bank_or_credit)}.`);
lines.push(`- ${receivablesCategoryLabel("tax_or_state")}: ${formatNumberWithDots(categoryCounts.tax_or_state)}.`);
lines.push(`- ${receivablesCategoryLabel("other")}: ${formatNumberWithDots(categoryCounts.other)}.`);
lines.push("");
lines.push("Блок 5. Подтвержденные позиции к получению");
if (confirmedBalances.length > 0) {
lines.push(...confirmedBalances.slice(0, 10).flatMap((item, index) => [
`${index + 1}. ${item.name} | категория: ${receivablesCategoryLabel(item.category)} | остаток к получению: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`,
""
]));
if (lines[lines.length - 1] === "") {
lines.pop();
}
}
else {
lines.push("- Подтвержденной открытой дебиторской задолженности на дату среза не найдено.");
}
return {
responseType: confirmedBalances.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
text: lines.map(emphasizeNumericTokens).join("\n"),
semantics: {
result_mode: "confirmed_balance",
evidence_strength: confirmedBalances.length > 0 ? "strong" : "medium",
balance_confirmed: true
}
};
}
if (intent === "list_payables_counterparties") {
const counterparties = buildPayablesCounterpartyRiskAggregate(rows);
const payablesAsOfDate = resolvePayablesAsOfDate(options);

View File

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

View File

@ -93,7 +93,8 @@ function inferAggregationProfile(intent, shape) {
}
if (intent === "account_balance_snapshot" ||
intent === "documents_forming_balance" ||
intent === "payables_confirmed_as_of_date") {
intent === "payables_confirmed_as_of_date" ||
intent === "receivables_confirmed_as_of_date") {
return "balance_snapshot";
}
if (intent === "open_items_by_counterparty_or_contract" ||

View File

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

View File

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

View File

@ -139,6 +139,10 @@ export const FEATURE_ASSISTANT_ROUTE_PAYABLES_CONFIRMED_V1 = toBooleanFlag(
process.env.FEATURE_ASSISTANT_ROUTE_PAYABLES_CONFIRMED_V1,
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(
process.env.FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1,
true

View File

@ -5,6 +5,7 @@ import {
FEATURE_ASSISTANT_ROUTE_DRILLDOWN_V1,
FEATURE_ASSISTANT_ROUTE_PAYABLES_CONFIRMED_V1,
FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1,
FEATURE_ASSISTANT_ROUTE_RECEIVABLES_CONFIRMED_V1,
FEATURE_ASSISTANT_ROUTE_RECEIVABLES_HEURISTIC_V1,
FEATURE_ASSISTANT_ROUTE_SHADOW_PAYABLES_EXACT_V1
} from "../config";
@ -22,7 +23,12 @@ export interface AddressCapabilityRouteDecision {
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>([
"list_documents_by_counterparty",
"bank_operations_by_counterparty",
@ -53,6 +59,9 @@ function defaultCapabilityId(intent: AddressIntent): string {
if (intent === "payables_confirmed_as_of_date") {
return "confirmed_payables_as_of_date";
}
if (intent === "receivables_confirmed_as_of_date") {
return "confirmed_receivables_as_of_date";
}
if (intent === "list_payables_counterparties") {
return "payables_candidates_list";
}
@ -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"
};
}
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") {
return {
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") {
return ["as_of_date"];
}
if (intent === "receivables_confirmed_as_of_date") {
return ["as_of_date"];
}
if (
intent === "list_documents_by_counterparty" ||
intent === "bank_operations_by_counterparty" ||
@ -857,7 +860,8 @@ function usesAsOfPrimaryWindow(intent: AddressIntent): boolean {
return (
intent === "open_items_by_counterparty_or_contract" ||
intent === "list_open_contracts" ||
intent === "payables_confirmed_as_of_date"
intent === "payables_confirmed_as_of_date" ||
intent === "receivables_confirmed_as_of_date"
);
}
@ -1045,7 +1049,8 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
if (
(intent === "account_balance_snapshot" ||
intent === "documents_forming_balance" ||
intent === "payables_confirmed_as_of_date") &&
intent === "payables_confirmed_as_of_date" ||
intent === "receivables_confirmed_as_of_date") &&
!filters.as_of_date
) {
if (filters.period_to) {

View File

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

View File

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

View File

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

View File

@ -49,6 +49,29 @@ const PAYABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
Сумма __ORDER_DIRECTION__
`;
const RECEIVABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
__AS_OF_EXPR__ КАК Период,
"Остатки на дату" КАК Регистратор,
ПРЕДСТАВЛЕНИЕ(Остатки.Счет) КАК СчетДт,
"" КАК СчетКт,
Остатки.СуммаРазвернутыйОстатокДт КАК Сумма,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоДт1,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоДт2,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоДт3,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоКт1,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоКт2,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоКт3,
ПРЕДСТАВЛЕНИЕ(Остатки.Организация) КАК Организация
ИЗ
РегистрБухгалтерии.Хозрасчетный.Остатки(__AS_OF_EXPR__, , , ) КАК Остатки
ГДЕ
Остатки.СуммаРазвернутыйОстатокДт > 0
И (__RECEIVABLE_ACCOUNTS_MATCH__)
УПОРЯДОЧИТЬ ПО
Сумма __ORDER_DIRECTION__
`;
const BANK_DOCS_QUERY_TEMPLATE = `
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
БанкСписание.Дата КАК Период,
@ -574,6 +597,17 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [
account_scope_mode: "strict",
query_template: "payables_confirmed_as_of_balance_profile"
},
{
recipe_id: "address_receivables_confirmed_as_of_date_v1",
intent: "receivables_confirmed_as_of_date",
purpose: "Build confirmed receivables snapshot as-of date from movements on accounts 62/76",
required_filters: ["as_of_date"],
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
default_limit: 200,
account_scope: ["62", "76"],
account_scope_mode: "strict",
query_template: "receivables_confirmed_as_of_balance_profile"
},
{
recipe_id: "address_movements_receivables_v1",
intent: "list_receivables_counterparties",
@ -1044,6 +1078,25 @@ export function buildAddressRecipePlan(
.replaceAll("__PAYABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"]))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
})()
: recipe.query_template === "receivables_confirmed_as_of_balance_profile"
? (() => {
const asOfExpr =
(typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
? toDateTimeExpr(filters.as_of_date, true)
: null) ??
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
? toDateTimeExpr(filters.period_to, true)
: null) ??
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
? toDateTimeExpr(filters.period_from, true)
: null) ??
"ТЕКУЩАЯДАТА()";
return RECEIVABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
.replaceAll("__LIMIT__", String(resolvedLimit))
.replaceAll("__AS_OF_EXPR__", asOfExpr)
.replaceAll("__RECEIVABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["62", "76"]))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
})()
: MOVEMENTS_QUERY_TEMPLATE
.replace("__LIMIT__", String(resolvedLimit))
.replace("__WHERE_CLAUSE__", (() => {

View File

@ -176,10 +176,7 @@ function formatMoneyRub(value: number): string {
}
function emphasizeNumericTokens(line: string): string {
if (!line) {
return line;
}
return line.replace(/(?<!\*)\d(?:[\d.,:/-]*\d)?(?!\*)/g, (token) => `**${token}**`);
return line;
}
function parseIsoDateToken(value: string | null | undefined): { year: number; month: number; day: number } | null {
@ -700,6 +697,19 @@ function liabilityCategoryLabel(category: PayablesLiabilityCategory): string {
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): {
scores: Record<PayablesLiabilityCategory, number>;
reasons: string[];
@ -784,6 +794,11 @@ function hasPayablesSectionPrefix(account: string | null): boolean {
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 {
const explicit = normalizeIsoDateOnly(options.asOfDate);
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[] {
const byCounterparty = new Map<string, CounterpartyRiskAggregate>();
@ -2462,7 +2597,7 @@ export function composeFactualReply(
`Итого подтвержденный долг на ${formatDateRu(payablesAsOfDate)}: ${formatMoneyRub(totalOutstandingAmount)}.`,
"",
"Блок 1. Статус результата",
"- Режим результата: подтвержденный срез обязательств к оплате (exact route)."
"- Результат: подтвержденный срез обязательств к оплате."
];
lines.push("");
@ -2492,11 +2627,14 @@ export function composeFactualReply(
lines.push("Блок 5. Подтвержденные позиции к оплате");
if (confirmedBalances.length > 0) {
lines.push(
...confirmedBalances.slice(0, 10).map(
(item, index) =>
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`
)
...confirmedBalances.slice(0, 10).flatMap((item, index) => [
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`,
""
])
);
if (lines[lines.length - 1] === "") {
lines.pop();
}
} else {
lines.push("- Подтвержденных открытых обязательств к оплате на дату среза не найдено.");
}
@ -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") {
const counterparties = buildPayablesCounterpartyRiskAggregate(rows);
const payablesAsOfDate = resolvePayablesAsOfDate(options);

View File

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

View File

@ -192,7 +192,8 @@ function inferAggregationProfile(intent: AddressIntent, shape: AddressQueryShape
if (
intent === "account_balance_snapshot" ||
intent === "documents_forming_balance" ||
intent === "payables_confirmed_as_of_date"
intent === "payables_confirmed_as_of_date" ||
intent === "receivables_confirmed_as_of_date"
) {
return "balance_snapshot";
}

View File

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

View File

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

View File

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

View File

@ -15,6 +15,15 @@ describe("address capability policy", () => {
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", () => {
const decision = resolveAddressCapabilityRouteDecision("list_documents_by_contract");
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");
});
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", () => {
const audit = evaluateAddressRouteExpectation({
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");
});
});