ДОМЕНЫ - ВОПРОСЫ - кто должен нам
This commit is contained in:
parent
ae9daa50e7
commit
040a55aaea
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,12 @@ exports.resolveAddressCapabilityRouteDecision = resolveAddressCapabilityRouteDec
|
|||
exports.isCapabilityRouteBlocked = isCapabilityRouteBlocked;
|
||||
exports.resolveShadowRouteIntent = resolveShadowRouteIntent;
|
||||
const config_1 = require("../config");
|
||||
const COMPUTE_EXACT_INTENTS = new Set(["account_balance_snapshot", "documents_forming_balance", "payables_confirmed_as_of_date"]);
|
||||
const COMPUTE_EXACT_INTENTS = new Set([
|
||||
"account_balance_snapshot",
|
||||
"documents_forming_balance",
|
||||
"payables_confirmed_as_of_date",
|
||||
"receivables_confirmed_as_of_date"
|
||||
]);
|
||||
const NAVIGATION_INTENTS = new Set([
|
||||
"list_documents_by_counterparty",
|
||||
"bank_operations_by_counterparty",
|
||||
|
|
@ -31,6 +36,9 @@ function defaultCapabilityId(intent) {
|
|||
if (intent === "payables_confirmed_as_of_date") {
|
||||
return "confirmed_payables_as_of_date";
|
||||
}
|
||||
if (intent === "receivables_confirmed_as_of_date") {
|
||||
return "confirmed_receivables_as_of_date";
|
||||
}
|
||||
if (intent === "list_payables_counterparties") {
|
||||
return "payables_candidates_list";
|
||||
}
|
||||
|
|
@ -58,6 +66,14 @@ function resolveCapabilityEnabled(intent) {
|
|||
reason: config_1.FEATURE_ASSISTANT_ROUTE_PAYABLES_CONFIRMED_V1 ? "payables_confirmed_route_enabled" : "payables_confirmed_route_disabled_by_flag"
|
||||
};
|
||||
}
|
||||
if (intent === "receivables_confirmed_as_of_date") {
|
||||
return {
|
||||
enabled: config_1.FEATURE_ASSISTANT_ROUTE_RECEIVABLES_CONFIRMED_V1,
|
||||
reason: config_1.FEATURE_ASSISTANT_ROUTE_RECEIVABLES_CONFIRMED_V1
|
||||
? "receivables_confirmed_route_enabled"
|
||||
: "receivables_confirmed_route_disabled_by_flag"
|
||||
};
|
||||
}
|
||||
if (intent === "list_payables_counterparties") {
|
||||
return {
|
||||
enabled: config_1.FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1,
|
||||
|
|
|
|||
|
|
@ -748,6 +748,9 @@ function requiredFiltersByIntent(intent) {
|
|||
if (intent === "payables_confirmed_as_of_date") {
|
||||
return ["as_of_date"];
|
||||
}
|
||||
if (intent === "receivables_confirmed_as_of_date") {
|
||||
return ["as_of_date"];
|
||||
}
|
||||
if (intent === "list_documents_by_counterparty" ||
|
||||
intent === "bank_operations_by_counterparty" ||
|
||||
intent === "list_contracts_by_counterparty") {
|
||||
|
|
@ -761,7 +764,8 @@ function requiredFiltersByIntent(intent) {
|
|||
function usesAsOfPrimaryWindow(intent) {
|
||||
return (intent === "open_items_by_counterparty_or_contract" ||
|
||||
intent === "list_open_contracts" ||
|
||||
intent === "payables_confirmed_as_of_date");
|
||||
intent === "payables_confirmed_as_of_date" ||
|
||||
intent === "receivables_confirmed_as_of_date");
|
||||
}
|
||||
function extractAddressFilters(userMessage, intent) {
|
||||
const rawText = String(userMessage ?? "").trim();
|
||||
|
|
@ -923,7 +927,8 @@ function extractAddressFilters(userMessage, intent) {
|
|||
// - else default to today.
|
||||
if ((intent === "account_balance_snapshot" ||
|
||||
intent === "documents_forming_balance" ||
|
||||
intent === "payables_confirmed_as_of_date") &&
|
||||
intent === "payables_confirmed_as_of_date" ||
|
||||
intent === "receivables_confirmed_as_of_date") &&
|
||||
!filters.as_of_date) {
|
||||
if (filters.period_to) {
|
||||
filters.as_of_date = filters.period_to;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|||
exports.resolveAddressIntent = resolveAddressIntent;
|
||||
const RECEIVABLES_STRONG = [
|
||||
"кто должен нам",
|
||||
"кто нам должен",
|
||||
"кто нам должэн",
|
||||
"нам должны",
|
||||
"нам должен",
|
||||
"нам должэны",
|
||||
"who owes us",
|
||||
"receivable",
|
||||
"receivables",
|
||||
|
|
@ -14,7 +18,11 @@ const RECEIVABLES_STRONG = [
|
|||
];
|
||||
const PAYABLES_STRONG = [
|
||||
"кому должны мы",
|
||||
"кому должэны мы",
|
||||
"кому мы должны",
|
||||
"кому мы должэны",
|
||||
"мы должны",
|
||||
"мы должэны",
|
||||
"who we owe",
|
||||
"payable",
|
||||
"payables",
|
||||
|
|
@ -865,6 +873,18 @@ function hasPayablesDebtLifecycleSignal(text) {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
function hasReceivablesDebtLifecycleSignal(text) {
|
||||
const hasOweUsSignal = /(?:кто\s+нам\s+долж(?:ен|ны)?|кто\s+долж(?:ен|ны)?\s+нам|нам\s+долж(?:ен|ны)|должник(?:и|ов|а)?|дебитор(?:ы|ов|ск)?|задолж|долг(?:и|ов|а|у)?|к\s+получению|на\s+поступление|к\s+взысканию|who\s+owes\s+us|receivables?)/iu.test(text);
|
||||
if (!hasOweUsSignal) {
|
||||
return false;
|
||||
}
|
||||
const hasPastInflowSignal = /(?:прин[её]с|зан[её]с|поступил|приход|inflow|paid\s+us|already\s+paid)/iu.test(text);
|
||||
const hasTopRankingSignal = /(?:топ|top|больше\s+всего|сам(?:ый|ая|ое|ые)|наибольш|максимальн)/iu.test(text);
|
||||
if (hasPastInflowSignal && hasTopRankingSignal) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
function hasReceivablesLatencyRiskSignal(text) {
|
||||
const hasBuyer = /(?:покупател|клиент|заказчик|customer|buyer)/iu.test(text);
|
||||
const hasCounterparty = /(?:контрагент|counterparty|partner)/iu.test(text);
|
||||
|
|
@ -1239,10 +1259,15 @@ function resolveAddressIntent(userMessage) {
|
|||
};
|
||||
}
|
||||
if (hasAny(text, RECEIVABLES_STRONG)) {
|
||||
const receivablesDebtLifecycleSignal = hasReceivablesDebtLifecycleSignal(text);
|
||||
const reasons = ["receivables_signal_detected"];
|
||||
if (receivablesDebtLifecycleSignal) {
|
||||
reasons.push("receivables_debt_lifecycle_signal_detected");
|
||||
}
|
||||
return {
|
||||
intent: "list_receivables_counterparties",
|
||||
intent: receivablesDebtLifecycleSignal ? "receivables_confirmed_as_of_date" : "list_receivables_counterparties",
|
||||
confidence: "high",
|
||||
reasons: ["receivables_signal_detected"]
|
||||
reasons
|
||||
};
|
||||
}
|
||||
if (hasAny(text, PAYABLES_STRONG)) {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ const DISPLAY_ENTITY_TYPE_BY_INTENT = {
|
|||
customer_revenue_and_payments: "counterparty",
|
||||
supplier_payouts_profile: "counterparty",
|
||||
list_payables_counterparties: "counterparty",
|
||||
receivables_confirmed_as_of_date: "counterparty",
|
||||
list_receivables_counterparties: "counterparty",
|
||||
list_contracts_by_counterparty: "contract",
|
||||
list_documents_by_counterparty: "document_ref",
|
||||
|
|
@ -28,6 +29,7 @@ const RESULT_SET_TYPE_BY_INTENT = {
|
|||
supplier_payouts_profile: "counterparty_list",
|
||||
list_payables_counterparties: "counterparty_list",
|
||||
payables_confirmed_as_of_date: "balance_snapshot",
|
||||
receivables_confirmed_as_of_date: "balance_snapshot",
|
||||
list_receivables_counterparties: "counterparty_list",
|
||||
list_contracts_by_counterparty: "contract_list",
|
||||
list_documents_by_counterparty: "document_list",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -44,6 +44,28 @@ const PAYABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
|
|||
УПОРЯДОЧИТЬ ПО
|
||||
Сумма __ORDER_DIRECTION__
|
||||
`;
|
||||
const RECEIVABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
__AS_OF_EXPR__ КАК Период,
|
||||
"Остатки на дату" КАК Регистратор,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Счет) КАК СчетДт,
|
||||
"" КАК СчетКт,
|
||||
Остатки.СуммаРазвернутыйОстатокДт КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоДт1,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоДт2,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоДт3,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоКт1,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоКт2,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоКт3,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Организация) КАК Организация
|
||||
ИЗ
|
||||
РегистрБухгалтерии.Хозрасчетный.Остатки(__AS_OF_EXPR__, , , ) КАК Остатки
|
||||
ГДЕ
|
||||
Остатки.СуммаРазвернутыйОстатокДт > 0
|
||||
И (__RECEIVABLE_ACCOUNTS_MATCH__)
|
||||
УПОРЯДОЧИТЬ ПО
|
||||
Сумма __ORDER_DIRECTION__
|
||||
`;
|
||||
const BANK_DOCS_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
БанкСписание.Дата КАК Период,
|
||||
|
|
@ -558,6 +580,17 @@ const BASE_RECIPES = [
|
|||
account_scope_mode: "strict",
|
||||
query_template: "payables_confirmed_as_of_balance_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_receivables_confirmed_as_of_date_v1",
|
||||
intent: "receivables_confirmed_as_of_date",
|
||||
purpose: "Build confirmed receivables snapshot as-of date from movements on accounts 62/76",
|
||||
required_filters: ["as_of_date"],
|
||||
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
|
||||
default_limit: 200,
|
||||
account_scope: ["62", "76"],
|
||||
account_scope_mode: "strict",
|
||||
query_template: "receivables_confirmed_as_of_balance_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_movements_receivables_v1",
|
||||
intent: "list_receivables_counterparties",
|
||||
|
|
@ -947,17 +980,35 @@ function buildAddressRecipePlan(recipe, filters) {
|
|||
.replaceAll("__PAYABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"]))
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
})()
|
||||
: MOVEMENTS_QUERY_TEMPLATE
|
||||
.replace("__LIMIT__", String(resolvedLimit))
|
||||
.replace("__WHERE_CLAUSE__", (() => {
|
||||
const extraConditions = [];
|
||||
const accountCondition = buildMovementAccountCondition(filters);
|
||||
if (accountCondition) {
|
||||
extraConditions.push(accountCondition);
|
||||
}
|
||||
return buildWhereClause(filters, "Движения.Период", extraConditions);
|
||||
})())
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
: recipe.query_template === "receivables_confirmed_as_of_balance_profile"
|
||||
? (() => {
|
||||
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||
? toDateTimeExpr(filters.as_of_date, true)
|
||||
: null) ??
|
||||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_to, true)
|
||||
: null) ??
|
||||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_from, true)
|
||||
: null) ??
|
||||
"ТЕКУЩАЯДАТА()";
|
||||
return RECEIVABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||
.replaceAll("__RECEIVABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["62", "76"]))
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
})()
|
||||
: MOVEMENTS_QUERY_TEMPLATE
|
||||
.replace("__LIMIT__", String(resolvedLimit))
|
||||
.replace("__WHERE_CLAUSE__", (() => {
|
||||
const extraConditions = [];
|
||||
const accountCondition = buildMovementAccountCondition(filters);
|
||||
if (accountCondition) {
|
||||
extraConditions.push(accountCondition);
|
||||
}
|
||||
return buildWhereClause(filters, "Движения.Период", extraConditions);
|
||||
})())
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
return {
|
||||
recipe,
|
||||
query,
|
||||
|
|
|
|||
|
|
@ -102,10 +102,7 @@ function formatMoneyRub(value) {
|
|||
return `${formatNumberWithDots(value, 2)} ₽`;
|
||||
}
|
||||
function emphasizeNumericTokens(line) {
|
||||
if (!line) {
|
||||
return line;
|
||||
}
|
||||
return line.replace(/(?<!\*)\d(?:[\d.,:/-]*\d)?(?!\*)/g, (token) => `**${token}**`);
|
||||
return line;
|
||||
}
|
||||
function parseIsoDateToken(value) {
|
||||
const source = String(value ?? "").trim();
|
||||
|
|
@ -514,6 +511,18 @@ function liabilityCategoryLabel(category) {
|
|||
}
|
||||
return "прочие";
|
||||
}
|
||||
function receivablesCategoryLabel(category) {
|
||||
if (category === "supplier_or_contractor") {
|
||||
return "покупатели/заказчики";
|
||||
}
|
||||
if (category === "bank_or_credit") {
|
||||
return "банки/финансовые";
|
||||
}
|
||||
if (category === "tax_or_state") {
|
||||
return "бюджет/госорганы";
|
||||
}
|
||||
return "прочие";
|
||||
}
|
||||
function classifyPayablesLiabilityCategory(row, counterparty) {
|
||||
const scores = {
|
||||
supplier_or_contractor: 0,
|
||||
|
|
@ -582,6 +591,10 @@ function hasPayablesSectionPrefix(account) {
|
|||
const section = extractAccountSectionCode(account);
|
||||
return section === "60" || section === "76";
|
||||
}
|
||||
function hasReceivablesSectionPrefix(account) {
|
||||
const section = extractAccountSectionCode(account);
|
||||
return section === "62" || section === "76";
|
||||
}
|
||||
function resolvePayablesAsOfDate(options) {
|
||||
const explicit = normalizeIsoDateOnly(options.asOfDate);
|
||||
if (explicit) {
|
||||
|
|
@ -762,6 +775,105 @@ function buildPayablesConfirmedBalanceAggregate(rows, asOfDate) {
|
|||
return left.name.localeCompare(right.name);
|
||||
});
|
||||
}
|
||||
function buildReceivablesConfirmedBalanceAggregate(rows, asOfDate) {
|
||||
const byCounterparty = new Map();
|
||||
const asOfTimestamp = toUtcDayTimestamp(asOfDate);
|
||||
for (const row of rows) {
|
||||
const name = extractCounterpartyName(row);
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
const rowTimestamp = toUtcDayTimestamp(row.period);
|
||||
if (asOfTimestamp !== null && rowTimestamp !== null && rowTimestamp > asOfTimestamp) {
|
||||
continue;
|
||||
}
|
||||
const amount = row.amount;
|
||||
if (typeof amount !== "number" || !Number.isFinite(amount)) {
|
||||
continue;
|
||||
}
|
||||
const absAmount = Math.abs(amount);
|
||||
let delta = 0;
|
||||
if (hasReceivablesSectionPrefix(row.account_dt)) {
|
||||
delta += absAmount;
|
||||
}
|
||||
if (hasReceivablesSectionPrefix(row.account_kt)) {
|
||||
delta -= absAmount;
|
||||
}
|
||||
if (Math.abs(delta) <= 0.0000001) {
|
||||
continue;
|
||||
}
|
||||
const classified = classifyPayablesLiabilityCategory(row, name);
|
||||
const contract = extractContractName(row);
|
||||
const sourceRefs = extractPayablesSourceRefs(row, name, contract);
|
||||
const current = byCounterparty.get(name);
|
||||
if (!current) {
|
||||
byCounterparty.set(name, {
|
||||
outstandingAmount: delta,
|
||||
operations: 1,
|
||||
firstPeriod: row.period,
|
||||
lastPeriod: row.period,
|
||||
categoryScores: {
|
||||
supplier_or_contractor: classified.scores.supplier_or_contractor,
|
||||
bank_or_credit: classified.scores.bank_or_credit,
|
||||
tax_or_state: classified.scores.tax_or_state,
|
||||
other: classified.scores.other
|
||||
},
|
||||
reasons: new Set(classified.reasons),
|
||||
contracts: new Set(contract ? [contract] : []),
|
||||
documents: new Set(row.registrator ? [row.registrator] : []),
|
||||
sourceRefs: new Set(sourceRefs)
|
||||
});
|
||||
continue;
|
||||
}
|
||||
current.outstandingAmount += delta;
|
||||
current.operations += 1;
|
||||
if ((row.period ?? "") < (current.firstPeriod ?? "")) {
|
||||
current.firstPeriod = row.period;
|
||||
}
|
||||
if ((row.period ?? "") > (current.lastPeriod ?? "")) {
|
||||
current.lastPeriod = row.period;
|
||||
}
|
||||
current.categoryScores.supplier_or_contractor += classified.scores.supplier_or_contractor;
|
||||
current.categoryScores.bank_or_credit += classified.scores.bank_or_credit;
|
||||
current.categoryScores.tax_or_state += classified.scores.tax_or_state;
|
||||
current.categoryScores.other += classified.scores.other;
|
||||
for (const reason of classified.reasons) {
|
||||
current.reasons.add(reason);
|
||||
}
|
||||
if (contract) {
|
||||
current.contracts.add(contract);
|
||||
}
|
||||
if (row.registrator) {
|
||||
current.documents.add(row.registrator);
|
||||
}
|
||||
for (const ref of sourceRefs) {
|
||||
current.sourceRefs.add(ref);
|
||||
}
|
||||
}
|
||||
return Array.from(byCounterparty.entries())
|
||||
.map(([name, item]) => ({
|
||||
name,
|
||||
outstandingAmount: item.outstandingAmount,
|
||||
operations: item.operations,
|
||||
firstPeriod: item.firstPeriod,
|
||||
lastPeriod: item.lastPeriod,
|
||||
category: resolvePayablesLiabilityCategory(item.categoryScores),
|
||||
categoryReasons: Array.from(item.reasons).slice(0, 2),
|
||||
contracts: Array.from(item.contracts).slice(0, 2),
|
||||
documents: Array.from(item.documents).slice(0, 2),
|
||||
sourceRefs: Array.from(item.sourceRefs).slice(0, 3)
|
||||
}))
|
||||
.filter((item) => item.outstandingAmount > 0.005)
|
||||
.sort((left, right) => {
|
||||
if (right.outstandingAmount !== left.outstandingAmount) {
|
||||
return right.outstandingAmount - left.outstandingAmount;
|
||||
}
|
||||
if (right.operations !== left.operations) {
|
||||
return right.operations - left.operations;
|
||||
}
|
||||
return left.name.localeCompare(right.name);
|
||||
});
|
||||
}
|
||||
function buildCounterpartyRiskAggregate(rows) {
|
||||
const byCounterparty = new Map();
|
||||
for (const row of rows) {
|
||||
|
|
@ -1932,7 +2044,7 @@ function composeFactualReply(intent, rows, options = {}) {
|
|||
`Итого подтвержденный долг на ${formatDateRu(payablesAsOfDate)}: ${formatMoneyRub(totalOutstandingAmount)}.`,
|
||||
"",
|
||||
"Блок 1. Статус результата",
|
||||
"- Режим результата: подтвержденный срез обязательств к оплате (exact route)."
|
||||
"- Результат: подтвержденный срез обязательств к оплате."
|
||||
];
|
||||
lines.push("");
|
||||
lines.push("Блок 2. Что учтено");
|
||||
|
|
@ -1957,7 +2069,13 @@ function composeFactualReply(intent, rows, options = {}) {
|
|||
lines.push("");
|
||||
lines.push("Блок 5. Подтвержденные позиции к оплате");
|
||||
if (confirmedBalances.length > 0) {
|
||||
lines.push(...confirmedBalances.slice(0, 10).map((item, index) => `${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`));
|
||||
lines.push(...confirmedBalances.slice(0, 10).flatMap((item, index) => [
|
||||
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`,
|
||||
""
|
||||
]));
|
||||
if (lines[lines.length - 1] === "") {
|
||||
lines.pop();
|
||||
}
|
||||
}
|
||||
else {
|
||||
lines.push("- Подтвержденных открытых обязательств к оплате на дату среза не найдено.");
|
||||
|
|
@ -1972,6 +2090,73 @@ function composeFactualReply(intent, rows, options = {}) {
|
|||
}
|
||||
};
|
||||
}
|
||||
if (intent === "receivables_confirmed_as_of_date") {
|
||||
const receivablesAsOfDate = resolveReceivablesAsOfDate(options);
|
||||
const confirmedBalances = buildReceivablesConfirmedBalanceAggregate(rows, receivablesAsOfDate);
|
||||
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
|
||||
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
|
||||
const periodTo = normalizeIsoDateOnly(options.periodTo);
|
||||
const totalOutstandingAmount = confirmedBalances.reduce((sum, item) => sum + item.outstandingAmount, 0);
|
||||
const periodScopeLine = !asOfDate && (periodFrom || periodTo)
|
||||
? `- Период анализа: ${formatDateRu(periodFrom ?? "...")}..${formatDateRu(periodTo ?? "...")}.`
|
||||
: null;
|
||||
const carryoverLine = asOfDate || periodFrom || periodTo
|
||||
? "- В срез могут входить задолженности, возникшие до периода, если они оставались открытыми на дату среза."
|
||||
: null;
|
||||
const categoryCounts = confirmedBalances.reduce((acc, item) => {
|
||||
acc[item.category] += 1;
|
||||
return acc;
|
||||
}, { supplier_or_contractor: 0, bank_or_credit: 0, tax_or_state: 0, other: 0 });
|
||||
const lines = [
|
||||
`Итого подтвержденная дебиторская задолженность на ${formatDateRu(receivablesAsOfDate)}: ${formatMoneyRub(totalOutstandingAmount)}.`,
|
||||
"",
|
||||
"Блок 1. Статус результата",
|
||||
"- Результат: подтвержденный срез дебиторской задолженности."
|
||||
];
|
||||
lines.push("");
|
||||
lines.push("Блок 2. Что учтено");
|
||||
lines.push(`- Дата среза: ${formatDateRu(receivablesAsOfDate)}.`);
|
||||
if (periodScopeLine) {
|
||||
lines.push(periodScopeLine);
|
||||
}
|
||||
lines.push("- Контур: дебиторская задолженность по счетам 62/76.");
|
||||
if (carryoverLine) {
|
||||
lines.push(carryoverLine);
|
||||
}
|
||||
lines.push("");
|
||||
lines.push("Блок 3. Сводка");
|
||||
lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`);
|
||||
lines.push(`- Контрагентов с подтвержденным остатком к получению: ${formatNumberWithDots(confirmedBalances.length)}.`);
|
||||
lines.push("");
|
||||
lines.push("Блок 4. Категории дебиторской задолженности");
|
||||
lines.push(`- ${receivablesCategoryLabel("supplier_or_contractor")}: ${formatNumberWithDots(categoryCounts.supplier_or_contractor)}.`);
|
||||
lines.push(`- ${receivablesCategoryLabel("bank_or_credit")}: ${formatNumberWithDots(categoryCounts.bank_or_credit)}.`);
|
||||
lines.push(`- ${receivablesCategoryLabel("tax_or_state")}: ${formatNumberWithDots(categoryCounts.tax_or_state)}.`);
|
||||
lines.push(`- ${receivablesCategoryLabel("other")}: ${formatNumberWithDots(categoryCounts.other)}.`);
|
||||
lines.push("");
|
||||
lines.push("Блок 5. Подтвержденные позиции к получению");
|
||||
if (confirmedBalances.length > 0) {
|
||||
lines.push(...confirmedBalances.slice(0, 10).flatMap((item, index) => [
|
||||
`${index + 1}. ${item.name} | категория: ${receivablesCategoryLabel(item.category)} | остаток к получению: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`,
|
||||
""
|
||||
]));
|
||||
if (lines[lines.length - 1] === "") {
|
||||
lines.pop();
|
||||
}
|
||||
}
|
||||
else {
|
||||
lines.push("- Подтвержденной открытой дебиторской задолженности на дату среза не найдено.");
|
||||
}
|
||||
return {
|
||||
responseType: confirmedBalances.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
|
||||
text: lines.map(emphasizeNumericTokens).join("\n"),
|
||||
semantics: {
|
||||
result_mode: "confirmed_balance",
|
||||
evidence_strength: confirmedBalances.length > 0 ? "strong" : "medium",
|
||||
balance_confirmed: true
|
||||
}
|
||||
};
|
||||
}
|
||||
if (intent === "list_payables_counterparties") {
|
||||
const counterparties = buildPayablesCounterpartyRiskAggregate(rows);
|
||||
const payablesAsOfDate = resolvePayablesAsOfDate(options);
|
||||
|
|
|
|||
|
|
@ -352,7 +352,8 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
|||
}
|
||||
if (intent === "open_items_by_counterparty_or_contract" ||
|
||||
intent === "list_open_contracts" ||
|
||||
intent === "payables_confirmed_as_of_date") {
|
||||
intent === "payables_confirmed_as_of_date" ||
|
||||
intent === "receivables_confirmed_as_of_date") {
|
||||
const inheritedContract = previousContract ?? (followupContext.previous_anchor_type === "contract" ? previousAnchorValue : null);
|
||||
const currentContract = toNonEmptyString(merged.contract);
|
||||
const shouldInheritContract = !currentContract ||
|
||||
|
|
@ -434,6 +435,7 @@ function resolveMissingRequiredFilters(intent, filters) {
|
|||
account_balance_snapshot: ["account", "as_of_date"],
|
||||
documents_forming_balance: ["account", "as_of_date"],
|
||||
payables_confirmed_as_of_date: ["as_of_date"],
|
||||
receivables_confirmed_as_of_date: ["as_of_date"],
|
||||
list_documents_by_counterparty: ["counterparty"],
|
||||
bank_operations_by_counterparty: ["counterparty"],
|
||||
list_contracts_by_counterparty: ["counterparty"],
|
||||
|
|
|
|||
|
|
@ -93,7 +93,8 @@ function inferAggregationProfile(intent, shape) {
|
|||
}
|
||||
if (intent === "account_balance_snapshot" ||
|
||||
intent === "documents_forming_balance" ||
|
||||
intent === "payables_confirmed_as_of_date") {
|
||||
intent === "payables_confirmed_as_of_date" ||
|
||||
intent === "receivables_confirmed_as_of_date") {
|
||||
return "balance_snapshot";
|
||||
}
|
||||
if (intent === "open_items_by_counterparty_or_contract" ||
|
||||
|
|
|
|||
|
|
@ -568,7 +568,7 @@ function scrubRawTechnicalRefs(value) {
|
|||
.replace(RAW_REF_TOKEN_PATTERN, "reference")
|
||||
.replace(/\(\s*\[id\]\s*\)/g, "")
|
||||
.replace(/\[\s*id\s*\](?:\s*,\s*\[\s*id\s*\])+/g, "[id]")
|
||||
.replace(/\s{2,}/g, " ")
|
||||
.replace(/[ \t]{2,}/g, " ")
|
||||
.trim();
|
||||
}
|
||||
function stripSyntheticPlaceholders(value) {
|
||||
|
|
@ -582,23 +582,48 @@ function stripSyntheticPlaceholders(value) {
|
|||
}
|
||||
function sanitizeUserFacingReply(value) {
|
||||
const raw = String(value ?? "");
|
||||
const hardCutMatch = raw.match(/(?:^|\n)\s*#{0,6}\s*(?:debug_payload_json|technical_breakdown_json)\b/i);
|
||||
const hardCutMatch = raw.match(/(?:^|\n)\s*#{0,6}\s*(?:debug_payload_json|technical_breakdown_json|technical_debug_payload_json)\b/i);
|
||||
const preCut = hardCutMatch ? raw.slice(0, hardCutMatch.index) : raw;
|
||||
const withoutDebugBlocks = preCut
|
||||
.replace(/###\s*debug_payload_json[\s\S]*?(?:```[\s\S]*?```|$)/gi, "")
|
||||
.replace(/###\s*technical_debug_payload_json[\s\S]*?(?:```[\s\S]*?```|$)/gi, "")
|
||||
.replace(/###\s*technical_breakdown_json[\s\S]*?(?:```[\s\S]*?```|$)/gi, "")
|
||||
.replace(/(?:^|\n)\s*#{0,6}\s*(?:debug_payload_json|technical_breakdown_json)\b[\s\S]*$/gi, "")
|
||||
.replace(/(?:^|\n)\s*#{0,6}\s*(?:debug_payload_json|technical_breakdown_json|technical_debug_payload_json)\b[\s\S]*$/gi, "")
|
||||
.replace(/```json[\s\S]*?```/gi, "");
|
||||
const normalized = scrubRawTechnicalRefs(withoutDebugBlocks).replace(/[ \t]+\n/g, "\n");
|
||||
const cleanedLines = normalized
|
||||
const preparedLines = normalized
|
||||
.split(/\r?\n/g)
|
||||
.map((line) => stripSyntheticPlaceholders(line))
|
||||
.map((line) => stripMojibakeFragments(line))
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0)
|
||||
.filter((line) => !/^(?:-\s*)?(?:action|clarify|open|limit|note):\s*$/i.test(line))
|
||||
.filter((line) => !hasUserFacingLeakage(line))
|
||||
.filter((line) => !looksLikeMojibake(line));
|
||||
.map((line) => line.trim());
|
||||
const cleanedLines = [];
|
||||
let previousWasBlank = false;
|
||||
for (const line of preparedLines) {
|
||||
if (line.length === 0) {
|
||||
if (!previousWasBlank && cleanedLines.length > 0) {
|
||||
cleanedLines.push("");
|
||||
}
|
||||
previousWasBlank = true;
|
||||
continue;
|
||||
}
|
||||
if (/^(?:-\s*)?(?:action|clarify|open|limit|note):\s*$/i.test(line)) {
|
||||
continue;
|
||||
}
|
||||
if (hasUserFacingLeakage(line)) {
|
||||
continue;
|
||||
}
|
||||
if (looksLikeMojibake(line)) {
|
||||
continue;
|
||||
}
|
||||
cleanedLines.push(line);
|
||||
previousWasBlank = false;
|
||||
}
|
||||
while (cleanedLines.length > 0 && cleanedLines[0] === "") {
|
||||
cleanedLines.shift();
|
||||
}
|
||||
while (cleanedLines.length > 0 && cleanedLines[cleanedLines.length - 1] === "") {
|
||||
cleanedLines.pop();
|
||||
}
|
||||
const cleaned = cleanedLines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
||||
return cleaned || "Available data requires clarification for a reliable user-facing answer.";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3714,7 +3714,10 @@ const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([
|
|||
"supplier_payouts_profile",
|
||||
"list_open_contracts",
|
||||
"open_items_by_counterparty_or_contract",
|
||||
"list_payables_counterparties",
|
||||
"list_receivables_counterparties",
|
||||
"payables_confirmed_as_of_date",
|
||||
"receivables_confirmed_as_of_date",
|
||||
"list_documents_by_contract",
|
||||
"bank_operations_by_contract",
|
||||
"list_documents_by_counterparty",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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__", (() => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue