ДОМЕНЫ - ВОПРОСЫ - НДС: развести as-of и tax-period intent, ускорить и стабилизировать VAT source probe, починить M23 тесты
This commit is contained in:
parent
f1ef5f9d3c
commit
c4f87222a8
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"schema_version": "address_route_expectations_v1",
|
"schema_version": "address_route_expectations_v1",
|
||||||
"updated_at": "2026-04-12T20:50:00.000Z",
|
"updated_at": "2026-04-13T00:15:00.000Z",
|
||||||
"entries": [
|
"entries": [
|
||||||
{
|
{
|
||||||
"intent": "payables_confirmed_as_of_date",
|
"intent": "payables_confirmed_as_of_date",
|
||||||
|
|
@ -20,6 +20,16 @@
|
||||||
"expected_requested_result_modes": ["confirmed_balance"],
|
"expected_requested_result_modes": ["confirmed_balance"],
|
||||||
"expected_result_modes": ["confirmed_balance"]
|
"expected_result_modes": ["confirmed_balance"]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"intent": "vat_payable_forecast",
|
||||||
|
"expected_selected_recipes": ["address_vat_payable_forecast_v1"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"intent": "vat_liability_confirmed_for_tax_period",
|
||||||
|
"expected_selected_recipes": ["address_vat_liability_confirmed_tax_period_v1"],
|
||||||
|
"expected_requested_result_modes": ["confirmed_balance"],
|
||||||
|
"expected_result_modes": ["confirmed_balance"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"intent": "list_payables_counterparties",
|
"intent": "list_payables_counterparties",
|
||||||
"expected_selected_recipes": ["address_movements_payables_v1", "address_open_items_by_party_or_contract_v1"],
|
"expected_selected_recipes": ["address_movements_payables_v1", "address_open_items_by_party_or_contract_v1"],
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"schema_version": "capabilities_registry_v1",
|
"schema_version": "capabilities_registry_v1",
|
||||||
"updated_at": "2026-04-12T20:50:00.000Z",
|
"updated_at": "2026-04-13T00:15:00.000Z",
|
||||||
"assistant_mode": "read_only",
|
"assistant_mode": "read_only",
|
||||||
"groups": [
|
"groups": [
|
||||||
{
|
{
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
"maturity_status": "partial",
|
"maturity_status": "partial",
|
||||||
"supported_operations": [
|
"supported_operations": [
|
||||||
"vat_period_snapshot",
|
"vat_period_snapshot",
|
||||||
|
"vat_liability_confirmed_for_tax_period",
|
||||||
"vat_payable_confirmed_as_of_date",
|
"vat_payable_confirmed_as_of_date",
|
||||||
"vat_payable_forecast",
|
"vat_payable_forecast",
|
||||||
"vat_turnover_breakdown"
|
"vat_turnover_breakdown"
|
||||||
|
|
@ -34,6 +35,7 @@
|
||||||
],
|
],
|
||||||
"related_routes": [
|
"related_routes": [
|
||||||
"address_vat_payable_confirmed_as_of_date_v1",
|
"address_vat_payable_confirmed_as_of_date_v1",
|
||||||
|
"address_vat_liability_confirmed_tax_period_v1",
|
||||||
"address_vat_payable_forecast_v1"
|
"address_vat_payable_forecast_v1"
|
||||||
],
|
],
|
||||||
"safe_alternatives": [
|
"safe_alternatives": [
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,8 @@ const COMPUTE_EXACT_INTENTS = new Set([
|
||||||
"documents_forming_balance",
|
"documents_forming_balance",
|
||||||
"payables_confirmed_as_of_date",
|
"payables_confirmed_as_of_date",
|
||||||
"receivables_confirmed_as_of_date",
|
"receivables_confirmed_as_of_date",
|
||||||
"vat_payable_confirmed_as_of_date"
|
"vat_payable_confirmed_as_of_date",
|
||||||
|
"vat_liability_confirmed_for_tax_period"
|
||||||
]);
|
]);
|
||||||
const NAVIGATION_INTENTS = new Set([
|
const NAVIGATION_INTENTS = new Set([
|
||||||
"list_documents_by_counterparty",
|
"list_documents_by_counterparty",
|
||||||
|
|
@ -43,6 +44,9 @@ function defaultCapabilityId(intent) {
|
||||||
if (intent === "vat_payable_confirmed_as_of_date") {
|
if (intent === "vat_payable_confirmed_as_of_date") {
|
||||||
return "confirmed_vat_payable_as_of_date";
|
return "confirmed_vat_payable_as_of_date";
|
||||||
}
|
}
|
||||||
|
if (intent === "vat_liability_confirmed_for_tax_period") {
|
||||||
|
return "confirmed_vat_liability_for_tax_period";
|
||||||
|
}
|
||||||
if (intent === "list_payables_counterparties") {
|
if (intent === "list_payables_counterparties") {
|
||||||
return "payables_candidates_list";
|
return "payables_candidates_list";
|
||||||
}
|
}
|
||||||
|
|
@ -86,6 +90,14 @@ function resolveCapabilityEnabled(intent) {
|
||||||
: "vat_payable_confirmed_route_disabled_by_flag"
|
: "vat_payable_confirmed_route_disabled_by_flag"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (intent === "vat_liability_confirmed_for_tax_period") {
|
||||||
|
return {
|
||||||
|
enabled: config_1.FEATURE_ASSISTANT_ROUTE_BALANCE_EXACT_V1,
|
||||||
|
reason: config_1.FEATURE_ASSISTANT_ROUTE_BALANCE_EXACT_V1
|
||||||
|
? "vat_liability_confirmed_tax_period_route_enabled"
|
||||||
|
: "vat_liability_confirmed_tax_period_route_disabled_by_flag"
|
||||||
|
};
|
||||||
|
}
|
||||||
if (intent === "list_payables_counterparties") {
|
if (intent === "list_payables_counterparties") {
|
||||||
return {
|
return {
|
||||||
enabled: config_1.FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1,
|
enabled: config_1.FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1,
|
||||||
|
|
|
||||||
|
|
@ -825,6 +825,9 @@ function requiredFiltersByIntent(intent) {
|
||||||
if (intent === "vat_payable_confirmed_as_of_date") {
|
if (intent === "vat_payable_confirmed_as_of_date") {
|
||||||
return ["as_of_date"];
|
return ["as_of_date"];
|
||||||
}
|
}
|
||||||
|
if (intent === "vat_liability_confirmed_for_tax_period") {
|
||||||
|
return ["period_from", "period_to"];
|
||||||
|
}
|
||||||
if (intent === "list_documents_by_counterparty" ||
|
if (intent === "list_documents_by_counterparty" ||
|
||||||
intent === "bank_operations_by_counterparty" ||
|
intent === "bank_operations_by_counterparty" ||
|
||||||
intent === "list_contracts_by_counterparty") {
|
intent === "list_contracts_by_counterparty") {
|
||||||
|
|
@ -972,6 +975,17 @@ function extractAddressFilters(userMessage, intent) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (intent === "vat_liability_confirmed_for_tax_period" && !periodRange.period_from && !periodRange.period_to) {
|
||||||
|
const periodToForQuarter = filters.period_to ?? vatAsOfDate ?? null;
|
||||||
|
if (periodToForQuarter) {
|
||||||
|
const quarterWindow = deriveQuarterWindowForDate(periodToForQuarter);
|
||||||
|
if (quarterWindow) {
|
||||||
|
filters.period_from = quarterWindow.period_from;
|
||||||
|
filters.period_to = quarterWindow.period_to;
|
||||||
|
warnings.push("period_derived_from_tax_quarter_for_confirmed_vat_liability");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (isManagementProfileIntent && !filters.period_to && !filters.as_of_date) {
|
if (isManagementProfileIntent && !filters.period_to && !filters.as_of_date) {
|
||||||
filters.period_to = new Date().toISOString().slice(0, 10);
|
filters.period_to = new Date().toISOString().slice(0, 10);
|
||||||
warnings.push("period_to_defaulted_today_for_management_profile");
|
warnings.push("period_to_defaulted_today_for_management_profile");
|
||||||
|
|
|
||||||
|
|
@ -554,6 +554,31 @@ function hasForecastTaxSignal(text) {
|
||||||
const hasTaxLexeme = /(?:ндс|vat|налог)/iu.test(text);
|
const hasTaxLexeme = /(?:ндс|vat|налог)/iu.test(text);
|
||||||
return hasForecastLexeme && hasTaxLexeme;
|
return hasForecastLexeme && hasTaxLexeme;
|
||||||
}
|
}
|
||||||
|
function hasVatLiabilityConfirmedTaxPeriodSignal(text) {
|
||||||
|
const hasVatLexeme = /(?:ндс|vat)/iu.test(text);
|
||||||
|
if (!hasVatLexeme) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasPaymentCue = /(?:к\s+уплате|надо|нужно|заплатить|уплатить|плат[её]ж|платежку|в\s+налогов|в\s+бюджет|должн[аы]?\s+заплатить)/iu.test(text);
|
||||||
|
if (!hasPaymentCue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasAsOfCue = /(?:на\s+дат|по\s+состоянию|на\s+конец|as\s+of)/iu.test(text);
|
||||||
|
if (hasAsOfCue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasTaxAuthorityCue = /(?:в\s+налогов|в\s+бюджет|декларац|налогов(?:ый|ую)\s+период)/iu.test(text);
|
||||||
|
const hasQuarterCue = /(?:\b[1-4]\s*(?:квартал|кв\.?)\b|квартал|кв\.?)/iu.test(text);
|
||||||
|
const hasZaPeriodCue = /(?:за\s+(?:\d{4}|январ|феврал|март|апрел|май|июн|июл|август|сентябр|октябр|ноябр|декабр|квартал|кв\.?|месяц|год|период))/iu.test(text);
|
||||||
|
const hasExplicitDayDate = /\b(?:\d{1,2}[./-]\d{1,2}[./-](?:\d{2}|\d{4})|(?:19|20)\d{2}[./-]\d{1,2}[./-]\d{1,2})\b/u.test(text);
|
||||||
|
const hasMonthYearNaCue = /(?:на\s+(?:январ|феврал|март|апрел|май|июн|июл|август|сентябр|октябр|ноябр|декабр)\S*\s+(?:19|20)\d{2})/iu.test(text);
|
||||||
|
const hasHowMuchCue = /(?:сколько|скока|скок)/iu.test(text);
|
||||||
|
// "На март 2020" и конкретная дата без налогового контекста чаще означают as-of срез.
|
||||||
|
if (!hasTaxAuthorityCue && !hasZaPeriodCue && !hasQuarterCue && (hasMonthYearNaCue || hasExplicitDayDate)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return hasTaxAuthorityCue || hasZaPeriodCue || hasQuarterCue || (hasHowMuchCue && hasTaxAuthorityCue);
|
||||||
|
}
|
||||||
function hasVatPayableConfirmedSignal(text) {
|
function hasVatPayableConfirmedSignal(text) {
|
||||||
const hasVatLexeme = /(?:ндс|vat)/iu.test(text);
|
const hasVatLexeme = /(?:ндс|vat)/iu.test(text);
|
||||||
if (!hasVatLexeme) {
|
if (!hasVatLexeme) {
|
||||||
|
|
@ -1277,6 +1302,13 @@ function hasAccountNumberAnchor(text) {
|
||||||
}
|
}
|
||||||
function resolveAddressIntent(userMessage) {
|
function resolveAddressIntent(userMessage) {
|
||||||
const text = String(userMessage ?? "").trim().toLowerCase();
|
const text = String(userMessage ?? "").trim().toLowerCase();
|
||||||
|
if (hasVatLiabilityConfirmedTaxPeriodSignal(text)) {
|
||||||
|
return {
|
||||||
|
intent: "vat_liability_confirmed_for_tax_period",
|
||||||
|
confidence: "high",
|
||||||
|
reasons: ["vat_liability_confirmed_tax_period_signal_detected"]
|
||||||
|
};
|
||||||
|
}
|
||||||
if (hasForecastTaxSignal(text)) {
|
if (hasForecastTaxSignal(text)) {
|
||||||
return {
|
return {
|
||||||
intent: "vat_payable_forecast",
|
intent: "vat_payable_forecast",
|
||||||
|
|
|
||||||
|
|
@ -245,7 +245,10 @@ function filterRowsByAccountScope(rows, accountScope) {
|
||||||
async function executeAddressMcpQuery(input) {
|
async function executeAddressMcpQuery(input) {
|
||||||
const endpoint = buildMcpUrl("/api/execute_query");
|
const endpoint = buildMcpUrl("/api/execute_query");
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeout = setTimeout(() => controller.abort(), Math.max(300, config_1.ASSISTANT_MCP_TIMEOUT_MS));
|
const resolvedTimeoutMs = typeof input.timeout_ms === "number" && Number.isFinite(input.timeout_ms)
|
||||||
|
? Math.max(300, Math.trunc(input.timeout_ms))
|
||||||
|
: Math.max(300, config_1.ASSISTANT_MCP_TIMEOUT_MS);
|
||||||
|
const timeout = setTimeout(() => controller.abort(), resolvedTimeoutMs);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(endpoint, {
|
const response = await fetch(endpoint, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
@ -305,7 +308,10 @@ async function executeAddressMcpQuery(input) {
|
||||||
async function executeAddressMcpMetadata(input) {
|
async function executeAddressMcpMetadata(input) {
|
||||||
const endpoint = buildMcpUrl("/api/get_metadata");
|
const endpoint = buildMcpUrl("/api/get_metadata");
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeout = setTimeout(() => controller.abort(), Math.max(300, config_1.ASSISTANT_MCP_TIMEOUT_MS));
|
const resolvedTimeoutMs = typeof input.timeout_ms === "number" && Number.isFinite(input.timeout_ms)
|
||||||
|
? Math.max(300, Math.trunc(input.timeout_ms))
|
||||||
|
: Math.max(300, config_1.ASSISTANT_MCP_TIMEOUT_MS);
|
||||||
|
const timeout = setTimeout(() => controller.abort(), resolvedTimeoutMs);
|
||||||
try {
|
try {
|
||||||
const body = {};
|
const body = {};
|
||||||
if (typeof input.filter === "string" && input.filter.trim().length > 0) {
|
if (typeof input.filter === "string" && input.filter.trim().length > 0) {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ const RESULT_SET_TYPE_BY_INTENT = {
|
||||||
list_payables_counterparties: "counterparty_list",
|
list_payables_counterparties: "counterparty_list",
|
||||||
payables_confirmed_as_of_date: "balance_snapshot",
|
payables_confirmed_as_of_date: "balance_snapshot",
|
||||||
vat_payable_confirmed_as_of_date: "balance_snapshot",
|
vat_payable_confirmed_as_of_date: "balance_snapshot",
|
||||||
|
vat_liability_confirmed_for_tax_period: "balance_snapshot",
|
||||||
receivables_confirmed_as_of_date: "balance_snapshot",
|
receivables_confirmed_as_of_date: "balance_snapshot",
|
||||||
list_receivables_counterparties: "counterparty_list",
|
list_receivables_counterparties: "counterparty_list",
|
||||||
list_contracts_by_counterparty: "contract_list",
|
list_contracts_by_counterparty: "contract_list",
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,15 @@ const ADDRESS_CONFIRMED_PAYABLES_MIN_LIMIT = 200;
|
||||||
const COUNTERPARTY_CATALOG_LOOKUP_LIMIT = 1000;
|
const COUNTERPARTY_CATALOG_LOOKUP_LIMIT = 1000;
|
||||||
const COUNTERPARTY_CATALOG_CACHE_TTL_MS = 120_000;
|
const COUNTERPARTY_CATALOG_CACHE_TTL_MS = 120_000;
|
||||||
const VAT_METADATA_PROBE_LIMIT = 100;
|
const VAT_METADATA_PROBE_LIMIT = 100;
|
||||||
const VAT_SOURCE_PROBE_MAX_OBJECTS = 8;
|
const VAT_SOURCE_PROBE_MAX_OBJECTS = 6;
|
||||||
const VAT_METADATA_PROBE_TYPES = ["РегистрНакопления", "РегистрСведений", "Документ"];
|
const VAT_METADATA_PROBE_TYPES = ["РегистрНакопления", "Документ"];
|
||||||
const VAT_METADATA_PROBE_MASKS = ["ндс", "книгапродаж", "книгапокупок", "счетфактур", "вычет", "восстанов"];
|
const VAT_METADATA_PROBE_MASKS = ["ндс", "книгапродаж", "книгапокупок", "счетфактур"];
|
||||||
|
const VAT_METADATA_PROBE_CONCURRENCY = 4;
|
||||||
|
const VAT_METADATA_PROBE_TIMEOUT_MS = 800;
|
||||||
|
const VAT_METADATA_PROBE_RETRY_TIMEOUT_MS = 1_200;
|
||||||
|
const VAT_OBJECT_PROBE_CONCURRENCY = 4;
|
||||||
|
const VAT_OBJECT_PROBE_TIMEOUT_MS = 800;
|
||||||
|
const VAT_OBJECT_PROBE_FALLBACK_TIMEOUT_MS = 800;
|
||||||
const PARTY_ANCHOR_STOPWORDS = new Set([
|
const PARTY_ANCHOR_STOPWORDS = new Set([
|
||||||
"ооо",
|
"ооо",
|
||||||
"ао",
|
"ао",
|
||||||
|
|
@ -212,6 +218,62 @@ function extractVatMetadataObjects(rows) {
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
function isVatMetadataObject(item) {
|
||||||
|
const source = `${item.fullName} ${item.synonym ?? ""}`.toLowerCase().replace(/ё/g, "е");
|
||||||
|
if (source.includes("ндфл")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(?:ндс|книгапокуп|книгапродаж|счет[\s-]?фактур)/iu.test(source);
|
||||||
|
}
|
||||||
|
function isAbortErrorMessage(error) {
|
||||||
|
const normalized = String(error ?? "").toLowerCase();
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return normalized.includes("aborted") || normalized.includes("abort");
|
||||||
|
}
|
||||||
|
async function mapWithConcurrency(items, concurrency, worker) {
|
||||||
|
if (items.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const boundedConcurrency = Math.max(1, Math.min(Math.trunc(concurrency), items.length));
|
||||||
|
const results = new Array(items.length);
|
||||||
|
let nextIndex = 0;
|
||||||
|
const runners = Array.from({ length: boundedConcurrency }, async () => {
|
||||||
|
while (true) {
|
||||||
|
const currentIndex = nextIndex;
|
||||||
|
nextIndex += 1;
|
||||||
|
if (currentIndex >= items.length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
results[currentIndex] = await worker(items[currentIndex], currentIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await Promise.all(runners);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
async function executeVatMetadataProbeRequest(request) {
|
||||||
|
const firstAttempt = await (0, addressMcpClient_1.executeAddressMcpMetadata)({
|
||||||
|
...request,
|
||||||
|
timeout_ms: VAT_METADATA_PROBE_TIMEOUT_MS
|
||||||
|
});
|
||||||
|
if (!firstAttempt.error || !isAbortErrorMessage(firstAttempt.error)) {
|
||||||
|
return firstAttempt;
|
||||||
|
}
|
||||||
|
const retryLimit = Math.max(20, Math.min(request.limit, Math.trunc(request.limit / 2)));
|
||||||
|
const retryAttempt = await (0, addressMcpClient_1.executeAddressMcpMetadata)({
|
||||||
|
...request,
|
||||||
|
limit: retryLimit,
|
||||||
|
timeout_ms: VAT_METADATA_PROBE_RETRY_TIMEOUT_MS
|
||||||
|
});
|
||||||
|
if (!retryAttempt.error) {
|
||||||
|
return retryAttempt;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...retryAttempt,
|
||||||
|
error: `${firstAttempt.error}; retry: ${retryAttempt.error}`
|
||||||
|
};
|
||||||
|
}
|
||||||
function scoreVatMetadataObject(item) {
|
function scoreVatMetadataObject(item) {
|
||||||
const fullName = item.fullName.toLowerCase();
|
const fullName = item.fullName.toLowerCase();
|
||||||
const synonym = String(item.synonym ?? "").toLowerCase();
|
const synonym = String(item.synonym ?? "").toLowerCase();
|
||||||
|
|
@ -239,8 +301,10 @@ function scoreVatMetadataObject(item) {
|
||||||
}
|
}
|
||||||
return score;
|
return score;
|
||||||
}
|
}
|
||||||
function buildVatObjectProbeQuery(object, asOfExpr) {
|
function buildVatObjectProbeQuery(object, asOfExpr, mode = "latest") {
|
||||||
|
const orderClause = mode === "latest" ? "\nУПОРЯДОЧИТЬ ПО\n Движения.Период УБЫВ" : "";
|
||||||
if (object.objectType === "document") {
|
if (object.objectType === "document") {
|
||||||
|
const documentOrderClause = mode === "latest" ? "\nУПОРЯДОЧИТЬ ПО\n Док.Дата УБЫВ" : "";
|
||||||
return `
|
return `
|
||||||
ВЫБРАТЬ ПЕРВЫЕ 1
|
ВЫБРАТЬ ПЕРВЫЕ 1
|
||||||
Док.Дата КАК Период,
|
Док.Дата КАК Период,
|
||||||
|
|
@ -252,9 +316,8 @@ function buildVatObjectProbeQuery(object, asOfExpr) {
|
||||||
${object.fullName} КАК Док
|
${object.fullName} КАК Док
|
||||||
ГДЕ
|
ГДЕ
|
||||||
Док.Дата <= ${asOfExpr}
|
Док.Дата <= ${asOfExpr}
|
||||||
УПОРЯДОЧИТЬ ПО
|
${documentOrderClause}
|
||||||
Док.Дата УБЫВ
|
`.trim().replace(/\n{3,}/g, "\n\n");
|
||||||
`.trim();
|
|
||||||
}
|
}
|
||||||
return `
|
return `
|
||||||
ВЫБРАТЬ ПЕРВЫЕ 1
|
ВЫБРАТЬ ПЕРВЫЕ 1
|
||||||
|
|
@ -267,9 +330,8 @@ function buildVatObjectProbeQuery(object, asOfExpr) {
|
||||||
${object.fullName} КАК Движения
|
${object.fullName} КАК Движения
|
||||||
ГДЕ
|
ГДЕ
|
||||||
Движения.Период <= ${asOfExpr}
|
Движения.Период <= ${asOfExpr}
|
||||||
УПОРЯДОЧИТЬ ПО
|
${orderClause}
|
||||||
Движения.Период УБЫВ
|
`.trim().replace(/\n{3,}/g, "\n\n");
|
||||||
`.trim();
|
|
||||||
}
|
}
|
||||||
async function probeVatDirectSources(filters) {
|
async function probeVatDirectSources(filters) {
|
||||||
const asOfDate = normalizeIsoDateForQuery(filters.as_of_date) ??
|
const asOfDate = normalizeIsoDateForQuery(filters.as_of_date) ??
|
||||||
|
|
@ -301,17 +363,30 @@ async function probeVatDirectSources(filters) {
|
||||||
name_mask: nameMask,
|
name_mask: nameMask,
|
||||||
limit: VAT_METADATA_PROBE_LIMIT
|
limit: VAT_METADATA_PROBE_LIMIT
|
||||||
})));
|
})));
|
||||||
const metadataResponses = await Promise.all(metadataRequests.map((request) => (0, addressMcpClient_1.executeAddressMcpMetadata)(request)));
|
const metadataResponses = await mapWithConcurrency(metadataRequests, VAT_METADATA_PROBE_CONCURRENCY, (request) => executeVatMetadataProbeRequest(request));
|
||||||
|
const metadataOutcomes = metadataResponses.map((response, index) => ({
|
||||||
|
request: metadataRequests[index],
|
||||||
|
response
|
||||||
|
}));
|
||||||
|
const successfulMetadataByType = new Map();
|
||||||
const metadataErrors = [];
|
const metadataErrors = [];
|
||||||
const metadataObjectsBuffer = [];
|
const metadataObjectsBuffer = [];
|
||||||
for (const [index, response] of metadataResponses.entries()) {
|
for (const { request, response } of metadataOutcomes) {
|
||||||
const request = metadataRequests[index];
|
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
metadataErrors.push(`${request.meta_type}:${request.name_mask}:${response.error}`);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const currentSuccessCount = successfulMetadataByType.get(request.meta_type) ?? 0;
|
||||||
|
successfulMetadataByType.set(request.meta_type, currentSuccessCount + 1);
|
||||||
metadataObjectsBuffer.push(...extractVatMetadataObjects(response.rows));
|
metadataObjectsBuffer.push(...extractVatMetadataObjects(response.rows));
|
||||||
}
|
}
|
||||||
|
for (const { request, response } of metadataOutcomes) {
|
||||||
|
if (response.error) {
|
||||||
|
if (isAbortErrorMessage(response.error) && (successfulMetadataByType.get(request.meta_type) ?? 0) > 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
metadataErrors.push(`${request.meta_type}:${request.name_mask}:${response.error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
const deduplicatedObjects = new Map();
|
const deduplicatedObjects = new Map();
|
||||||
for (const item of metadataObjectsBuffer) {
|
for (const item of metadataObjectsBuffer) {
|
||||||
const existing = deduplicatedObjects.get(item.fullName);
|
const existing = deduplicatedObjects.get(item.fullName);
|
||||||
|
|
@ -326,46 +401,67 @@ async function probeVatDirectSources(filters) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const discoveredMetadataObjects = Array.from(deduplicatedObjects.values()).sort((a, b) => scoreVatMetadataObject(b) - scoreVatMetadataObject(a) || a.fullName.localeCompare(b.fullName, "ru"));
|
const discoveredMetadataObjects = Array.from(deduplicatedObjects.values())
|
||||||
|
.filter((item) => isVatMetadataObject(item))
|
||||||
|
.sort((a, b) => scoreVatMetadataObject(b) - scoreVatMetadataObject(a) || a.fullName.localeCompare(b.fullName, "ru"));
|
||||||
const metadataObjects = discoveredMetadataObjects.slice(0, VAT_SOURCE_PROBE_MAX_OBJECTS);
|
const metadataObjects = discoveredMetadataObjects.slice(0, VAT_SOURCE_PROBE_MAX_OBJECTS);
|
||||||
const probeRows = [];
|
const probeRows = await mapWithConcurrency(metadataObjects, VAT_OBJECT_PROBE_CONCURRENCY, async (object) => {
|
||||||
for (const object of metadataObjects) {
|
let probeResult = await (0, addressMcpClient_1.executeAddressMcpQuery)({
|
||||||
const probeQuery = buildVatObjectProbeQuery(object, asOfExpr);
|
query: buildVatObjectProbeQuery(object, asOfExpr, "latest"),
|
||||||
const probeResult = await (0, addressMcpClient_1.executeAddressMcpQuery)({
|
limit: 1,
|
||||||
query: probeQuery,
|
timeout_ms: VAT_OBJECT_PROBE_TIMEOUT_MS
|
||||||
limit: 1
|
|
||||||
});
|
});
|
||||||
|
let fallbackUsed = false;
|
||||||
if (probeResult.error) {
|
if (probeResult.error) {
|
||||||
probeRows.push({
|
if (isAbortErrorMessage(probeResult.error)) {
|
||||||
fullName: object.fullName,
|
return {
|
||||||
synonym: object.synonym,
|
fullName: object.fullName,
|
||||||
objectType: object.objectType,
|
synonym: object.synonym,
|
||||||
status: "error",
|
objectType: object.objectType,
|
||||||
rowsFetched: probeResult.fetched_rows,
|
status: "error",
|
||||||
error: probeResult.error
|
rowsFetched: probeResult.fetched_rows,
|
||||||
|
error: probeResult.error
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const fallbackResult = await (0, addressMcpClient_1.executeAddressMcpQuery)({
|
||||||
|
query: buildVatObjectProbeQuery(object, asOfExpr, "exists"),
|
||||||
|
limit: 1,
|
||||||
|
timeout_ms: VAT_OBJECT_PROBE_FALLBACK_TIMEOUT_MS
|
||||||
});
|
});
|
||||||
continue;
|
if (!fallbackResult.error) {
|
||||||
|
probeResult = fallbackResult;
|
||||||
|
fallbackUsed = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return {
|
||||||
|
fullName: object.fullName,
|
||||||
|
synonym: object.synonym,
|
||||||
|
objectType: object.objectType,
|
||||||
|
status: "error",
|
||||||
|
rowsFetched: probeResult.fetched_rows,
|
||||||
|
error: `${probeResult.error}; fallback: ${fallbackResult.error}`
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const firstRow = probeResult.raw_rows[0] ?? null;
|
const firstRow = probeResult.raw_rows[0] ?? null;
|
||||||
const lastPeriod = firstRow !== null
|
const lastPeriod = firstRow !== null
|
||||||
? valueAsString(firstRow.Период ?? firstRow.period).trim() ||
|
? valueAsString(firstRow.Период ?? firstRow.period).trim() || null
|
||||||
null
|
|
||||||
: null;
|
: null;
|
||||||
const sampleRegistrator = firstRow !== null
|
const sampleRegistrator = firstRow !== null
|
||||||
? valueAsString(firstRow.Регистратор ??
|
? valueAsString(firstRow.Регистратор ??
|
||||||
firstRow.registrator ??
|
firstRow.registrator ??
|
||||||
firstRow.Registrator).trim() || null
|
firstRow.Registrator).trim() || null
|
||||||
: null;
|
: null;
|
||||||
probeRows.push({
|
return {
|
||||||
fullName: object.fullName,
|
fullName: object.fullName,
|
||||||
synonym: object.synonym,
|
synonym: object.synonym,
|
||||||
objectType: object.objectType,
|
objectType: object.objectType,
|
||||||
status: probeResult.raw_rows.length > 0 ? "ok" : "empty",
|
status: probeResult.raw_rows.length > 0 ? "ok" : "empty",
|
||||||
rowsFetched: probeResult.fetched_rows,
|
rowsFetched: probeResult.fetched_rows,
|
||||||
lastPeriod,
|
lastPeriod: fallbackUsed ? null : lastPeriod,
|
||||||
sampleRegistrator
|
sampleRegistrator
|
||||||
});
|
};
|
||||||
}
|
});
|
||||||
const status = metadataResponses.every((item) => item.error) ? "error" : "ok";
|
const status = metadataResponses.every((item) => item.error) ? "error" : "ok";
|
||||||
const allErrors = [
|
const allErrors = [
|
||||||
...metadataErrors,
|
...metadataErrors,
|
||||||
|
|
@ -909,7 +1005,8 @@ function isConfirmedBalanceIntent(intent) {
|
||||||
intent === "documents_forming_balance" ||
|
intent === "documents_forming_balance" ||
|
||||||
intent === "payables_confirmed_as_of_date" ||
|
intent === "payables_confirmed_as_of_date" ||
|
||||||
intent === "receivables_confirmed_as_of_date" ||
|
intent === "receivables_confirmed_as_of_date" ||
|
||||||
intent === "vat_payable_confirmed_as_of_date");
|
intent === "vat_payable_confirmed_as_of_date" ||
|
||||||
|
intent === "vat_liability_confirmed_for_tax_period");
|
||||||
}
|
}
|
||||||
function resolveAsOfDateBasis(filters) {
|
function resolveAsOfDateBasis(filters) {
|
||||||
const asOfDate = normalizeAnalysisDateHint(filters.as_of_date);
|
const asOfDate = normalizeAnalysisDateHint(filters.as_of_date);
|
||||||
|
|
@ -1566,6 +1663,9 @@ function buildLimitedOffers(input) {
|
||||||
else if (input.intent === "vat_payable_confirmed_as_of_date") {
|
else if (input.intent === "vat_payable_confirmed_as_of_date") {
|
||||||
offers.push("показать подтвержденную сумму НДС к уплате на дату среза по счетам 68*");
|
offers.push("показать подтвержденную сумму НДС к уплате на дату среза по счетам 68*");
|
||||||
}
|
}
|
||||||
|
else if (input.intent === "vat_liability_confirmed_for_tax_period") {
|
||||||
|
offers.push("показать подтвержденный расчет НДС к уплате за налоговый период по книгам продаж/покупок");
|
||||||
|
}
|
||||||
else if (input.intent === "payables_confirmed_as_of_date") {
|
else if (input.intent === "payables_confirmed_as_of_date") {
|
||||||
offers.push("показать подтвержденный реестр открытых обязательств на дату среза по 60/76");
|
offers.push("показать подтвержденный реестр открытых обязательств на дату среза по 60/76");
|
||||||
}
|
}
|
||||||
|
|
@ -1613,7 +1713,8 @@ function buildLimitedIntentSignalLine(input) {
|
||||||
list_payables_counterparties: "Сигнал запроса: нужен ранжированный список кредиторов.",
|
list_payables_counterparties: "Сигнал запроса: нужен ранжированный список кредиторов.",
|
||||||
receivables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез дебиторской задолженности на дату.",
|
receivables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез дебиторской задолженности на дату.",
|
||||||
payables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез обязательств к оплате на дату.",
|
payables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез обязательств к оплате на дату.",
|
||||||
vat_payable_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез НДС к уплате на дату."
|
vat_payable_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез НДС к уплате на дату.",
|
||||||
|
vat_liability_confirmed_for_tax_period: "Сигнал запроса: нужен подтвержденный расчет НДС к уплате за налоговый период."
|
||||||
};
|
};
|
||||||
const byShape = {
|
const byShape = {
|
||||||
AGGREGATE_LOOKUP: "Сигнал запроса: агрегатный вопрос по периоду/срезу.",
|
AGGREGATE_LOOKUP: "Сигнал запроса: агрегатный вопрос по периоду/срезу.",
|
||||||
|
|
@ -1745,7 +1846,9 @@ function buildLimitedExecutionResult(input) {
|
||||||
? "exact_receivables_mode_limited_response"
|
? "exact_receivables_mode_limited_response"
|
||||||
: input.intent.intent === "vat_payable_confirmed_as_of_date"
|
: input.intent.intent === "vat_payable_confirmed_as_of_date"
|
||||||
? "exact_vat_payable_mode_limited_response"
|
? "exact_vat_payable_mode_limited_response"
|
||||||
: null;
|
: input.intent.intent === "vat_liability_confirmed_for_tax_period"
|
||||||
|
? "exact_vat_tax_period_mode_limited_response"
|
||||||
|
: null;
|
||||||
const reasons = exactLimitedReason && !reasonsWithConfirmedFallback.includes(exactLimitedReason)
|
const reasons = exactLimitedReason && !reasonsWithConfirmedFallback.includes(exactLimitedReason)
|
||||||
? [...reasonsWithConfirmedFallback, exactLimitedReason]
|
? [...reasonsWithConfirmedFallback, exactLimitedReason]
|
||||||
: reasonsWithConfirmedFallback;
|
: reasonsWithConfirmedFallback;
|
||||||
|
|
@ -1973,6 +2076,10 @@ class AddressQueryService {
|
||||||
!baseReasons.includes("confirmed_balance_exact_vat_payable_intent")) {
|
!baseReasons.includes("confirmed_balance_exact_vat_payable_intent")) {
|
||||||
baseReasons.push("confirmed_balance_exact_vat_payable_intent");
|
baseReasons.push("confirmed_balance_exact_vat_payable_intent");
|
||||||
}
|
}
|
||||||
|
if (intent.intent === "vat_liability_confirmed_for_tax_period" &&
|
||||||
|
!baseReasons.includes("confirmed_balance_exact_vat_tax_period_intent")) {
|
||||||
|
baseReasons.push("confirmed_balance_exact_vat_tax_period_intent");
|
||||||
|
}
|
||||||
if (requestedResultMode === "confirmed_balance" &&
|
if (requestedResultMode === "confirmed_balance" &&
|
||||||
recipeIntent === "open_items_by_counterparty_or_contract" &&
|
recipeIntent === "open_items_by_counterparty_or_contract" &&
|
||||||
!baseReasons.includes("confirmed_balance_unavailable_fallback_to_heuristic_candidates")) {
|
!baseReasons.includes("confirmed_balance_unavailable_fallback_to_heuristic_candidates")) {
|
||||||
|
|
@ -2899,13 +3006,15 @@ class AddressQueryService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const vatProbeRequired = composeIntent === "vat_payable_confirmed_as_of_date" ||
|
const vatProbeRequired = composeIntent === "vat_payable_confirmed_as_of_date" ||
|
||||||
|
composeIntent === "vat_liability_confirmed_for_tax_period" ||
|
||||||
(composeIntent === "vat_payable_forecast" && shouldProbeVatSourcesForForecast(userMessage));
|
(composeIntent === "vat_payable_forecast" && shouldProbeVatSourcesForForecast(userMessage));
|
||||||
const vatDirectSourceProbe = vatProbeRequired ? await probeVatDirectSources(executionFilters) : null;
|
const vatDirectSourceProbe = vatProbeRequired ? await probeVatDirectSources(executionFilters) : null;
|
||||||
const shouldEmphasizeNumbers = composeIntent === "vat_payable_forecast" ||
|
const shouldEmphasizeNumbers = composeIntent === "vat_payable_forecast" ||
|
||||||
composeIntent === "vat_payable_confirmed_as_of_date" ||
|
composeIntent === "vat_payable_confirmed_as_of_date" ||
|
||||||
|
composeIntent === "vat_liability_confirmed_for_tax_period" ||
|
||||||
composeIntent === "payables_confirmed_as_of_date" ||
|
composeIntent === "payables_confirmed_as_of_date" ||
|
||||||
composeIntent === "receivables_confirmed_as_of_date";
|
composeIntent === "receivables_confirmed_as_of_date";
|
||||||
const shouldUseRubCurrency = composeIntent === "vat_payable_forecast";
|
const shouldUseRubCurrency = composeIntent === "vat_payable_forecast" || composeIntent === "vat_liability_confirmed_for_tax_period";
|
||||||
const factual = (0, composeStage_1.composeFactualReply)(composeIntent, filteredRows, composeOptionsFromFilters(executionFilters, {
|
const factual = (0, composeStage_1.composeFactualReply)(composeIntent, filteredRows, composeOptionsFromFilters(executionFilters, {
|
||||||
vatDirectSourceProbe,
|
vatDirectSourceProbe,
|
||||||
emphasizeNumbers: shouldEmphasizeNumbers,
|
emphasizeNumbers: shouldEmphasizeNumbers,
|
||||||
|
|
@ -2969,13 +3078,17 @@ class AddressQueryService {
|
||||||
}
|
}
|
||||||
const exactConfirmedIntent = (intent.intent === "payables_confirmed_as_of_date" && composeIntent === "payables_confirmed_as_of_date") ||
|
const exactConfirmedIntent = (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") ||
|
(intent.intent === "receivables_confirmed_as_of_date" && composeIntent === "receivables_confirmed_as_of_date") ||
|
||||||
(intent.intent === "vat_payable_confirmed_as_of_date" && composeIntent === "vat_payable_confirmed_as_of_date");
|
(intent.intent === "vat_payable_confirmed_as_of_date" && composeIntent === "vat_payable_confirmed_as_of_date") ||
|
||||||
|
(intent.intent === "vat_liability_confirmed_for_tax_period" &&
|
||||||
|
composeIntent === "vat_liability_confirmed_for_tax_period");
|
||||||
if (exactConfirmedIntent && factualResultSemantics.balance_confirmed !== true) {
|
if (exactConfirmedIntent && factualResultSemantics.balance_confirmed !== true) {
|
||||||
const exactModeName = intent.intent === "payables_confirmed_as_of_date"
|
const exactModeName = intent.intent === "payables_confirmed_as_of_date"
|
||||||
? "payables"
|
? "payables"
|
||||||
: intent.intent === "receivables_confirmed_as_of_date"
|
: intent.intent === "receivables_confirmed_as_of_date"
|
||||||
? "receivables"
|
? "receivables"
|
||||||
: "vat_payable";
|
: intent.intent === "vat_liability_confirmed_for_tax_period"
|
||||||
|
? "vat_tax_period"
|
||||||
|
: "vat_payable";
|
||||||
return buildLimitedExecutionResult({
|
return buildLimitedExecutionResult({
|
||||||
mode,
|
mode,
|
||||||
shape,
|
shape,
|
||||||
|
|
@ -3002,7 +3115,9 @@ class AddressQueryService {
|
||||||
reasonText: `exact ${exactModeName} 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: intent.intent === "vat_payable_confirmed_as_of_date"
|
nextStep: intent.intent === "vat_payable_confirmed_as_of_date"
|
||||||
? "specify as_of_date/organization or provide VAT settlement registers to prove exact VAT payable balance"
|
? "specify as_of_date/organization or provide VAT settlement registers to prove exact VAT payable balance"
|
||||||
: "specify as_of_date/counterparty or enable detailed settlement registers for exact confirmed balance",
|
: intent.intent === "vat_liability_confirmed_for_tax_period"
|
||||||
|
? "specify tax period boundaries and ensure purchase/sales VAT books are available via MCP"
|
||||||
|
: "specify as_of_date/counterparty or enable detailed settlement registers for exact confirmed balance",
|
||||||
limitations: [`exact_${exactModeName}_mode_unconfirmed_output_blocked`],
|
limitations: [`exact_${exactModeName}_mode_unconfirmed_output_blocked`],
|
||||||
reasons: [...baseReasons, `exact_${exactModeName}_mode_unconfirmed_output_blocked`],
|
reasons: [...baseReasons, `exact_${exactModeName}_mode_unconfirmed_output_blocked`],
|
||||||
capabilityAudit,
|
capabilityAudit,
|
||||||
|
|
|
||||||
|
|
@ -480,6 +480,29 @@ __WHERE_CLAUSE__
|
||||||
УПОРЯДОЧИТЬ ПО
|
УПОРЯДОЧИТЬ ПО
|
||||||
Регистратор
|
Регистратор
|
||||||
`;
|
`;
|
||||||
|
const VAT_LIABILITY_CONFIRMED_TAX_PERIOD_QUERY_TEMPLATE = `
|
||||||
|
ВЫБРАТЬ
|
||||||
|
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
|
||||||
|
"VAT_BOOK_SALES" КАК Регистратор,
|
||||||
|
"68.02" КАК СчетДт,
|
||||||
|
"" КАК СчетКт,
|
||||||
|
СУММА(ЕСТЬNULL(Движения.НДС, 0)) КАК Сумма
|
||||||
|
ИЗ
|
||||||
|
РегистрНакопления.НДСЗаписиКнигиПродаж КАК Движения
|
||||||
|
__WHERE_CLAUSE__
|
||||||
|
ОБЪЕДИНИТЬ ВСЕ
|
||||||
|
ВЫБРАТЬ
|
||||||
|
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
|
||||||
|
"VAT_BOOK_PURCHASES" КАК Регистратор,
|
||||||
|
"19" КАК СчетДт,
|
||||||
|
"" КАК СчетКт,
|
||||||
|
СУММА(ЕСТЬNULL(Движения.НДС, 0)) КАК Сумма
|
||||||
|
ИЗ
|
||||||
|
РегистрНакопления.НДСЗаписиКнигиПокупок КАК Движения
|
||||||
|
__WHERE_CLAUSE__
|
||||||
|
УПОРЯДОЧИТЬ ПО
|
||||||
|
Регистратор
|
||||||
|
`;
|
||||||
const BASE_RECIPES = [
|
const BASE_RECIPES = [
|
||||||
{
|
{
|
||||||
recipe_id: "address_period_coverage_profile_v1",
|
recipe_id: "address_period_coverage_profile_v1",
|
||||||
|
|
@ -582,6 +605,16 @@ const BASE_RECIPES = [
|
||||||
account_scope_mode: "strict",
|
account_scope_mode: "strict",
|
||||||
query_template: "vat_payable_confirmed_as_of_balance_profile"
|
query_template: "vat_payable_confirmed_as_of_balance_profile"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
recipe_id: "address_vat_liability_confirmed_tax_period_v1",
|
||||||
|
intent: "vat_liability_confirmed_for_tax_period",
|
||||||
|
purpose: "Build confirmed VAT liability for tax period from purchase/sales VAT books",
|
||||||
|
required_filters: ["period_from", "period_to"],
|
||||||
|
optional_filters: ["organization"],
|
||||||
|
default_limit: 32,
|
||||||
|
account_scope_mode: "preferred",
|
||||||
|
query_template: "vat_liability_confirmed_tax_period_profile"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
recipe_id: "address_contracts_by_counterparty_v1",
|
recipe_id: "address_contracts_by_counterparty_v1",
|
||||||
intent: "list_contracts_by_counterparty",
|
intent: "list_contracts_by_counterparty",
|
||||||
|
|
@ -887,6 +920,7 @@ function maxLimitForIntent(intent) {
|
||||||
intent === "supplier_payouts_profile" ||
|
intent === "supplier_payouts_profile" ||
|
||||||
intent === "contract_usage_and_value" ||
|
intent === "contract_usage_and_value" ||
|
||||||
intent === "vat_payable_forecast" ||
|
intent === "vat_payable_forecast" ||
|
||||||
|
intent === "vat_liability_confirmed_for_tax_period" ||
|
||||||
intent === "list_contracts_by_counterparty" ||
|
intent === "list_contracts_by_counterparty" ||
|
||||||
intent === "list_documents_by_counterparty" ||
|
intent === "list_documents_by_counterparty" ||
|
||||||
intent === "bank_operations_by_counterparty" ||
|
intent === "bank_operations_by_counterparty" ||
|
||||||
|
|
@ -926,7 +960,8 @@ function buildAddressRecipePlan(recipe, filters) {
|
||||||
recipe.query_template === "document_section_profile" ||
|
recipe.query_template === "document_section_profile" ||
|
||||||
recipe.query_template === "counterparty_roles_profile" ||
|
recipe.query_template === "counterparty_roles_profile" ||
|
||||||
recipe.query_template === "contract_usage_profile" ||
|
recipe.query_template === "contract_usage_profile" ||
|
||||||
recipe.query_template === "vat_payable_forecast_profile";
|
recipe.query_template === "vat_payable_forecast_profile" ||
|
||||||
|
recipe.query_template === "vat_liability_confirmed_tax_period_profile";
|
||||||
const baseLimit = typeof filters.limit === "number" && Number.isFinite(filters.limit)
|
const baseLimit = typeof filters.limit === "number" && Number.isFinite(filters.limit)
|
||||||
? Math.max(1, Math.min(maxLimit, Math.trunc(filters.limit)))
|
? Math.max(1, Math.min(maxLimit, Math.trunc(filters.limit)))
|
||||||
: recipe.default_limit;
|
: recipe.default_limit;
|
||||||
|
|
@ -993,45 +1028,29 @@ function buildAddressRecipePlan(recipe, filters) {
|
||||||
.replaceAll("__VAT68_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", config_1.VAT_PAYABLE_68_PREFIXES))
|
.replaceAll("__VAT68_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", config_1.VAT_PAYABLE_68_PREFIXES))
|
||||||
.replaceAll("__VAT19_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", config_1.VAT_PAYABLE_19_PREFIXES))
|
.replaceAll("__VAT19_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", config_1.VAT_PAYABLE_19_PREFIXES))
|
||||||
.replaceAll("__VAT19_KT_MATCH__", buildAccountPrefixPredicate("Движения.СчетКт", config_1.VAT_PAYABLE_19_PREFIXES))
|
.replaceAll("__VAT19_KT_MATCH__", buildAccountPrefixPredicate("Движения.СчетКт", config_1.VAT_PAYABLE_19_PREFIXES))
|
||||||
: recipe.query_template === "vat_payable_confirmed_as_of_balance_profile"
|
: recipe.query_template === "vat_liability_confirmed_tax_period_profile"
|
||||||
? (() => {
|
? VAT_LIABILITY_CONFIRMED_TAX_PERIOD_QUERY_TEMPLATE.replaceAll("__WHERE_CLAUSE__", buildManagementWhereClause(filters, "Движения.Период"))
|
||||||
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
: recipe.query_template === "vat_payable_confirmed_as_of_balance_profile"
|
||||||
? toDateTimeExpr(filters.as_of_date, true)
|
? (() => {
|
||||||
: null) ??
|
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
? toDateTimeExpr(filters.as_of_date, true)
|
||||||
? toDateTimeExpr(filters.period_to, true)
|
|
||||||
: null) ??
|
: null) ??
|
||||||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
||||||
? toDateTimeExpr(filters.period_from, true)
|
? toDateTimeExpr(filters.period_to, true)
|
||||||
: null) ??
|
|
||||||
"ТЕКУЩАЯДАТА()";
|
|
||||||
return VAT_PAYABLE_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
|
||||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
|
||||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
|
||||||
.replaceAll("__VAT_PAYABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", config_1.VAT_PAYABLE_68_PREFIXES))
|
|
||||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
|
||||||
})()
|
|
||||||
: recipe.query_template === "contracts_by_counterparty_profile"
|
|
||||||
? CONTRACTS_BY_COUNTERPARTY_QUERY_TEMPLATE.replaceAll("__LIMIT__", String(resolvedLimit))
|
|
||||||
: recipe.query_template === "payables_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) ??
|
: null) ??
|
||||||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
||||||
? toDateTimeExpr(filters.period_to, true)
|
? toDateTimeExpr(filters.period_from, true)
|
||||||
: null) ??
|
: null) ??
|
||||||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
"ТЕКУЩАЯДАТА()";
|
||||||
? toDateTimeExpr(filters.period_from, true)
|
return VAT_PAYABLE_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||||
: null) ??
|
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||||
"ТЕКУЩАЯДАТА()";
|
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||||
return PAYABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
.replaceAll("__VAT_PAYABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", config_1.VAT_PAYABLE_68_PREFIXES))
|
||||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
})()
|
||||||
.replaceAll("__PAYABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"]))
|
: recipe.query_template === "contracts_by_counterparty_profile"
|
||||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
? CONTRACTS_BY_COUNTERPARTY_QUERY_TEMPLATE.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||||
})()
|
: recipe.query_template === "payables_confirmed_as_of_balance_profile"
|
||||||
: recipe.query_template === "receivables_confirmed_as_of_balance_profile"
|
|
||||||
? (() => {
|
? (() => {
|
||||||
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||||
? toDateTimeExpr(filters.as_of_date, true)
|
? toDateTimeExpr(filters.as_of_date, true)
|
||||||
|
|
@ -1043,23 +1062,41 @@ function buildAddressRecipePlan(recipe, filters) {
|
||||||
? toDateTimeExpr(filters.period_from, true)
|
? toDateTimeExpr(filters.period_from, true)
|
||||||
: null) ??
|
: null) ??
|
||||||
"ТЕКУЩАЯДАТА()";
|
"ТЕКУЩАЯДАТА()";
|
||||||
return RECEIVABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
return PAYABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||||
.replaceAll("__RECEIVABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["62", "76"]))
|
.replaceAll("__PAYABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"]))
|
||||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||||
})()
|
})()
|
||||||
: MOVEMENTS_QUERY_TEMPLATE
|
: recipe.query_template === "receivables_confirmed_as_of_balance_profile"
|
||||||
.replace("__LIMIT__", String(resolvedLimit))
|
? (() => {
|
||||||
.replace("__WHERE_CLAUSE__", (() => {
|
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||||
const extraConditions = [];
|
? toDateTimeExpr(filters.as_of_date, true)
|
||||||
const accountCondition = buildMovementAccountCondition(filters);
|
: null) ??
|
||||||
if (accountCondition) {
|
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
||||||
extraConditions.push(accountCondition);
|
? toDateTimeExpr(filters.period_to, true)
|
||||||
}
|
: null) ??
|
||||||
return buildWhereClause(filters, "Движения.Период", extraConditions);
|
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
||||||
})())
|
? toDateTimeExpr(filters.period_from, true)
|
||||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
: null) ??
|
||||||
|
"ТЕКУЩАЯДАТА()";
|
||||||
|
return RECEIVABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||||
|
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||||
|
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||||
|
.replaceAll("__RECEIVABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["62", "76"]))
|
||||||
|
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||||
|
})()
|
||||||
|
: MOVEMENTS_QUERY_TEMPLATE
|
||||||
|
.replace("__LIMIT__", String(resolvedLimit))
|
||||||
|
.replace("__WHERE_CLAUSE__", (() => {
|
||||||
|
const extraConditions = [];
|
||||||
|
const accountCondition = buildMovementAccountCondition(filters);
|
||||||
|
if (accountCondition) {
|
||||||
|
extraConditions.push(accountCondition);
|
||||||
|
}
|
||||||
|
return buildWhereClause(filters, "Движения.Период", extraConditions);
|
||||||
|
})())
|
||||||
|
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||||
return {
|
return {
|
||||||
recipe,
|
recipe,
|
||||||
query,
|
query,
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,9 @@ function emphasizeNumericTokens(line) {
|
||||||
if (!line) {
|
if (!line) {
|
||||||
return line;
|
return line;
|
||||||
}
|
}
|
||||||
|
const isDigit = (char) => /\d/.test(char);
|
||||||
|
const isLetter = (char) => /[A-Za-zА-Яа-яЁё]/.test(char);
|
||||||
|
const dateLikePunctuation = new Set([".", "-", "/", ":"]);
|
||||||
const chunks = line.split(/(`[^`]*`)/g);
|
const chunks = line.split(/(`[^`]*`)/g);
|
||||||
return chunks
|
return chunks
|
||||||
.map((chunk, index) => {
|
.map((chunk, index) => {
|
||||||
|
|
@ -123,9 +126,23 @@ function emphasizeNumericTokens(line) {
|
||||||
return chunk.replace(/\b-?(?:\d{1,3}(?:[.\s]\d{3})+|\d+)(?:[.,]\d+)?\b/g, (match, offset, source) => {
|
return chunk.replace(/\b-?(?:\d{1,3}(?:[.\s]\d{3})+|\d+)(?:[.,]\d+)?\b/g, (match, offset, source) => {
|
||||||
const before = offset > 0 ? source[offset - 1] : "";
|
const before = offset > 0 ? source[offset - 1] : "";
|
||||||
const after = offset + match.length < source.length ? source[offset + match.length] : "";
|
const after = offset + match.length < source.length ? source[offset + match.length] : "";
|
||||||
|
const before2 = offset > 1 ? source[offset - 2] : "";
|
||||||
|
const after2 = offset + match.length + 1 < source.length ? source[offset + match.length + 1] : "";
|
||||||
if (before === "*" || after === "*") {
|
if (before === "*" || after === "*") {
|
||||||
return match;
|
return match;
|
||||||
}
|
}
|
||||||
|
if (isLetter(before) || isLetter(after)) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
if (offset === 0 && (after === "." || after === ")")) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
if (dateLikePunctuation.has(before) && isDigit(before2)) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
if (dateLikePunctuation.has(after) && isDigit(after2)) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
return `**${match}**`;
|
return `**${match}**`;
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
@ -1997,11 +2014,24 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||||
];
|
];
|
||||||
if (vatProbe && vatProbe.status === "ok") {
|
if (vatProbe && vatProbe.status === "ok") {
|
||||||
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
|
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
|
||||||
lines.push("", "Покрытие VAT-источников через MCP:", `- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`, `- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`, `- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`);
|
const statusRank = (status) => status === "ok" ? 0 : status === "empty" ? 1 : 2;
|
||||||
if (vatProbe.probedSources.length > 0) {
|
const orderedProbeRows = [...vatProbe.probedSources].sort((a, b) => statusRank(a.status) - statusRank(b.status) ||
|
||||||
lines.push(...vatProbe.probedSources.slice(0, 6).map((item, index) => {
|
a.fullName.localeCompare(b.fullName, "ru"));
|
||||||
|
const nonErrorProbeRows = orderedProbeRows.filter((item) => item.status !== "error");
|
||||||
|
const visibleProbeRows = (nonErrorProbeRows.length > 0 ? nonErrorProbeRows : orderedProbeRows).slice(0, 6);
|
||||||
|
const erroredSources = vatProbe.probedSources.filter((item) => item.status === "error").length;
|
||||||
|
lines.push("", "Покрытие VAT-источников через MCP:", `- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`, `- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`, `- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`, `- Источников с ошибкой запроса: ${formatNumberWithDots(erroredSources)}.`);
|
||||||
|
if (visibleProbeRows.length > 0) {
|
||||||
|
lines.push(...visibleProbeRows.map((item, index) => {
|
||||||
const name = item.synonym ? `${item.fullName} (${item.synonym})` : item.fullName;
|
const name = item.synonym ? `${item.fullName} (${item.synonym})` : item.fullName;
|
||||||
return `${index + 1}. ${name} | ${formatVatProbeStatusRu(item.status)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}`;
|
const extra = item.status === "ok"
|
||||||
|
? item.lastPeriod
|
||||||
|
? ` | последнее движение: ${item.lastPeriod}`
|
||||||
|
: ""
|
||||||
|
: item.status === "error" && item.error
|
||||||
|
? ` | ошибка: ${item.error}`
|
||||||
|
: "";
|
||||||
|
return `${index + 1}. ${name} | ${formatVatProbeStatusRu(item.status)}${extra}`;
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
if (vatProbe.errors.length > 0) {
|
if (vatProbe.errors.length > 0) {
|
||||||
|
|
@ -2044,6 +2074,61 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||||
text: joinLines(lines)
|
text: joinLines(lines)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (intent === "vat_liability_confirmed_for_tax_period") {
|
||||||
|
const rowsByMarker = new Map();
|
||||||
|
for (const row of rows) {
|
||||||
|
const marker = String(row.registrator ?? "").trim().toUpperCase();
|
||||||
|
if (!marker) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const nextValue = (rowsByMarker.get(marker) ?? 0) + (row.amount ?? 0);
|
||||||
|
rowsByMarker.set(marker, nextValue);
|
||||||
|
}
|
||||||
|
const salesVat = rowsByMarker.get("VAT_BOOK_SALES") ?? 0;
|
||||||
|
const purchaseVat = rowsByMarker.get("VAT_BOOK_PURCHASES") ?? 0;
|
||||||
|
const netVat = salesVat - purchaseVat;
|
||||||
|
const vatToPay = Math.max(0, netVat);
|
||||||
|
const carryoverOrOverpayment = Math.max(0, -netVat);
|
||||||
|
const periodWindowLabel = options.periodFrom && options.periodTo ? `${formatDateRu(options.periodFrom)}..${formatDateRu(options.periodTo)}` : null;
|
||||||
|
const formatConfirmedMoney = (value) => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
|
||||||
|
const vatProbe = options.vatDirectSourceProbe ?? null;
|
||||||
|
const lines = [
|
||||||
|
`Собран подтвержденный расчет НДС к уплате за налоговый период: ${formatConfirmedMoney(vatToPay)}.`,
|
||||||
|
`Налоговый период расчета: ${periodWindowLabel ?? "не задан (нужен явный период)"}.`,
|
||||||
|
`Потенциальный перенос/переплата: ${formatConfirmedMoney(carryoverOrOverpayment)}.`,
|
||||||
|
"Режим результата: подтвержденный расчет по регистрам книг продаж/покупок (tax-period mode, без surrogate-формулы 68/19).",
|
||||||
|
"",
|
||||||
|
"База расчета:",
|
||||||
|
`- Строк агрегата: ${formatNumberWithDots(rows.length)}.`,
|
||||||
|
`- НДС по книге продаж: ${formatConfirmedMoney(salesVat)}.`,
|
||||||
|
`- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`,
|
||||||
|
`- Нетто НДС (книга продаж - книга покупок): ${formatConfirmedMoney(netVat)}.`
|
||||||
|
];
|
||||||
|
if (vatProbe && vatProbe.status === "ok") {
|
||||||
|
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
|
||||||
|
const erroredSources = vatProbe.probedSources.filter((item) => item.status === "error").length;
|
||||||
|
lines.push("", "Покрытие VAT-источников через MCP:", `- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`, `- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`, `- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`, `- Источников с ошибкой запроса: ${formatNumberWithDots(erroredSources)}.`);
|
||||||
|
if (vatProbe.errors.length > 0) {
|
||||||
|
lines.push(`- Ограничения probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
||||||
|
}
|
||||||
|
lines.push("- Сумма расчета выше получена по книгам продаж/покупок; probe использован для контроля полноты VAT-источников.");
|
||||||
|
}
|
||||||
|
else if (vatProbe && vatProbe.status === "error") {
|
||||||
|
lines.push("", "Покрытие VAT-источников через MCP: probe завершился ошибкой, проверьте доступность регистров книг продаж/покупок.");
|
||||||
|
}
|
||||||
|
if (rows.length === 0) {
|
||||||
|
lines.push("", "За выбранный налоговый период не найдены строки книг продаж/покупок, поэтому подтвержденная сумма к уплате равна 0.");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
responseType: "FACTUAL_SUMMARY",
|
||||||
|
text: joinLines(lines),
|
||||||
|
semantics: {
|
||||||
|
result_mode: "confirmed_balance",
|
||||||
|
evidence_strength: "strong",
|
||||||
|
balance_confirmed: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
if (intent === "vat_payable_confirmed_as_of_date") {
|
if (intent === "vat_payable_confirmed_as_of_date") {
|
||||||
const asOfDate = resolvePayablesAsOfDate(options);
|
const asOfDate = resolvePayablesAsOfDate(options);
|
||||||
const confirmedRows = rows.filter((row) => {
|
const confirmedRows = rows.filter((row) => {
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,9 @@ function hasVatCue(text) {
|
||||||
function hasVatForecastCue(text) {
|
function hasVatForecastCue(text) {
|
||||||
return /(?:прогноз|forecast|прикин|оцен|план)/iu.test(String(text ?? ""));
|
return /(?:прогноз|forecast|прикин|оцен|план)/iu.test(String(text ?? ""));
|
||||||
}
|
}
|
||||||
|
function hasVatTaxPaymentCue(text) {
|
||||||
|
return /(?:к\s+уплате|надо|нужно|заплатить|уплатить|плат[её]ж|в\s+налогов|в\s+бюджет)/iu.test(String(text ?? ""));
|
||||||
|
}
|
||||||
function hasDocumentSignal(text) {
|
function hasDocumentSignal(text) {
|
||||||
return /(?:док(?:и|умент|ументы|ументов|ументами)|docs?|documents?|doki|docy|doci)/iu.test(String(text ?? ""));
|
return /(?:док(?:и|умент|ументы|ументов|ументами)|docs?|documents?|doki|docy|doci)/iu.test(String(text ?? ""));
|
||||||
}
|
}
|
||||||
|
|
@ -432,7 +435,10 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
const hasExplicitPeriodInMessage = hasExplicitPeriodLiteral(userMessage);
|
const hasExplicitPeriodInMessage = hasExplicitPeriodLiteral(userMessage);
|
||||||
const currentHasPeriod = hasExplicitPeriodWindow(merged);
|
const currentHasPeriod = hasExplicitPeriodWindow(merged);
|
||||||
const previousHasPeriod = hasExplicitPeriodWindow(previous);
|
const previousHasPeriod = hasExplicitPeriodWindow(previous);
|
||||||
if (intent === "vat_payable_forecast" && previousHasPeriod && hasFollowupSignal && !hasExplicitPeriodInMessage) {
|
if ((intent === "vat_payable_forecast" || intent === "vat_liability_confirmed_for_tax_period") &&
|
||||||
|
previousHasPeriod &&
|
||||||
|
hasFollowupSignal &&
|
||||||
|
!hasExplicitPeriodInMessage) {
|
||||||
const currentPeriodFrom = toNonEmptyString(merged.period_from);
|
const currentPeriodFrom = toNonEmptyString(merged.period_from);
|
||||||
const currentPeriodTo = toNonEmptyString(merged.period_to);
|
const currentPeriodTo = toNonEmptyString(merged.period_to);
|
||||||
const todayIso = new Date().toISOString().slice(0, 10);
|
const todayIso = new Date().toISOString().slice(0, 10);
|
||||||
|
|
@ -465,6 +471,7 @@ function resolveMissingRequiredFilters(intent, filters) {
|
||||||
payables_confirmed_as_of_date: ["as_of_date"],
|
payables_confirmed_as_of_date: ["as_of_date"],
|
||||||
receivables_confirmed_as_of_date: ["as_of_date"],
|
receivables_confirmed_as_of_date: ["as_of_date"],
|
||||||
vat_payable_confirmed_as_of_date: ["as_of_date"],
|
vat_payable_confirmed_as_of_date: ["as_of_date"],
|
||||||
|
vat_liability_confirmed_for_tax_period: ["period_from", "period_to"],
|
||||||
list_documents_by_counterparty: ["counterparty"],
|
list_documents_by_counterparty: ["counterparty"],
|
||||||
bank_operations_by_counterparty: ["counterparty"],
|
bank_operations_by_counterparty: ["counterparty"],
|
||||||
list_contracts_by_counterparty: ["counterparty"],
|
list_contracts_by_counterparty: ["counterparty"],
|
||||||
|
|
@ -497,9 +504,11 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
||||||
const hasAnyPartyAnchor = hasPreviousContract || hasPreviousCounterparty;
|
const hasAnyPartyAnchor = hasPreviousContract || hasPreviousCounterparty;
|
||||||
const isVatFollowup = hasVatCue(normalizedMessage);
|
const isVatFollowup = hasVatCue(normalizedMessage);
|
||||||
if (detectedIntent.intent === "unknown" && isVatFollowup) {
|
if (detectedIntent.intent === "unknown" && isVatFollowup) {
|
||||||
const vatIntent = hasVatForecastCue(normalizedMessage)
|
const vatIntent = hasVatTaxPaymentCue(normalizedMessage)
|
||||||
? "vat_payable_forecast"
|
? "vat_liability_confirmed_for_tax_period"
|
||||||
: "vat_payable_confirmed_as_of_date";
|
: hasVatForecastCue(normalizedMessage)
|
||||||
|
? "vat_payable_forecast"
|
||||||
|
: "vat_payable_confirmed_as_of_date";
|
||||||
return {
|
return {
|
||||||
intent: vatIntent,
|
intent: vatIntent,
|
||||||
confidence: "low",
|
confidence: "low",
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,8 @@ function inferAggregationProfile(intent, shape) {
|
||||||
intent === "documents_forming_balance" ||
|
intent === "documents_forming_balance" ||
|
||||||
intent === "payables_confirmed_as_of_date" ||
|
intent === "payables_confirmed_as_of_date" ||
|
||||||
intent === "receivables_confirmed_as_of_date" ||
|
intent === "receivables_confirmed_as_of_date" ||
|
||||||
intent === "vat_payable_confirmed_as_of_date") {
|
intent === "vat_payable_confirmed_as_of_date" ||
|
||||||
|
intent === "vat_liability_confirmed_for_tax_period") {
|
||||||
return "balance_snapshot";
|
return "balance_snapshot";
|
||||||
}
|
}
|
||||||
if (intent === "open_items_by_counterparty_or_contract" ||
|
if (intent === "open_items_by_counterparty_or_contract" ||
|
||||||
|
|
|
||||||
|
|
@ -3835,6 +3835,7 @@ const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([
|
||||||
"contract_usage_overview",
|
"contract_usage_overview",
|
||||||
"contract_usage_and_value",
|
"contract_usage_and_value",
|
||||||
"vat_payable_forecast",
|
"vat_payable_forecast",
|
||||||
|
"vat_liability_confirmed_for_tax_period",
|
||||||
"vat_payable_confirmed_as_of_date"
|
"vat_payable_confirmed_as_of_date"
|
||||||
]);
|
]);
|
||||||
function resolveAssistantOrchestrationDecision(input) {
|
function resolveAssistantOrchestrationDecision(input) {
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,8 @@ const COMPUTE_EXACT_INTENTS = new Set<AddressIntent>([
|
||||||
"documents_forming_balance",
|
"documents_forming_balance",
|
||||||
"payables_confirmed_as_of_date",
|
"payables_confirmed_as_of_date",
|
||||||
"receivables_confirmed_as_of_date",
|
"receivables_confirmed_as_of_date",
|
||||||
"vat_payable_confirmed_as_of_date"
|
"vat_payable_confirmed_as_of_date",
|
||||||
|
"vat_liability_confirmed_for_tax_period"
|
||||||
]);
|
]);
|
||||||
const NAVIGATION_INTENTS = new Set<AddressIntent>([
|
const NAVIGATION_INTENTS = new Set<AddressIntent>([
|
||||||
"list_documents_by_counterparty",
|
"list_documents_by_counterparty",
|
||||||
|
|
@ -66,6 +67,9 @@ function defaultCapabilityId(intent: AddressIntent): string {
|
||||||
if (intent === "vat_payable_confirmed_as_of_date") {
|
if (intent === "vat_payable_confirmed_as_of_date") {
|
||||||
return "confirmed_vat_payable_as_of_date";
|
return "confirmed_vat_payable_as_of_date";
|
||||||
}
|
}
|
||||||
|
if (intent === "vat_liability_confirmed_for_tax_period") {
|
||||||
|
return "confirmed_vat_liability_for_tax_period";
|
||||||
|
}
|
||||||
if (intent === "list_payables_counterparties") {
|
if (intent === "list_payables_counterparties") {
|
||||||
return "payables_candidates_list";
|
return "payables_candidates_list";
|
||||||
}
|
}
|
||||||
|
|
@ -110,6 +114,14 @@ function resolveCapabilityEnabled(intent: AddressIntent): { enabled: boolean; re
|
||||||
: "vat_payable_confirmed_route_disabled_by_flag"
|
: "vat_payable_confirmed_route_disabled_by_flag"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (intent === "vat_liability_confirmed_for_tax_period") {
|
||||||
|
return {
|
||||||
|
enabled: FEATURE_ASSISTANT_ROUTE_BALANCE_EXACT_V1,
|
||||||
|
reason: FEATURE_ASSISTANT_ROUTE_BALANCE_EXACT_V1
|
||||||
|
? "vat_liability_confirmed_tax_period_route_enabled"
|
||||||
|
: "vat_liability_confirmed_tax_period_route_disabled_by_flag"
|
||||||
|
};
|
||||||
|
}
|
||||||
if (intent === "list_payables_counterparties") {
|
if (intent === "list_payables_counterparties") {
|
||||||
return {
|
return {
|
||||||
enabled: FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1,
|
enabled: FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1,
|
||||||
|
|
|
||||||
|
|
@ -932,6 +932,9 @@ function requiredFiltersByIntent(intent: AddressIntent): Array<keyof AddressFilt
|
||||||
if (intent === "vat_payable_confirmed_as_of_date") {
|
if (intent === "vat_payable_confirmed_as_of_date") {
|
||||||
return ["as_of_date"];
|
return ["as_of_date"];
|
||||||
}
|
}
|
||||||
|
if (intent === "vat_liability_confirmed_for_tax_period") {
|
||||||
|
return ["period_from", "period_to"];
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
intent === "list_documents_by_counterparty" ||
|
intent === "list_documents_by_counterparty" ||
|
||||||
intent === "bank_operations_by_counterparty" ||
|
intent === "bank_operations_by_counterparty" ||
|
||||||
|
|
@ -1102,6 +1105,17 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (intent === "vat_liability_confirmed_for_tax_period" && !periodRange.period_from && !periodRange.period_to) {
|
||||||
|
const periodToForQuarter = filters.period_to ?? vatAsOfDate ?? null;
|
||||||
|
if (periodToForQuarter) {
|
||||||
|
const quarterWindow = deriveQuarterWindowForDate(periodToForQuarter);
|
||||||
|
if (quarterWindow) {
|
||||||
|
filters.period_from = quarterWindow.period_from;
|
||||||
|
filters.period_to = quarterWindow.period_to;
|
||||||
|
warnings.push("period_derived_from_tax_quarter_for_confirmed_vat_liability");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isManagementProfileIntent && !filters.period_to && !filters.as_of_date) {
|
if (isManagementProfileIntent && !filters.period_to && !filters.as_of_date) {
|
||||||
filters.period_to = new Date().toISOString().slice(0, 10);
|
filters.period_to = new Date().toISOString().slice(0, 10);
|
||||||
|
|
|
||||||
|
|
@ -602,6 +602,44 @@ function hasForecastTaxSignal(text: string): boolean {
|
||||||
return hasForecastLexeme && hasTaxLexeme;
|
return hasForecastLexeme && hasTaxLexeme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasVatLiabilityConfirmedTaxPeriodSignal(text: string): boolean {
|
||||||
|
const hasVatLexeme = /(?:ндс|vat)/iu.test(text);
|
||||||
|
if (!hasVatLexeme) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasPaymentCue =
|
||||||
|
/(?:к\s+уплате|надо|нужно|заплатить|уплатить|плат[её]ж|платежку|в\s+налогов|в\s+бюджет|должн[аы]?\s+заплатить)/iu.test(
|
||||||
|
text
|
||||||
|
);
|
||||||
|
if (!hasPaymentCue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasAsOfCue = /(?:на\s+дат|по\s+состоянию|на\s+конец|as\s+of)/iu.test(text);
|
||||||
|
if (hasAsOfCue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasTaxAuthorityCue = /(?:в\s+налогов|в\s+бюджет|декларац|налогов(?:ый|ую)\s+период)/iu.test(text);
|
||||||
|
const hasQuarterCue = /(?:\b[1-4]\s*(?:квартал|кв\.?)\b|квартал|кв\.?)/iu.test(text);
|
||||||
|
const hasZaPeriodCue =
|
||||||
|
/(?:за\s+(?:\d{4}|январ|феврал|март|апрел|май|июн|июл|август|сентябр|октябр|ноябр|декабр|квартал|кв\.?|месяц|год|период))/iu.test(
|
||||||
|
text
|
||||||
|
);
|
||||||
|
const hasExplicitDayDate =
|
||||||
|
/\b(?:\d{1,2}[./-]\d{1,2}[./-](?:\d{2}|\d{4})|(?:19|20)\d{2}[./-]\d{1,2}[./-]\d{1,2})\b/u.test(text);
|
||||||
|
const hasMonthYearNaCue =
|
||||||
|
/(?:на\s+(?:январ|феврал|март|апрел|май|июн|июл|август|сентябр|октябр|ноябр|декабр)\S*\s+(?:19|20)\d{2})/iu.test(
|
||||||
|
text
|
||||||
|
);
|
||||||
|
const hasHowMuchCue = /(?:сколько|скока|скок)/iu.test(text);
|
||||||
|
|
||||||
|
// "На март 2020" и конкретная дата без налогового контекста чаще означают as-of срез.
|
||||||
|
if (!hasTaxAuthorityCue && !hasZaPeriodCue && !hasQuarterCue && (hasMonthYearNaCue || hasExplicitDayDate)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasTaxAuthorityCue || hasZaPeriodCue || hasQuarterCue || (hasHowMuchCue && hasTaxAuthorityCue);
|
||||||
|
}
|
||||||
|
|
||||||
function hasVatPayableConfirmedSignal(text: string): boolean {
|
function hasVatPayableConfirmedSignal(text: string): boolean {
|
||||||
const hasVatLexeme = /(?:ндс|vat)/iu.test(text);
|
const hasVatLexeme = /(?:ндс|vat)/iu.test(text);
|
||||||
if (!hasVatLexeme) {
|
if (!hasVatLexeme) {
|
||||||
|
|
@ -1503,6 +1541,14 @@ function hasAccountNumberAnchor(text: string): boolean {
|
||||||
export function resolveAddressIntent(userMessage: string): AddressIntentResolution {
|
export function resolveAddressIntent(userMessage: string): AddressIntentResolution {
|
||||||
const text = String(userMessage ?? "").trim().toLowerCase();
|
const text = String(userMessage ?? "").trim().toLowerCase();
|
||||||
|
|
||||||
|
if (hasVatLiabilityConfirmedTaxPeriodSignal(text)) {
|
||||||
|
return {
|
||||||
|
intent: "vat_liability_confirmed_for_tax_period",
|
||||||
|
confidence: "high",
|
||||||
|
reasons: ["vat_liability_confirmed_tax_period_signal_detected"]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (hasForecastTaxSignal(text)) {
|
if (hasForecastTaxSignal(text)) {
|
||||||
return {
|
return {
|
||||||
intent: "vat_payable_forecast",
|
intent: "vat_payable_forecast",
|
||||||
|
|
|
||||||
|
|
@ -297,6 +297,7 @@ export async function executeAddressMcpQuery(input: {
|
||||||
query: string;
|
query: string;
|
||||||
limit: number;
|
limit: number;
|
||||||
account_scope?: string[];
|
account_scope?: string[];
|
||||||
|
timeout_ms?: number;
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
fetched_rows: number;
|
fetched_rows: number;
|
||||||
matched_rows: number;
|
matched_rows: number;
|
||||||
|
|
@ -306,7 +307,11 @@ export async function executeAddressMcpQuery(input: {
|
||||||
}> {
|
}> {
|
||||||
const endpoint = buildMcpUrl("/api/execute_query");
|
const endpoint = buildMcpUrl("/api/execute_query");
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeout = setTimeout(() => controller.abort(), Math.max(300, ASSISTANT_MCP_TIMEOUT_MS));
|
const resolvedTimeoutMs =
|
||||||
|
typeof input.timeout_ms === "number" && Number.isFinite(input.timeout_ms)
|
||||||
|
? Math.max(300, Math.trunc(input.timeout_ms))
|
||||||
|
: Math.max(300, ASSISTANT_MCP_TIMEOUT_MS);
|
||||||
|
const timeout = setTimeout(() => controller.abort(), resolvedTimeoutMs);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(endpoint, {
|
const response = await fetch(endpoint, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
@ -373,10 +378,15 @@ export async function executeAddressMcpMetadata(input: {
|
||||||
offset?: number;
|
offset?: number;
|
||||||
sections?: string[];
|
sections?: string[];
|
||||||
extension_name?: string | null;
|
extension_name?: string | null;
|
||||||
|
timeout_ms?: number;
|
||||||
}): Promise<AddressMcpMetadataRowsResult> {
|
}): Promise<AddressMcpMetadataRowsResult> {
|
||||||
const endpoint = buildMcpUrl("/api/get_metadata");
|
const endpoint = buildMcpUrl("/api/get_metadata");
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeout = setTimeout(() => controller.abort(), Math.max(300, ASSISTANT_MCP_TIMEOUT_MS));
|
const resolvedTimeoutMs =
|
||||||
|
typeof input.timeout_ms === "number" && Number.isFinite(input.timeout_ms)
|
||||||
|
? Math.max(300, Math.trunc(input.timeout_ms))
|
||||||
|
: Math.max(300, ASSISTANT_MCP_TIMEOUT_MS);
|
||||||
|
const timeout = setTimeout(() => controller.abort(), resolvedTimeoutMs);
|
||||||
try {
|
try {
|
||||||
const body: Record<string, unknown> = {};
|
const body: Record<string, unknown> = {};
|
||||||
if (typeof input.filter === "string" && input.filter.trim().length > 0) {
|
if (typeof input.filter === "string" && input.filter.trim().length > 0) {
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ const RESULT_SET_TYPE_BY_INTENT: Partial<Record<AddressIntent, AddressResultSetT
|
||||||
list_payables_counterparties: "counterparty_list",
|
list_payables_counterparties: "counterparty_list",
|
||||||
payables_confirmed_as_of_date: "balance_snapshot",
|
payables_confirmed_as_of_date: "balance_snapshot",
|
||||||
vat_payable_confirmed_as_of_date: "balance_snapshot",
|
vat_payable_confirmed_as_of_date: "balance_snapshot",
|
||||||
|
vat_liability_confirmed_for_tax_period: "balance_snapshot",
|
||||||
receivables_confirmed_as_of_date: "balance_snapshot",
|
receivables_confirmed_as_of_date: "balance_snapshot",
|
||||||
list_receivables_counterparties: "counterparty_list",
|
list_receivables_counterparties: "counterparty_list",
|
||||||
list_contracts_by_counterparty: "contract_list",
|
list_contracts_by_counterparty: "contract_list",
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,11 @@ import {
|
||||||
selectAddressRecipe,
|
selectAddressRecipe,
|
||||||
type AddressRecipeExecutionPlan
|
type AddressRecipeExecutionPlan
|
||||||
} from "./addressRecipeCatalog";
|
} from "./addressRecipeCatalog";
|
||||||
import { executeAddressMcpMetadata, executeAddressMcpQuery } from "./addressMcpClient";
|
import {
|
||||||
|
executeAddressMcpMetadata,
|
||||||
|
executeAddressMcpQuery,
|
||||||
|
type AddressMcpMetadataRowsResult
|
||||||
|
} from "./addressMcpClient";
|
||||||
import { runAddressDecomposeStage, type AddressFollowupContext } from "./address_runtime/decomposeStage";
|
import { runAddressDecomposeStage, type AddressFollowupContext } from "./address_runtime/decomposeStage";
|
||||||
import { resolvePrimaryAnchor, refineAnchorFromRows, type AnchorResolutionDebug } from "./address_runtime/resolveStage";
|
import { resolvePrimaryAnchor, refineAnchorFromRows, type AnchorResolutionDebug } from "./address_runtime/resolveStage";
|
||||||
import {
|
import {
|
||||||
|
|
@ -87,9 +91,15 @@ const ADDRESS_CONFIRMED_PAYABLES_MIN_LIMIT = 200;
|
||||||
const COUNTERPARTY_CATALOG_LOOKUP_LIMIT = 1000;
|
const COUNTERPARTY_CATALOG_LOOKUP_LIMIT = 1000;
|
||||||
const COUNTERPARTY_CATALOG_CACHE_TTL_MS = 120_000;
|
const COUNTERPARTY_CATALOG_CACHE_TTL_MS = 120_000;
|
||||||
const VAT_METADATA_PROBE_LIMIT = 100;
|
const VAT_METADATA_PROBE_LIMIT = 100;
|
||||||
const VAT_SOURCE_PROBE_MAX_OBJECTS = 8;
|
const VAT_SOURCE_PROBE_MAX_OBJECTS = 6;
|
||||||
const VAT_METADATA_PROBE_TYPES = ["РегистрНакопления", "РегистрСведений", "Документ"] as const;
|
const VAT_METADATA_PROBE_TYPES = ["РегистрНакопления", "Документ"] as const;
|
||||||
const VAT_METADATA_PROBE_MASKS = ["ндс", "книгапродаж", "книгапокупок", "счетфактур", "вычет", "восстанов"] as const;
|
const VAT_METADATA_PROBE_MASKS = ["ндс", "книгапродаж", "книгапокупок", "счетфактур"] as const;
|
||||||
|
const VAT_METADATA_PROBE_CONCURRENCY = 4;
|
||||||
|
const VAT_METADATA_PROBE_TIMEOUT_MS = 800;
|
||||||
|
const VAT_METADATA_PROBE_RETRY_TIMEOUT_MS = 1_200;
|
||||||
|
const VAT_OBJECT_PROBE_CONCURRENCY = 4;
|
||||||
|
const VAT_OBJECT_PROBE_TIMEOUT_MS = 800;
|
||||||
|
const VAT_OBJECT_PROBE_FALLBACK_TIMEOUT_MS = 800;
|
||||||
const PARTY_ANCHOR_STOPWORDS = new Set([
|
const PARTY_ANCHOR_STOPWORDS = new Set([
|
||||||
"ооо",
|
"ооо",
|
||||||
"ао",
|
"ао",
|
||||||
|
|
@ -312,6 +322,76 @@ function extractVatMetadataObjects(rows: Array<Record<string, unknown>>): VatMet
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isVatMetadataObject(item: VatMetadataObject): boolean {
|
||||||
|
const source = `${item.fullName} ${item.synonym ?? ""}`.toLowerCase().replace(/ё/g, "е");
|
||||||
|
if (source.includes("ндфл")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(?:ндс|книгапокуп|книгапродаж|счет[\s-]?фактур)/iu.test(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAbortErrorMessage(error: string | null | undefined): boolean {
|
||||||
|
const normalized = String(error ?? "").toLowerCase();
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return normalized.includes("aborted") || normalized.includes("abort");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function mapWithConcurrency<T, R>(
|
||||||
|
items: T[],
|
||||||
|
concurrency: number,
|
||||||
|
worker: (item: T, index: number) => Promise<R>
|
||||||
|
): Promise<R[]> {
|
||||||
|
if (items.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const boundedConcurrency = Math.max(1, Math.min(Math.trunc(concurrency), items.length));
|
||||||
|
const results = new Array<R>(items.length);
|
||||||
|
let nextIndex = 0;
|
||||||
|
const runners = Array.from({ length: boundedConcurrency }, async () => {
|
||||||
|
while (true) {
|
||||||
|
const currentIndex = nextIndex;
|
||||||
|
nextIndex += 1;
|
||||||
|
if (currentIndex >= items.length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
results[currentIndex] = await worker(items[currentIndex], currentIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await Promise.all(runners);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeVatMetadataProbeRequest(request: {
|
||||||
|
meta_type: string;
|
||||||
|
name_mask: string;
|
||||||
|
limit: number;
|
||||||
|
}): Promise<AddressMcpMetadataRowsResult> {
|
||||||
|
const firstAttempt = await executeAddressMcpMetadata({
|
||||||
|
...request,
|
||||||
|
timeout_ms: VAT_METADATA_PROBE_TIMEOUT_MS
|
||||||
|
});
|
||||||
|
if (!firstAttempt.error || !isAbortErrorMessage(firstAttempt.error)) {
|
||||||
|
return firstAttempt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const retryLimit = Math.max(20, Math.min(request.limit, Math.trunc(request.limit / 2)));
|
||||||
|
const retryAttempt = await executeAddressMcpMetadata({
|
||||||
|
...request,
|
||||||
|
limit: retryLimit,
|
||||||
|
timeout_ms: VAT_METADATA_PROBE_RETRY_TIMEOUT_MS
|
||||||
|
});
|
||||||
|
if (!retryAttempt.error) {
|
||||||
|
return retryAttempt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...retryAttempt,
|
||||||
|
error: `${firstAttempt.error}; retry: ${retryAttempt.error}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function scoreVatMetadataObject(item: VatMetadataObject): number {
|
function scoreVatMetadataObject(item: VatMetadataObject): number {
|
||||||
const fullName = item.fullName.toLowerCase();
|
const fullName = item.fullName.toLowerCase();
|
||||||
const synonym = String(item.synonym ?? "").toLowerCase();
|
const synonym = String(item.synonym ?? "").toLowerCase();
|
||||||
|
|
@ -340,8 +420,12 @@ function scoreVatMetadataObject(item: VatMetadataObject): number {
|
||||||
return score;
|
return score;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildVatObjectProbeQuery(object: VatMetadataObject, asOfExpr: string): string {
|
type VatObjectProbeMode = "latest" | "exists";
|
||||||
|
|
||||||
|
function buildVatObjectProbeQuery(object: VatMetadataObject, asOfExpr: string, mode: VatObjectProbeMode = "latest"): string {
|
||||||
|
const orderClause = mode === "latest" ? "\nУПОРЯДОЧИТЬ ПО\n Движения.Период УБЫВ" : "";
|
||||||
if (object.objectType === "document") {
|
if (object.objectType === "document") {
|
||||||
|
const documentOrderClause = mode === "latest" ? "\nУПОРЯДОЧИТЬ ПО\n Док.Дата УБЫВ" : "";
|
||||||
return `
|
return `
|
||||||
ВЫБРАТЬ ПЕРВЫЕ 1
|
ВЫБРАТЬ ПЕРВЫЕ 1
|
||||||
Док.Дата КАК Период,
|
Док.Дата КАК Период,
|
||||||
|
|
@ -353,9 +437,8 @@ function buildVatObjectProbeQuery(object: VatMetadataObject, asOfExpr: string):
|
||||||
${object.fullName} КАК Док
|
${object.fullName} КАК Док
|
||||||
ГДЕ
|
ГДЕ
|
||||||
Док.Дата <= ${asOfExpr}
|
Док.Дата <= ${asOfExpr}
|
||||||
УПОРЯДОЧИТЬ ПО
|
${documentOrderClause}
|
||||||
Док.Дата УБЫВ
|
`.trim().replace(/\n{3,}/g, "\n\n");
|
||||||
`.trim();
|
|
||||||
}
|
}
|
||||||
return `
|
return `
|
||||||
ВЫБРАТЬ ПЕРВЫЕ 1
|
ВЫБРАТЬ ПЕРВЫЕ 1
|
||||||
|
|
@ -368,9 +451,8 @@ function buildVatObjectProbeQuery(object: VatMetadataObject, asOfExpr: string):
|
||||||
${object.fullName} КАК Движения
|
${object.fullName} КАК Движения
|
||||||
ГДЕ
|
ГДЕ
|
||||||
Движения.Период <= ${asOfExpr}
|
Движения.Период <= ${asOfExpr}
|
||||||
УПОРЯДОЧИТЬ ПО
|
${orderClause}
|
||||||
Движения.Период УБЫВ
|
`.trim().replace(/\n{3,}/g, "\n\n");
|
||||||
`.trim();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function probeVatDirectSources(filters: AddressFilterSet): Promise<VatDirectSourceProbeSummary> {
|
async function probeVatDirectSources(filters: AddressFilterSet): Promise<VatDirectSourceProbeSummary> {
|
||||||
|
|
@ -409,18 +491,35 @@ async function probeVatDirectSources(filters: AddressFilterSet): Promise<VatDire
|
||||||
limit: VAT_METADATA_PROBE_LIMIT
|
limit: VAT_METADATA_PROBE_LIMIT
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
const metadataResponses = await Promise.all(metadataRequests.map((request) => executeAddressMcpMetadata(request)));
|
const metadataResponses = await mapWithConcurrency(
|
||||||
|
metadataRequests,
|
||||||
|
VAT_METADATA_PROBE_CONCURRENCY,
|
||||||
|
(request) => executeVatMetadataProbeRequest(request)
|
||||||
|
);
|
||||||
|
|
||||||
|
const metadataOutcomes = metadataResponses.map((response, index) => ({
|
||||||
|
request: metadataRequests[index],
|
||||||
|
response
|
||||||
|
}));
|
||||||
|
const successfulMetadataByType = new Map<string, number>();
|
||||||
const metadataErrors: string[] = [];
|
const metadataErrors: string[] = [];
|
||||||
const metadataObjectsBuffer: VatMetadataObject[] = [];
|
const metadataObjectsBuffer: VatMetadataObject[] = [];
|
||||||
for (const [index, response] of metadataResponses.entries()) {
|
for (const { request, response } of metadataOutcomes) {
|
||||||
const request = metadataRequests[index];
|
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
metadataErrors.push(`${request.meta_type}:${request.name_mask}:${response.error}`);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const currentSuccessCount = successfulMetadataByType.get(request.meta_type) ?? 0;
|
||||||
|
successfulMetadataByType.set(request.meta_type, currentSuccessCount + 1);
|
||||||
metadataObjectsBuffer.push(...extractVatMetadataObjects(response.rows));
|
metadataObjectsBuffer.push(...extractVatMetadataObjects(response.rows));
|
||||||
}
|
}
|
||||||
|
for (const { request, response } of metadataOutcomes) {
|
||||||
|
if (response.error) {
|
||||||
|
if (isAbortErrorMessage(response.error) && (successfulMetadataByType.get(request.meta_type) ?? 0) > 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
metadataErrors.push(`${request.meta_type}:${request.name_mask}:${response.error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const deduplicatedObjects = new Map<string, VatMetadataObject>();
|
const deduplicatedObjects = new Map<string, VatMetadataObject>();
|
||||||
for (const item of metadataObjectsBuffer) {
|
for (const item of metadataObjectsBuffer) {
|
||||||
|
|
@ -437,54 +536,79 @@ async function probeVatDirectSources(filters: AddressFilterSet): Promise<VatDire
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const discoveredMetadataObjects = Array.from(deduplicatedObjects.values()).sort(
|
const discoveredMetadataObjects = Array.from(deduplicatedObjects.values())
|
||||||
|
.filter((item) => isVatMetadataObject(item))
|
||||||
|
.sort(
|
||||||
(a, b) => scoreVatMetadataObject(b) - scoreVatMetadataObject(a) || a.fullName.localeCompare(b.fullName, "ru")
|
(a, b) => scoreVatMetadataObject(b) - scoreVatMetadataObject(a) || a.fullName.localeCompare(b.fullName, "ru")
|
||||||
);
|
);
|
||||||
const metadataObjects = discoveredMetadataObjects.slice(0, VAT_SOURCE_PROBE_MAX_OBJECTS);
|
const metadataObjects = discoveredMetadataObjects.slice(0, VAT_SOURCE_PROBE_MAX_OBJECTS);
|
||||||
|
|
||||||
const probeRows: VatDirectSourceProbeItem[] = [];
|
const probeRows = await mapWithConcurrency(
|
||||||
for (const object of metadataObjects) {
|
metadataObjects,
|
||||||
const probeQuery = buildVatObjectProbeQuery(object, asOfExpr);
|
VAT_OBJECT_PROBE_CONCURRENCY,
|
||||||
const probeResult = await executeAddressMcpQuery({
|
async (object): Promise<VatDirectSourceProbeItem> => {
|
||||||
query: probeQuery,
|
let probeResult = await executeAddressMcpQuery({
|
||||||
limit: 1
|
query: buildVatObjectProbeQuery(object, asOfExpr, "latest"),
|
||||||
});
|
limit: 1,
|
||||||
if (probeResult.error) {
|
timeout_ms: VAT_OBJECT_PROBE_TIMEOUT_MS
|
||||||
probeRows.push({
|
});
|
||||||
|
let fallbackUsed = false;
|
||||||
|
if (probeResult.error) {
|
||||||
|
if (isAbortErrorMessage(probeResult.error)) {
|
||||||
|
return {
|
||||||
|
fullName: object.fullName,
|
||||||
|
synonym: object.synonym,
|
||||||
|
objectType: object.objectType,
|
||||||
|
status: "error",
|
||||||
|
rowsFetched: probeResult.fetched_rows,
|
||||||
|
error: probeResult.error
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const fallbackResult = await executeAddressMcpQuery({
|
||||||
|
query: buildVatObjectProbeQuery(object, asOfExpr, "exists"),
|
||||||
|
limit: 1,
|
||||||
|
timeout_ms: VAT_OBJECT_PROBE_FALLBACK_TIMEOUT_MS
|
||||||
|
});
|
||||||
|
if (!fallbackResult.error) {
|
||||||
|
probeResult = fallbackResult;
|
||||||
|
fallbackUsed = true;
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
fullName: object.fullName,
|
||||||
|
synonym: object.synonym,
|
||||||
|
objectType: object.objectType,
|
||||||
|
status: "error",
|
||||||
|
rowsFetched: probeResult.fetched_rows,
|
||||||
|
error: `${probeResult.error}; fallback: ${fallbackResult.error}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const firstRow = probeResult.raw_rows[0] ?? null;
|
||||||
|
const lastPeriod =
|
||||||
|
firstRow !== null
|
||||||
|
? valueAsString(
|
||||||
|
(firstRow as Record<string, unknown>).Период ?? (firstRow as Record<string, unknown>).period
|
||||||
|
).trim() || null
|
||||||
|
: null;
|
||||||
|
const sampleRegistrator =
|
||||||
|
firstRow !== null
|
||||||
|
? valueAsString(
|
||||||
|
(firstRow as Record<string, unknown>).Регистратор ??
|
||||||
|
(firstRow as Record<string, unknown>).registrator ??
|
||||||
|
(firstRow as Record<string, unknown>).Registrator
|
||||||
|
).trim() || null
|
||||||
|
: null;
|
||||||
|
return {
|
||||||
fullName: object.fullName,
|
fullName: object.fullName,
|
||||||
synonym: object.synonym,
|
synonym: object.synonym,
|
||||||
objectType: object.objectType,
|
objectType: object.objectType,
|
||||||
status: "error",
|
status: probeResult.raw_rows.length > 0 ? "ok" : "empty",
|
||||||
rowsFetched: probeResult.fetched_rows,
|
rowsFetched: probeResult.fetched_rows,
|
||||||
error: probeResult.error
|
lastPeriod: fallbackUsed ? null : lastPeriod,
|
||||||
});
|
sampleRegistrator
|
||||||
continue;
|
};
|
||||||
}
|
}
|
||||||
|
);
|
||||||
const firstRow = probeResult.raw_rows[0] ?? null;
|
|
||||||
const lastPeriod =
|
|
||||||
firstRow !== null
|
|
||||||
? valueAsString((firstRow as Record<string, unknown>).Период ?? (firstRow as Record<string, unknown>).period).trim() ||
|
|
||||||
null
|
|
||||||
: null;
|
|
||||||
const sampleRegistrator =
|
|
||||||
firstRow !== null
|
|
||||||
? valueAsString(
|
|
||||||
(firstRow as Record<string, unknown>).Регистратор ??
|
|
||||||
(firstRow as Record<string, unknown>).registrator ??
|
|
||||||
(firstRow as Record<string, unknown>).Registrator
|
|
||||||
).trim() || null
|
|
||||||
: null;
|
|
||||||
probeRows.push({
|
|
||||||
fullName: object.fullName,
|
|
||||||
synonym: object.synonym,
|
|
||||||
objectType: object.objectType,
|
|
||||||
status: probeResult.raw_rows.length > 0 ? "ok" : "empty",
|
|
||||||
rowsFetched: probeResult.fetched_rows,
|
|
||||||
lastPeriod,
|
|
||||||
sampleRegistrator
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const status: VatDirectSourceProbeSummary["status"] = metadataResponses.every((item) => item.error) ? "error" : "ok";
|
const status: VatDirectSourceProbeSummary["status"] = metadataResponses.every((item) => item.error) ? "error" : "ok";
|
||||||
const allErrors = [
|
const allErrors = [
|
||||||
|
|
@ -1106,7 +1230,8 @@ function isConfirmedBalanceIntent(intent: AddressIntent): boolean {
|
||||||
intent === "documents_forming_balance" ||
|
intent === "documents_forming_balance" ||
|
||||||
intent === "payables_confirmed_as_of_date" ||
|
intent === "payables_confirmed_as_of_date" ||
|
||||||
intent === "receivables_confirmed_as_of_date" ||
|
intent === "receivables_confirmed_as_of_date" ||
|
||||||
intent === "vat_payable_confirmed_as_of_date"
|
intent === "vat_payable_confirmed_as_of_date" ||
|
||||||
|
intent === "vat_liability_confirmed_for_tax_period"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1942,6 +2067,8 @@ function buildLimitedOffers(input: {
|
||||||
offers.push("показать подтвержденный реестр открытой дебиторской задолженности на дату среза по 62/76");
|
offers.push("показать подтвержденный реестр открытой дебиторской задолженности на дату среза по 62/76");
|
||||||
} else if (input.intent === "vat_payable_confirmed_as_of_date") {
|
} else if (input.intent === "vat_payable_confirmed_as_of_date") {
|
||||||
offers.push("показать подтвержденную сумму НДС к уплате на дату среза по счетам 68*");
|
offers.push("показать подтвержденную сумму НДС к уплате на дату среза по счетам 68*");
|
||||||
|
} else if (input.intent === "vat_liability_confirmed_for_tax_period") {
|
||||||
|
offers.push("показать подтвержденный расчет НДС к уплате за налоговый период по книгам продаж/покупок");
|
||||||
} else if (input.intent === "payables_confirmed_as_of_date") {
|
} else if (input.intent === "payables_confirmed_as_of_date") {
|
||||||
offers.push("показать подтвержденный реестр открытых обязательств на дату среза по 60/76");
|
offers.push("показать подтвержденный реестр открытых обязательств на дату среза по 60/76");
|
||||||
} else if (input.intent === "list_payables_counterparties") {
|
} else if (input.intent === "list_payables_counterparties") {
|
||||||
|
|
@ -1996,7 +2123,8 @@ function buildLimitedIntentSignalLine(input: {
|
||||||
list_payables_counterparties: "Сигнал запроса: нужен ранжированный список кредиторов.",
|
list_payables_counterparties: "Сигнал запроса: нужен ранжированный список кредиторов.",
|
||||||
receivables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез дебиторской задолженности на дату.",
|
receivables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез дебиторской задолженности на дату.",
|
||||||
payables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез обязательств к оплате на дату.",
|
payables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез обязательств к оплате на дату.",
|
||||||
vat_payable_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез НДС к уплате на дату."
|
vat_payable_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез НДС к уплате на дату.",
|
||||||
|
vat_liability_confirmed_for_tax_period: "Сигнал запроса: нужен подтвержденный расчет НДС к уплате за налоговый период."
|
||||||
};
|
};
|
||||||
|
|
||||||
const byShape: Partial<Record<AddressQueryShapeDetection["shape"], string>> = {
|
const byShape: Partial<Record<AddressQueryShapeDetection["shape"], string>> = {
|
||||||
|
|
@ -2201,6 +2329,8 @@ function buildLimitedExecutionResult(input: {
|
||||||
? "exact_receivables_mode_limited_response"
|
? "exact_receivables_mode_limited_response"
|
||||||
: input.intent.intent === "vat_payable_confirmed_as_of_date"
|
: input.intent.intent === "vat_payable_confirmed_as_of_date"
|
||||||
? "exact_vat_payable_mode_limited_response"
|
? "exact_vat_payable_mode_limited_response"
|
||||||
|
: input.intent.intent === "vat_liability_confirmed_for_tax_period"
|
||||||
|
? "exact_vat_tax_period_mode_limited_response"
|
||||||
: null;
|
: null;
|
||||||
const reasons =
|
const reasons =
|
||||||
exactLimitedReason && !reasonsWithConfirmedFallback.includes(exactLimitedReason)
|
exactLimitedReason && !reasonsWithConfirmedFallback.includes(exactLimitedReason)
|
||||||
|
|
@ -2460,6 +2590,12 @@ export class AddressQueryService {
|
||||||
) {
|
) {
|
||||||
baseReasons.push("confirmed_balance_exact_vat_payable_intent");
|
baseReasons.push("confirmed_balance_exact_vat_payable_intent");
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
intent.intent === "vat_liability_confirmed_for_tax_period" &&
|
||||||
|
!baseReasons.includes("confirmed_balance_exact_vat_tax_period_intent")
|
||||||
|
) {
|
||||||
|
baseReasons.push("confirmed_balance_exact_vat_tax_period_intent");
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
requestedResultMode === "confirmed_balance" &&
|
requestedResultMode === "confirmed_balance" &&
|
||||||
recipeIntent === "open_items_by_counterparty_or_contract" &&
|
recipeIntent === "open_items_by_counterparty_or_contract" &&
|
||||||
|
|
@ -3519,14 +3655,17 @@ export class AddressQueryService {
|
||||||
|
|
||||||
const vatProbeRequired =
|
const vatProbeRequired =
|
||||||
composeIntent === "vat_payable_confirmed_as_of_date" ||
|
composeIntent === "vat_payable_confirmed_as_of_date" ||
|
||||||
|
composeIntent === "vat_liability_confirmed_for_tax_period" ||
|
||||||
(composeIntent === "vat_payable_forecast" && shouldProbeVatSourcesForForecast(userMessage));
|
(composeIntent === "vat_payable_forecast" && shouldProbeVatSourcesForForecast(userMessage));
|
||||||
const vatDirectSourceProbe = vatProbeRequired ? await probeVatDirectSources(executionFilters) : null;
|
const vatDirectSourceProbe = vatProbeRequired ? await probeVatDirectSources(executionFilters) : null;
|
||||||
const shouldEmphasizeNumbers =
|
const shouldEmphasizeNumbers =
|
||||||
composeIntent === "vat_payable_forecast" ||
|
composeIntent === "vat_payable_forecast" ||
|
||||||
composeIntent === "vat_payable_confirmed_as_of_date" ||
|
composeIntent === "vat_payable_confirmed_as_of_date" ||
|
||||||
|
composeIntent === "vat_liability_confirmed_for_tax_period" ||
|
||||||
composeIntent === "payables_confirmed_as_of_date" ||
|
composeIntent === "payables_confirmed_as_of_date" ||
|
||||||
composeIntent === "receivables_confirmed_as_of_date";
|
composeIntent === "receivables_confirmed_as_of_date";
|
||||||
const shouldUseRubCurrency = composeIntent === "vat_payable_forecast";
|
const shouldUseRubCurrency =
|
||||||
|
composeIntent === "vat_payable_forecast" || composeIntent === "vat_liability_confirmed_for_tax_period";
|
||||||
const factual = composeFactualReply(
|
const factual = composeFactualReply(
|
||||||
composeIntent,
|
composeIntent,
|
||||||
filteredRows,
|
filteredRows,
|
||||||
|
|
@ -3599,14 +3738,18 @@ export class AddressQueryService {
|
||||||
const exactConfirmedIntent =
|
const exactConfirmedIntent =
|
||||||
(intent.intent === "payables_confirmed_as_of_date" && composeIntent === "payables_confirmed_as_of_date") ||
|
(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") ||
|
(intent.intent === "receivables_confirmed_as_of_date" && composeIntent === "receivables_confirmed_as_of_date") ||
|
||||||
(intent.intent === "vat_payable_confirmed_as_of_date" && composeIntent === "vat_payable_confirmed_as_of_date");
|
(intent.intent === "vat_payable_confirmed_as_of_date" && composeIntent === "vat_payable_confirmed_as_of_date") ||
|
||||||
|
(intent.intent === "vat_liability_confirmed_for_tax_period" &&
|
||||||
|
composeIntent === "vat_liability_confirmed_for_tax_period");
|
||||||
if (exactConfirmedIntent && factualResultSemantics.balance_confirmed !== true) {
|
if (exactConfirmedIntent && factualResultSemantics.balance_confirmed !== true) {
|
||||||
const exactModeName =
|
const exactModeName =
|
||||||
intent.intent === "payables_confirmed_as_of_date"
|
intent.intent === "payables_confirmed_as_of_date"
|
||||||
? "payables"
|
? "payables"
|
||||||
: intent.intent === "receivables_confirmed_as_of_date"
|
: intent.intent === "receivables_confirmed_as_of_date"
|
||||||
? "receivables"
|
? "receivables"
|
||||||
: "vat_payable";
|
: intent.intent === "vat_liability_confirmed_for_tax_period"
|
||||||
|
? "vat_tax_period"
|
||||||
|
: "vat_payable";
|
||||||
return buildLimitedExecutionResult({
|
return buildLimitedExecutionResult({
|
||||||
mode,
|
mode,
|
||||||
shape,
|
shape,
|
||||||
|
|
@ -3634,6 +3777,8 @@ export class AddressQueryService {
|
||||||
nextStep:
|
nextStep:
|
||||||
intent.intent === "vat_payable_confirmed_as_of_date"
|
intent.intent === "vat_payable_confirmed_as_of_date"
|
||||||
? "specify as_of_date/organization or provide VAT settlement registers to prove exact VAT payable balance"
|
? "specify as_of_date/organization or provide VAT settlement registers to prove exact VAT payable balance"
|
||||||
|
: intent.intent === "vat_liability_confirmed_for_tax_period"
|
||||||
|
? "specify tax period boundaries and ensure purchase/sales VAT books are available via MCP"
|
||||||
: "specify as_of_date/counterparty or enable detailed settlement registers for exact confirmed balance",
|
: "specify as_of_date/counterparty or enable detailed settlement registers for exact confirmed balance",
|
||||||
limitations: [`exact_${exactModeName}_mode_unconfirmed_output_blocked`],
|
limitations: [`exact_${exactModeName}_mode_unconfirmed_output_blocked`],
|
||||||
reasons: [...baseReasons, `exact_${exactModeName}_mode_unconfirmed_output_blocked`],
|
reasons: [...baseReasons, `exact_${exactModeName}_mode_unconfirmed_output_blocked`],
|
||||||
|
|
|
||||||
|
|
@ -498,6 +498,30 @@ __WHERE_CLAUSE__
|
||||||
Регистратор
|
Регистратор
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const VAT_LIABILITY_CONFIRMED_TAX_PERIOD_QUERY_TEMPLATE = `
|
||||||
|
ВЫБРАТЬ
|
||||||
|
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
|
||||||
|
"VAT_BOOK_SALES" КАК Регистратор,
|
||||||
|
"68.02" КАК СчетДт,
|
||||||
|
"" КАК СчетКт,
|
||||||
|
СУММА(ЕСТЬNULL(Движения.НДС, 0)) КАК Сумма
|
||||||
|
ИЗ
|
||||||
|
РегистрНакопления.НДСЗаписиКнигиПродаж КАК Движения
|
||||||
|
__WHERE_CLAUSE__
|
||||||
|
ОБЪЕДИНИТЬ ВСЕ
|
||||||
|
ВЫБРАТЬ
|
||||||
|
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
|
||||||
|
"VAT_BOOK_PURCHASES" КАК Регистратор,
|
||||||
|
"19" КАК СчетДт,
|
||||||
|
"" КАК СчетКт,
|
||||||
|
СУММА(ЕСТЬNULL(Движения.НДС, 0)) КАК Сумма
|
||||||
|
ИЗ
|
||||||
|
РегистрНакопления.НДСЗаписиКнигиПокупок КАК Движения
|
||||||
|
__WHERE_CLAUSE__
|
||||||
|
УПОРЯДОЧИТЬ ПО
|
||||||
|
Регистратор
|
||||||
|
`;
|
||||||
|
|
||||||
const BASE_RECIPES: AddressRecipeDefinition[] = [
|
const BASE_RECIPES: AddressRecipeDefinition[] = [
|
||||||
{
|
{
|
||||||
recipe_id: "address_period_coverage_profile_v1",
|
recipe_id: "address_period_coverage_profile_v1",
|
||||||
|
|
@ -600,6 +624,16 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [
|
||||||
account_scope_mode: "strict",
|
account_scope_mode: "strict",
|
||||||
query_template: "vat_payable_confirmed_as_of_balance_profile"
|
query_template: "vat_payable_confirmed_as_of_balance_profile"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
recipe_id: "address_vat_liability_confirmed_tax_period_v1",
|
||||||
|
intent: "vat_liability_confirmed_for_tax_period",
|
||||||
|
purpose: "Build confirmed VAT liability for tax period from purchase/sales VAT books",
|
||||||
|
required_filters: ["period_from", "period_to"],
|
||||||
|
optional_filters: ["organization"],
|
||||||
|
default_limit: 32,
|
||||||
|
account_scope_mode: "preferred",
|
||||||
|
query_template: "vat_liability_confirmed_tax_period_profile"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
recipe_id: "address_contracts_by_counterparty_v1",
|
recipe_id: "address_contracts_by_counterparty_v1",
|
||||||
intent: "list_contracts_by_counterparty",
|
intent: "list_contracts_by_counterparty",
|
||||||
|
|
@ -947,6 +981,7 @@ function maxLimitForIntent(intent: AddressIntent): number {
|
||||||
intent === "supplier_payouts_profile" ||
|
intent === "supplier_payouts_profile" ||
|
||||||
intent === "contract_usage_and_value" ||
|
intent === "contract_usage_and_value" ||
|
||||||
intent === "vat_payable_forecast" ||
|
intent === "vat_payable_forecast" ||
|
||||||
|
intent === "vat_liability_confirmed_for_tax_period" ||
|
||||||
intent === "list_contracts_by_counterparty" ||
|
intent === "list_contracts_by_counterparty" ||
|
||||||
intent === "list_documents_by_counterparty" ||
|
intent === "list_documents_by_counterparty" ||
|
||||||
intent === "bank_operations_by_counterparty" ||
|
intent === "bank_operations_by_counterparty" ||
|
||||||
|
|
@ -996,7 +1031,8 @@ export function buildAddressRecipePlan(
|
||||||
recipe.query_template === "document_section_profile" ||
|
recipe.query_template === "document_section_profile" ||
|
||||||
recipe.query_template === "counterparty_roles_profile" ||
|
recipe.query_template === "counterparty_roles_profile" ||
|
||||||
recipe.query_template === "contract_usage_profile" ||
|
recipe.query_template === "contract_usage_profile" ||
|
||||||
recipe.query_template === "vat_payable_forecast_profile";
|
recipe.query_template === "vat_payable_forecast_profile" ||
|
||||||
|
recipe.query_template === "vat_liability_confirmed_tax_period_profile";
|
||||||
const baseLimit =
|
const baseLimit =
|
||||||
typeof filters.limit === "number" && Number.isFinite(filters.limit)
|
typeof filters.limit === "number" && Number.isFinite(filters.limit)
|
||||||
? Math.max(1, Math.min(maxLimit, Math.trunc(filters.limit)))
|
? Math.max(1, Math.min(maxLimit, Math.trunc(filters.limit)))
|
||||||
|
|
@ -1091,6 +1127,11 @@ export function buildAddressRecipePlan(
|
||||||
.replaceAll("__VAT68_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", VAT_PAYABLE_68_PREFIXES))
|
.replaceAll("__VAT68_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", VAT_PAYABLE_68_PREFIXES))
|
||||||
.replaceAll("__VAT19_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", VAT_PAYABLE_19_PREFIXES))
|
.replaceAll("__VAT19_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", VAT_PAYABLE_19_PREFIXES))
|
||||||
.replaceAll("__VAT19_KT_MATCH__", buildAccountPrefixPredicate("Движения.СчетКт", VAT_PAYABLE_19_PREFIXES))
|
.replaceAll("__VAT19_KT_MATCH__", buildAccountPrefixPredicate("Движения.СчетКт", VAT_PAYABLE_19_PREFIXES))
|
||||||
|
: recipe.query_template === "vat_liability_confirmed_tax_period_profile"
|
||||||
|
? VAT_LIABILITY_CONFIRMED_TAX_PERIOD_QUERY_TEMPLATE.replaceAll(
|
||||||
|
"__WHERE_CLAUSE__",
|
||||||
|
buildManagementWhereClause(filters, "Движения.Период")
|
||||||
|
)
|
||||||
: recipe.query_template === "vat_payable_confirmed_as_of_balance_profile"
|
: recipe.query_template === "vat_payable_confirmed_as_of_balance_profile"
|
||||||
? (() => {
|
? (() => {
|
||||||
const asOfExpr =
|
const asOfExpr =
|
||||||
|
|
|
||||||
|
|
@ -212,6 +212,9 @@ function emphasizeNumericTokens(line: string): string {
|
||||||
if (!line) {
|
if (!line) {
|
||||||
return line;
|
return line;
|
||||||
}
|
}
|
||||||
|
const isDigit = (char: string): boolean => /\d/.test(char);
|
||||||
|
const isLetter = (char: string): boolean => /[A-Za-zА-Яа-яЁё]/.test(char);
|
||||||
|
const dateLikePunctuation = new Set([".", "-", "/", ":"]);
|
||||||
const chunks = line.split(/(`[^`]*`)/g);
|
const chunks = line.split(/(`[^`]*`)/g);
|
||||||
return chunks
|
return chunks
|
||||||
.map((chunk, index) => {
|
.map((chunk, index) => {
|
||||||
|
|
@ -221,9 +224,23 @@ function emphasizeNumericTokens(line: string): string {
|
||||||
return chunk.replace(/\b-?(?:\d{1,3}(?:[.\s]\d{3})+|\d+)(?:[.,]\d+)?\b/g, (match, offset, source) => {
|
return chunk.replace(/\b-?(?:\d{1,3}(?:[.\s]\d{3})+|\d+)(?:[.,]\d+)?\b/g, (match, offset, source) => {
|
||||||
const before = offset > 0 ? source[offset - 1] : "";
|
const before = offset > 0 ? source[offset - 1] : "";
|
||||||
const after = offset + match.length < source.length ? source[offset + match.length] : "";
|
const after = offset + match.length < source.length ? source[offset + match.length] : "";
|
||||||
|
const before2 = offset > 1 ? source[offset - 2] : "";
|
||||||
|
const after2 = offset + match.length + 1 < source.length ? source[offset + match.length + 1] : "";
|
||||||
if (before === "*" || after === "*") {
|
if (before === "*" || after === "*") {
|
||||||
return match;
|
return match;
|
||||||
}
|
}
|
||||||
|
if (isLetter(before) || isLetter(after)) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
if (offset === 0 && (after === "." || after === ")")) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
if (dateLikePunctuation.has(before) && isDigit(before2)) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
if (dateLikePunctuation.has(after) && isDigit(after2)) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
return `**${match}**`;
|
return `**${match}**`;
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
@ -2545,18 +2562,37 @@ export function composeFactualReply(
|
||||||
|
|
||||||
if (vatProbe && vatProbe.status === "ok") {
|
if (vatProbe && vatProbe.status === "ok") {
|
||||||
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
|
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
|
||||||
|
const statusRank = (status: VatDirectSourceProbeItem["status"]): number =>
|
||||||
|
status === "ok" ? 0 : status === "empty" ? 1 : 2;
|
||||||
|
const orderedProbeRows = [...vatProbe.probedSources].sort(
|
||||||
|
(a, b) =>
|
||||||
|
statusRank(a.status) - statusRank(b.status) ||
|
||||||
|
a.fullName.localeCompare(b.fullName, "ru")
|
||||||
|
);
|
||||||
|
const nonErrorProbeRows = orderedProbeRows.filter((item) => item.status !== "error");
|
||||||
|
const visibleProbeRows = (nonErrorProbeRows.length > 0 ? nonErrorProbeRows : orderedProbeRows).slice(0, 6);
|
||||||
|
const erroredSources = vatProbe.probedSources.filter((item) => item.status === "error").length;
|
||||||
lines.push(
|
lines.push(
|
||||||
"",
|
"",
|
||||||
"Покрытие VAT-источников через MCP:",
|
"Покрытие VAT-источников через MCP:",
|
||||||
`- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`,
|
`- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`,
|
||||||
`- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`,
|
`- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`,
|
||||||
`- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`
|
`- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`,
|
||||||
|
`- Источников с ошибкой запроса: ${formatNumberWithDots(erroredSources)}.`
|
||||||
);
|
);
|
||||||
if (vatProbe.probedSources.length > 0) {
|
if (visibleProbeRows.length > 0) {
|
||||||
lines.push(
|
lines.push(
|
||||||
...vatProbe.probedSources.slice(0, 6).map((item, index) => {
|
...visibleProbeRows.map((item, index) => {
|
||||||
const name = item.synonym ? `${item.fullName} (${item.synonym})` : item.fullName;
|
const name = item.synonym ? `${item.fullName} (${item.synonym})` : item.fullName;
|
||||||
return `${index + 1}. ${name} | ${formatVatProbeStatusRu(item.status)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}`;
|
const extra =
|
||||||
|
item.status === "ok"
|
||||||
|
? item.lastPeriod
|
||||||
|
? ` | последнее движение: ${item.lastPeriod}`
|
||||||
|
: ""
|
||||||
|
: item.status === "error" && item.error
|
||||||
|
? ` | ошибка: ${item.error}`
|
||||||
|
: "";
|
||||||
|
return `${index + 1}. ${name} | ${formatVatProbeStatusRu(item.status)}${extra}`;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -2632,6 +2668,77 @@ export function composeFactualReply(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (intent === "vat_liability_confirmed_for_tax_period") {
|
||||||
|
const rowsByMarker = new Map<string, number>();
|
||||||
|
for (const row of rows) {
|
||||||
|
const marker = String(row.registrator ?? "").trim().toUpperCase();
|
||||||
|
if (!marker) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const nextValue = (rowsByMarker.get(marker) ?? 0) + (row.amount ?? 0);
|
||||||
|
rowsByMarker.set(marker, nextValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
const salesVat = rowsByMarker.get("VAT_BOOK_SALES") ?? 0;
|
||||||
|
const purchaseVat = rowsByMarker.get("VAT_BOOK_PURCHASES") ?? 0;
|
||||||
|
const netVat = salesVat - purchaseVat;
|
||||||
|
const vatToPay = Math.max(0, netVat);
|
||||||
|
const carryoverOrOverpayment = Math.max(0, -netVat);
|
||||||
|
const periodWindowLabel =
|
||||||
|
options.periodFrom && options.periodTo ? `${formatDateRu(options.periodFrom)}..${formatDateRu(options.periodTo)}` : null;
|
||||||
|
const formatConfirmedMoney = (value: number): string => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
|
||||||
|
const vatProbe = options.vatDirectSourceProbe ?? null;
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
`Собран подтвержденный расчет НДС к уплате за налоговый период: ${formatConfirmedMoney(vatToPay)}.`,
|
||||||
|
`Налоговый период расчета: ${periodWindowLabel ?? "не задан (нужен явный период)"}.`,
|
||||||
|
`Потенциальный перенос/переплата: ${formatConfirmedMoney(carryoverOrOverpayment)}.`,
|
||||||
|
"Режим результата: подтвержденный расчет по регистрам книг продаж/покупок (tax-period mode, без surrogate-формулы 68/19).",
|
||||||
|
"",
|
||||||
|
"База расчета:",
|
||||||
|
`- Строк агрегата: ${formatNumberWithDots(rows.length)}.`,
|
||||||
|
`- НДС по книге продаж: ${formatConfirmedMoney(salesVat)}.`,
|
||||||
|
`- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`,
|
||||||
|
`- Нетто НДС (книга продаж - книга покупок): ${formatConfirmedMoney(netVat)}.`
|
||||||
|
];
|
||||||
|
|
||||||
|
if (vatProbe && vatProbe.status === "ok") {
|
||||||
|
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
|
||||||
|
const erroredSources = vatProbe.probedSources.filter((item) => item.status === "error").length;
|
||||||
|
lines.push(
|
||||||
|
"",
|
||||||
|
"Покрытие VAT-источников через MCP:",
|
||||||
|
`- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`,
|
||||||
|
`- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`,
|
||||||
|
`- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`,
|
||||||
|
`- Источников с ошибкой запроса: ${formatNumberWithDots(erroredSources)}.`
|
||||||
|
);
|
||||||
|
if (vatProbe.errors.length > 0) {
|
||||||
|
lines.push(`- Ограничения probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
||||||
|
}
|
||||||
|
lines.push("- Сумма расчета выше получена по книгам продаж/покупок; probe использован для контроля полноты VAT-источников.");
|
||||||
|
} else if (vatProbe && vatProbe.status === "error") {
|
||||||
|
lines.push("", "Покрытие VAT-источников через MCP: probe завершился ошибкой, проверьте доступность регистров книг продаж/покупок.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rows.length === 0) {
|
||||||
|
lines.push(
|
||||||
|
"",
|
||||||
|
"За выбранный налоговый период не найдены строки книг продаж/покупок, поэтому подтвержденная сумма к уплате равна 0."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
responseType: "FACTUAL_SUMMARY",
|
||||||
|
text: joinLines(lines),
|
||||||
|
semantics: {
|
||||||
|
result_mode: "confirmed_balance",
|
||||||
|
evidence_strength: "strong",
|
||||||
|
balance_confirmed: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (intent === "vat_payable_confirmed_as_of_date") {
|
if (intent === "vat_payable_confirmed_as_of_date") {
|
||||||
const asOfDate = resolvePayablesAsOfDate(options);
|
const asOfDate = resolvePayablesAsOfDate(options);
|
||||||
const confirmedRows = rows.filter((row) => {
|
const confirmedRows = rows.filter((row) => {
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,10 @@ function hasVatForecastCue(text: string): boolean {
|
||||||
return /(?:прогноз|forecast|прикин|оцен|план)/iu.test(String(text ?? ""));
|
return /(?:прогноз|forecast|прикин|оцен|план)/iu.test(String(text ?? ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasVatTaxPaymentCue(text: string): boolean {
|
||||||
|
return /(?:к\s+уплате|надо|нужно|заплатить|уплатить|плат[её]ж|в\s+налогов|в\s+бюджет)/iu.test(String(text ?? ""));
|
||||||
|
}
|
||||||
|
|
||||||
function hasDocumentSignal(text: string): boolean {
|
function hasDocumentSignal(text: string): boolean {
|
||||||
return /(?:док(?:и|умент|ументы|ументов|ументами)|docs?|documents?|doki|docy|doci)/iu.test(String(text ?? ""));
|
return /(?:док(?:и|умент|ументы|ументов|ументами)|docs?|documents?|doki|docy|doci)/iu.test(String(text ?? ""));
|
||||||
}
|
}
|
||||||
|
|
@ -534,7 +538,12 @@ function mergeFollowupFilters(
|
||||||
const currentHasPeriod = hasExplicitPeriodWindow(merged);
|
const currentHasPeriod = hasExplicitPeriodWindow(merged);
|
||||||
const previousHasPeriod = hasExplicitPeriodWindow(previous);
|
const previousHasPeriod = hasExplicitPeriodWindow(previous);
|
||||||
|
|
||||||
if (intent === "vat_payable_forecast" && previousHasPeriod && hasFollowupSignal && !hasExplicitPeriodInMessage) {
|
if (
|
||||||
|
(intent === "vat_payable_forecast" || intent === "vat_liability_confirmed_for_tax_period") &&
|
||||||
|
previousHasPeriod &&
|
||||||
|
hasFollowupSignal &&
|
||||||
|
!hasExplicitPeriodInMessage
|
||||||
|
) {
|
||||||
const currentPeriodFrom = toNonEmptyString(merged.period_from);
|
const currentPeriodFrom = toNonEmptyString(merged.period_from);
|
||||||
const currentPeriodTo = toNonEmptyString(merged.period_to);
|
const currentPeriodTo = toNonEmptyString(merged.period_to);
|
||||||
const todayIso = new Date().toISOString().slice(0, 10);
|
const todayIso = new Date().toISOString().slice(0, 10);
|
||||||
|
|
@ -570,6 +579,7 @@ function resolveMissingRequiredFilters(intent: AddressIntent, filters: AddressFi
|
||||||
payables_confirmed_as_of_date: ["as_of_date"],
|
payables_confirmed_as_of_date: ["as_of_date"],
|
||||||
receivables_confirmed_as_of_date: ["as_of_date"],
|
receivables_confirmed_as_of_date: ["as_of_date"],
|
||||||
vat_payable_confirmed_as_of_date: ["as_of_date"],
|
vat_payable_confirmed_as_of_date: ["as_of_date"],
|
||||||
|
vat_liability_confirmed_for_tax_period: ["period_from", "period_to"],
|
||||||
list_documents_by_counterparty: ["counterparty"],
|
list_documents_by_counterparty: ["counterparty"],
|
||||||
bank_operations_by_counterparty: ["counterparty"],
|
bank_operations_by_counterparty: ["counterparty"],
|
||||||
list_contracts_by_counterparty: ["counterparty"],
|
list_contracts_by_counterparty: ["counterparty"],
|
||||||
|
|
@ -611,9 +621,11 @@ function deriveIntentWithFollowupContext(
|
||||||
const isVatFollowup = hasVatCue(normalizedMessage);
|
const isVatFollowup = hasVatCue(normalizedMessage);
|
||||||
|
|
||||||
if (detectedIntent.intent === "unknown" && isVatFollowup) {
|
if (detectedIntent.intent === "unknown" && isVatFollowup) {
|
||||||
const vatIntent: AddressIntent = hasVatForecastCue(normalizedMessage)
|
const vatIntent: AddressIntent = hasVatTaxPaymentCue(normalizedMessage)
|
||||||
? "vat_payable_forecast"
|
? "vat_liability_confirmed_for_tax_period"
|
||||||
: "vat_payable_confirmed_as_of_date";
|
: hasVatForecastCue(normalizedMessage)
|
||||||
|
? "vat_payable_forecast"
|
||||||
|
: "vat_payable_confirmed_as_of_date";
|
||||||
return {
|
return {
|
||||||
intent: vatIntent,
|
intent: vatIntent,
|
||||||
confidence: "low",
|
confidence: "low",
|
||||||
|
|
|
||||||
|
|
@ -194,7 +194,8 @@ function inferAggregationProfile(intent: AddressIntent, shape: AddressQueryShape
|
||||||
intent === "documents_forming_balance" ||
|
intent === "documents_forming_balance" ||
|
||||||
intent === "payables_confirmed_as_of_date" ||
|
intent === "payables_confirmed_as_of_date" ||
|
||||||
intent === "receivables_confirmed_as_of_date" ||
|
intent === "receivables_confirmed_as_of_date" ||
|
||||||
intent === "vat_payable_confirmed_as_of_date"
|
intent === "vat_payable_confirmed_as_of_date" ||
|
||||||
|
intent === "vat_liability_confirmed_for_tax_period"
|
||||||
) {
|
) {
|
||||||
return "balance_snapshot";
|
return "balance_snapshot";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3793,6 +3793,7 @@ const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([
|
||||||
"contract_usage_overview",
|
"contract_usage_overview",
|
||||||
"contract_usage_and_value",
|
"contract_usage_and_value",
|
||||||
"vat_payable_forecast",
|
"vat_payable_forecast",
|
||||||
|
"vat_liability_confirmed_for_tax_period",
|
||||||
"vat_payable_confirmed_as_of_date"
|
"vat_payable_confirmed_as_of_date"
|
||||||
]);
|
]);
|
||||||
export function resolveAssistantOrchestrationDecision(input) {
|
export function resolveAssistantOrchestrationDecision(input) {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ export type AddressIntent =
|
||||||
| "supplier_payouts_profile"
|
| "supplier_payouts_profile"
|
||||||
| "contract_usage_and_value"
|
| "contract_usage_and_value"
|
||||||
| "vat_payable_forecast"
|
| "vat_payable_forecast"
|
||||||
|
| "vat_liability_confirmed_for_tax_period"
|
||||||
| "vat_payable_confirmed_as_of_date"
|
| "vat_payable_confirmed_as_of_date"
|
||||||
| "list_contracts_by_counterparty"
|
| "list_contracts_by_counterparty"
|
||||||
| "list_open_contracts"
|
| "list_open_contracts"
|
||||||
|
|
@ -132,6 +133,7 @@ export interface AddressRecipeDefinition {
|
||||||
| "contract_value_profile"
|
| "contract_value_profile"
|
||||||
| "contracts_by_counterparty_profile"
|
| "contracts_by_counterparty_profile"
|
||||||
| "vat_payable_forecast_profile"
|
| "vat_payable_forecast_profile"
|
||||||
|
| "vat_liability_confirmed_tax_period_profile"
|
||||||
| "vat_payable_confirmed_as_of_balance_profile"
|
| "vat_payable_confirmed_as_of_balance_profile"
|
||||||
| "payables_confirmed_as_of_balance_profile"
|
| "payables_confirmed_as_of_balance_profile"
|
||||||
| "receivables_confirmed_as_of_balance_profile";
|
| "receivables_confirmed_as_of_balance_profile";
|
||||||
|
|
|
||||||
|
|
@ -1031,7 +1031,7 @@ describe("address compose stage utf8 headers", () => {
|
||||||
|
|
||||||
expect(reply.text).toContain("Профиль договорной базы собран");
|
expect(reply.text).toContain("Профиль договорной базы собран");
|
||||||
expect(reply.text).toContain("Всего договоров в базе: 520.");
|
expect(reply.text).toContain("Всего договоров в базе: 520.");
|
||||||
expect(reply.text).toContain("<EFBFBD>?Использованных договоров (есть factual связь с операциями): 148.");
|
expect(reply.text).toContain("Использованных договоров (есть factual связь с операциями): 148.");
|
||||||
expect(reply.text).toContain("Неиспользуемых договоров: 372.");
|
expect(reply.text).toContain("Неиспользуемых договоров: 372.");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1458,6 +1458,42 @@ describe("address compose stage utf8 headers", () => {
|
||||||
expect(reply.text).toContain("РегистрНакопления.НДСПродажи");
|
expect(reply.text).toContain("РегистрНакопления.НДСПродажи");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("builds confirmed VAT tax-period reply from sales and purchase book markers", () => {
|
||||||
|
const reply = composeFactualReply(
|
||||||
|
"vat_liability_confirmed_for_tax_period",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
period: "2019-12-31T23:59:59Z",
|
||||||
|
registrator: "VAT_BOOK_SALES",
|
||||||
|
account_dt: "68.02",
|
||||||
|
account_kt: "",
|
||||||
|
amount: 120000,
|
||||||
|
analytics: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
period: "2019-12-31T23:59:59Z",
|
||||||
|
registrator: "VAT_BOOK_PURCHASES",
|
||||||
|
account_dt: "19",
|
||||||
|
account_kt: "",
|
||||||
|
amount: 70000,
|
||||||
|
analytics: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{
|
||||||
|
userMessage: "сколько платить ндс в налоговую за декабрь 2019",
|
||||||
|
periodFrom: "2019-10-01",
|
||||||
|
periodTo: "2019-12-31",
|
||||||
|
useRubCurrency: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
|
||||||
|
expect(reply.text).toContain("Собран подтвержденный расчет НДС к уплате за налоговый период");
|
||||||
|
expect(reply.text).toContain("50.000,00 ₽");
|
||||||
|
expect(reply.semantics?.result_mode).toBe("confirmed_balance");
|
||||||
|
expect(reply.semantics?.balance_confirmed).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("formats VAT forecast amounts in rubles and emphasizes numbers when requested", () => {
|
it("formats VAT forecast amounts in rubles and emphasizes numbers when requested", () => {
|
||||||
const reply = composeFactualReply(
|
const reply = composeFactualReply(
|
||||||
"vat_payable_forecast",
|
"vat_payable_forecast",
|
||||||
|
|
@ -1482,6 +1518,80 @@ describe("address compose stage utf8 headers", () => {
|
||||||
expect(reply.text).toContain("Собран прогноз НДС к уплате:");
|
expect(reply.text).toContain("Собран прогноз НДС к уплате:");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not split dates and list numbering when numeric emphasis is enabled", () => {
|
||||||
|
const reply = composeFactualReply(
|
||||||
|
"vat_payable_forecast",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
period: "2019-12-31T23:59:59Z",
|
||||||
|
registrator: "VAT_68_CREDIT",
|
||||||
|
account_dt: "68",
|
||||||
|
account_kt: "",
|
||||||
|
amount: 0,
|
||||||
|
analytics: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
period: "2019-12-31T23:59:59Z",
|
||||||
|
registrator: "VAT_68_DEBIT",
|
||||||
|
account_dt: "68",
|
||||||
|
account_kt: "",
|
||||||
|
amount: 0,
|
||||||
|
analytics: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{
|
||||||
|
userMessage: "почему по ндс ноль",
|
||||||
|
periodFrom: "2019-12-01",
|
||||||
|
periodTo: "2019-12-31",
|
||||||
|
emphasizeNumbers: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(reply.text).toContain("Период оценки: 01.12.2019..31.12.2019.");
|
||||||
|
expect(reply.text).not.toContain("**01**.**12**.**2019**");
|
||||||
|
expect(reply.text).toContain("1) Проверьте ОСВ/анализ счета");
|
||||||
|
expect(reply.text).not.toContain("**1**)");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps VAT probe timestamps intact when numeric emphasis is enabled", () => {
|
||||||
|
const reply = composeFactualReply(
|
||||||
|
"vat_payable_forecast",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
period: "2019-12-31T23:59:59Z",
|
||||||
|
registrator: "VAT_68_CREDIT",
|
||||||
|
account_dt: "68",
|
||||||
|
account_kt: "",
|
||||||
|
amount: 1000,
|
||||||
|
analytics: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{
|
||||||
|
userMessage: "прикинь ндс",
|
||||||
|
emphasizeNumbers: true,
|
||||||
|
vatDirectSourceProbe: {
|
||||||
|
status: "ok",
|
||||||
|
objectsTotal: 1,
|
||||||
|
documentsTotal: 0,
|
||||||
|
registersTotal: 1,
|
||||||
|
probedSources: [
|
||||||
|
{
|
||||||
|
fullName: "РегистрНакопления.НДСПредъявленный",
|
||||||
|
objectType: "register",
|
||||||
|
status: "ok",
|
||||||
|
rowsFetched: 1,
|
||||||
|
lastPeriod: "2019-12-31T23:59:59Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
errors: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(reply.text).toContain("последнее движение: 2019-12-31T23:59:59Z");
|
||||||
|
expect(reply.text).not.toContain("2019****-12**-31T23:**59**:59Z");
|
||||||
|
});
|
||||||
|
|
||||||
it("adds MCP VAT source probe block for confirmed VAT as-of response", () => {
|
it("adds MCP VAT source probe block for confirmed VAT as-of response", () => {
|
||||||
const reply = composeFactualReply(
|
const reply = composeFactualReply(
|
||||||
"vat_payable_confirmed_as_of_date",
|
"vat_payable_confirmed_as_of_date",
|
||||||
|
|
@ -1972,7 +2082,7 @@ describe("address intent resolver expansion (M2.3a)", () => {
|
||||||
expect(result.reasons).toContain("forecast_tax_signal_detected");
|
expect(result.reasons).toContain("forecast_tax_signal_detected");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resolves colloquial VAT payable estimate wording without explicit 'прогноз'", () => {
|
it("keeps colloquial VAT payment wording in forecast intent when tax-authority cue is absent", () => {
|
||||||
const result = resolveAddressIntent("мож прикинусь плиз скока ндс надо заплатить на 15 марта 2020 года");
|
const result = resolveAddressIntent("мож прикинусь плиз скока ндс надо заплатить на 15 марта 2020 года");
|
||||||
expect(result.intent).toBe("vat_payable_forecast");
|
expect(result.intent).toBe("vat_payable_forecast");
|
||||||
expect(result.reasons).toContain("forecast_tax_signal_detected");
|
expect(result.reasons).toContain("forecast_tax_signal_detected");
|
||||||
|
|
@ -2215,6 +2325,16 @@ describe("address filter extraction for balance drilldown", () => {
|
||||||
expect(extracted.warnings).toContain("period_to_derived_from_as_of_date_for_vat_forecast");
|
expect(extracted.warnings).toContain("period_to_derived_from_as_of_date_for_vat_forecast");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("derives full tax quarter window for confirmed VAT tax-period intent from month phrase", () => {
|
||||||
|
const extracted = extractAddressFilters(
|
||||||
|
"сколько ндс надо заплатить в налоговую за декабрь 2019",
|
||||||
|
"vat_liability_confirmed_for_tax_period"
|
||||||
|
);
|
||||||
|
expect(extracted.extracted_filters.period_from).toBe("2019-10-01");
|
||||||
|
expect(extracted.extracted_filters.period_to).toBe("2019-12-31");
|
||||||
|
expect(extracted.warnings).toContain("period_derived_from_tax_quarter_for_confirmed_vat_liability");
|
||||||
|
});
|
||||||
|
|
||||||
it("derives VAT forecast quarter-to-date window for explicit day+month+year phrase", () => {
|
it("derives VAT forecast quarter-to-date window for explicit day+month+year phrase", () => {
|
||||||
const extracted = extractAddressFilters(
|
const extracted = extractAddressFilters(
|
||||||
"сколько НДС нужно заплатить за 5 марта 2017 года",
|
"сколько НДС нужно заплатить за 5 марта 2017 года",
|
||||||
|
|
@ -2503,7 +2623,7 @@ describe("address filter extraction for balance drilldown", () => {
|
||||||
|
|
||||||
it("repairs mojibake phrase before extracting counterparty filters", () => {
|
it("repairs mojibake phrase before extracting counterparty filters", () => {
|
||||||
const result = extractAddressFilters(
|
const result = extractAddressFilters(
|
||||||
"Показать РґРѕРєСѓР<EFBFBD>?енты РЎР’Рљ Р·Р° 2020 РіРѕРґ.",
|
"Показать документы СВК за 2020 год.",
|
||||||
"list_documents_by_counterparty"
|
"list_documents_by_counterparty"
|
||||||
);
|
);
|
||||||
expect(result.extracted_filters.counterparty).toBe("СВК");
|
expect(result.extracted_filters.counterparty).toBe("СВК");
|
||||||
|
|
@ -3050,10 +3170,11 @@ describe("address query limited taxonomy and stage diagnostics", { timeout: 1500
|
||||||
expect(result?.debug.selected_recipe).toBe("address_vat_payable_forecast_v1");
|
expect(result?.debug.selected_recipe).toBe("address_vat_payable_forecast_v1");
|
||||||
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
|
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
|
||||||
expect(result?.debug.mcp_call_status).not.toBe("skipped");
|
expect(result?.debug.mcp_call_status).not.toBe("skipped");
|
||||||
|
expect(result?.debug.route_expectation_status).toBe("matched");
|
||||||
expect(result?.debug.extracted_filters.counterparty).toBeUndefined();
|
expect(result?.debug.extracted_filters.counterparty).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("routes colloquial VAT payable estimate wording into VAT forecast recipe", async () => {
|
it("routes colloquial VAT payment wording without tax-authority cue into VAT forecast recipe", async () => {
|
||||||
const service = new AddressQueryService();
|
const service = new AddressQueryService();
|
||||||
const result = await service.tryHandle("мож прикинусь плиз скока ндс надо заплатить на 15 марта 2020 года");
|
const result = await service.tryHandle("мож прикинусь плиз скока ндс надо заплатить на 15 марта 2020 года");
|
||||||
expect(result?.handled).toBe(true);
|
expect(result?.handled).toBe(true);
|
||||||
|
|
@ -3061,6 +3182,18 @@ describe("address query limited taxonomy and stage diagnostics", { timeout: 1500
|
||||||
expect(result?.debug.selected_recipe).toBe("address_vat_payable_forecast_v1");
|
expect(result?.debug.selected_recipe).toBe("address_vat_payable_forecast_v1");
|
||||||
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
|
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
|
||||||
expect(result?.debug.mcp_call_status).not.toBe("skipped");
|
expect(result?.debug.mcp_call_status).not.toBe("skipped");
|
||||||
|
expect(result?.debug.route_expectation_status).toBe("matched");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("routes 'в налоговую за декабрь' VAT wording into confirmed tax-period route", async () => {
|
||||||
|
const service = new AddressQueryService();
|
||||||
|
const result = await service.tryHandle("прикинь скок надо заплатить ндс в налоговую на декабрь 2019");
|
||||||
|
expect(result?.handled).toBe(true);
|
||||||
|
expect(result?.debug.detected_intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||||
|
expect(result?.debug.selected_recipe).toBe("address_vat_liability_confirmed_tax_period_v1");
|
||||||
|
expect(result?.debug.extracted_filters.period_from).toBe("2019-10-01");
|
||||||
|
expect(result?.debug.extracted_filters.period_to).toBe("2019-12-31");
|
||||||
|
expect(result?.debug.route_expectation_status).toBe("matched");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("routes customer lifecycle question into dedicated aggregate recipe", async () => {
|
it("routes customer lifecycle question into dedicated aggregate recipe", async () => {
|
||||||
|
|
@ -3648,7 +3781,7 @@ describe("address recipe catalog counterparty filtering", () => {
|
||||||
expect(plan.query).toContain("DOC_TYPE_DOCS");
|
expect(plan.query).toContain("DOC_TYPE_DOCS");
|
||||||
expect(plan.query).toContain("SECTION_DT_OPS");
|
expect(plan.query).toContain("SECTION_DT_OPS");
|
||||||
expect(plan.query).toContain("SECTION_KT_OPS");
|
expect(plan.query).toContain("SECTION_KT_OPS");
|
||||||
expect(plan.query).toContain("СГРУПП<EFBFBD>?РОВАТЬ ПО\n Движения.СчетДт");
|
expect(plan.query).toContain("СГРУППИРОВАТЬ ПО\n Движения.СчетДт");
|
||||||
expect(plan.query).not.toContain("ЛЕВ(Движения.СчетДт.Код, 2)");
|
expect(plan.query).not.toContain("ЛЕВ(Движения.СчетДт.Код, 2)");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -3721,7 +3854,7 @@ describe("address recipe catalog counterparty filtering", () => {
|
||||||
|
|
||||||
expect(plan.recipe.recipe_id).toBe("address_contracts_by_counterparty_v1");
|
expect(plan.recipe.recipe_id).toBe("address_contracts_by_counterparty_v1");
|
||||||
expect(plan.query).toContain("Справочник.ДоговорыКонтрагентов");
|
expect(plan.query).toContain("Справочник.ДоговорыКонтрагентов");
|
||||||
expect(plan.query).toContain("ПРЕДСТАВЛЕН<EFBFBD>?Е(Договоры.Владелец)");
|
expect(plan.query).toContain("ПРЕДСТАВЛЕНИЕ(Договоры.Владелец)");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("selects counterparty lifecycle recipe and keeps activity marker", () => {
|
it("selects counterparty lifecycle recipe and keeps activity marker", () => {
|
||||||
|
|
@ -3756,7 +3889,7 @@ describe("address recipe catalog counterparty filtering", () => {
|
||||||
counterparty: "Жуковка 51",
|
counterparty: "Жуковка 51",
|
||||||
sort: "period_asc"
|
sort: "period_asc"
|
||||||
});
|
});
|
||||||
expect(plan.query).toContain("УПОРЯДОЧ<EFBFBD>?ТЬ ПО");
|
expect(plan.query).toContain("УПОРЯДОЧИТЬ ПО");
|
||||||
expect(plan.query).toContain("Период ВОЗР");
|
expect(plan.query).toContain("Период ВОЗР");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -3822,7 +3955,7 @@ describe("address recipe catalog counterparty filtering", () => {
|
||||||
|
|
||||||
expect(plan.query).toContain("Документ.СписаниеСРасчетногоСчета");
|
expect(plan.query).toContain("Документ.СписаниеСРасчетногоСчета");
|
||||||
expect(plan.query).toContain("Документ.ПоступлениеНаРасчетныйСчет");
|
expect(plan.query).toContain("Документ.ПоступлениеНаРасчетныйСчет");
|
||||||
expect(plan.query).toContain("ПРЕДСТАВЛЕН<EFBFBD>?Е(БанкПоступление.ДоговорКонтрагента) КАК Договор");
|
expect(plan.query).toContain("ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows extended limit for open-contracts intent", () => {
|
it("allows extended limit for open-contracts intent", () => {
|
||||||
|
|
@ -3880,8 +4013,23 @@ describe("address recipe catalog counterparty filtering", () => {
|
||||||
expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Движения.СчетДт.Код, \"\"), 1, 4) = \"68.2\"");
|
expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Движения.СчетДт.Код, \"\"), 1, 4) = \"68.2\"");
|
||||||
expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Движения.СчетДт.Код, \"\"), 1, 2) = \"19\"");
|
expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Движения.СчетДт.Код, \"\"), 1, 2) = \"19\"");
|
||||||
expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Движения.СчетКт.Код, \"\"), 1, 2) = \"19\"");
|
expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Движения.СчетКт.Код, \"\"), 1, 2) = \"19\"");
|
||||||
expect(plan.query).not.toContain("ПРЕДСТАВЛЕН<D095>?Е(Движения.СчетКт) ПОДОБНО");
|
expect(plan.query).not.toContain("ПРЕДСТАВЛЕНИЕ(Движения.СчетКт) ПОДОБНО");
|
||||||
expect(plan.query).not.toContain("ПРЕДСТАВЛЕН<D095>?Е(Движения.СчетДт) ПОДОБНО");
|
expect(plan.query).not.toContain("ПРЕДСТАВЛЕНИЕ(Движения.СчетДт) ПОДОБНО");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds confirmed VAT tax-period query from sales and purchase VAT books", () => {
|
||||||
|
const filters = extractAddressFilters(
|
||||||
|
"сколько ндс надо заплатить в налоговую за декабрь 2019",
|
||||||
|
"vat_liability_confirmed_for_tax_period"
|
||||||
|
).extracted_filters;
|
||||||
|
const selected = selectAddressRecipe("vat_liability_confirmed_for_tax_period", filters);
|
||||||
|
expect(selected.selected_recipe).toBeTruthy();
|
||||||
|
const plan = buildAddressRecipePlan(selected.selected_recipe!, filters);
|
||||||
|
|
||||||
|
expect(plan.query).toContain("РегистрНакопления.НДСЗаписиКнигиПродаж");
|
||||||
|
expect(plan.query).toContain("РегистрНакопления.НДСЗаписиКнигиПокупок");
|
||||||
|
expect(plan.query).toContain("VAT_BOOK_SALES");
|
||||||
|
expect(plan.query).toContain("VAT_BOOK_PURCHASES");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,17 @@ describe("address route expectations contract", () => {
|
||||||
expect(audit.reason).toBe("route_expectation_matched");
|
expect(audit.reason).toBe("route_expectation_matched");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("matches expected recipe and result mode for exact VAT tax-period liability route", () => {
|
||||||
|
const audit = evaluateAddressRouteExpectation({
|
||||||
|
intent: "vat_liability_confirmed_for_tax_period",
|
||||||
|
selectedRecipe: "address_vat_liability_confirmed_tax_period_v1",
|
||||||
|
requestedResultMode: "confirmed_balance",
|
||||||
|
resultMode: "confirmed_balance"
|
||||||
|
});
|
||||||
|
expect(audit.status).toBe("matched");
|
||||||
|
expect(audit.reason).toBe("route_expectation_matched");
|
||||||
|
});
|
||||||
|
|
||||||
it("detects selected recipe mismatch", () => {
|
it("detects selected recipe mismatch", () => {
|
||||||
const audit = evaluateAddressRouteExpectation({
|
const audit = evaluateAddressRouteExpectation({
|
||||||
intent: "payables_confirmed_as_of_date",
|
intent: "payables_confirmed_as_of_date",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue