ДОМЕНЫ - ВОПРОСЫ - НДС: развести as-of и tax-period intent, ускорить и стабилизировать VAT source probe, починить M23 тесты

This commit is contained in:
2026-04-13 08:04:02 +03:00
parent f1ef5f9d3c
commit c4f87222a8
27 changed files with 1065 additions and 189 deletions
@@ -9,7 +9,8 @@ const COMPUTE_EXACT_INTENTS = new Set([
"documents_forming_balance",
"payables_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([
"list_documents_by_counterparty",
@@ -43,6 +44,9 @@ function defaultCapabilityId(intent) {
if (intent === "vat_payable_confirmed_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") {
return "payables_candidates_list";
}
@@ -86,6 +90,14 @@ function resolveCapabilityEnabled(intent) {
: "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") {
return {
enabled: config_1.FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1,
@@ -825,6 +825,9 @@ function requiredFiltersByIntent(intent) {
if (intent === "vat_payable_confirmed_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" ||
intent === "bank_operations_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) {
filters.period_to = new Date().toISOString().slice(0, 10);
warnings.push("period_to_defaulted_today_for_management_profile");
@@ -554,6 +554,31 @@ function hasForecastTaxSignal(text) {
const hasTaxLexeme = /(?:ндс|vat|налог)/iu.test(text);
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) {
const hasVatLexeme = /(?:ндс|vat)/iu.test(text);
if (!hasVatLexeme) {
@@ -1277,6 +1302,13 @@ function hasAccountNumberAnchor(text) {
}
function resolveAddressIntent(userMessage) {
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)) {
return {
intent: "vat_payable_forecast",
+8 -2
View File
@@ -245,7 +245,10 @@ function filterRowsByAccountScope(rows, accountScope) {
async function executeAddressMcpQuery(input) {
const endpoint = buildMcpUrl("/api/execute_query");
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 {
const response = await fetch(endpoint, {
method: "POST",
@@ -305,7 +308,10 @@ async function executeAddressMcpQuery(input) {
async function executeAddressMcpMetadata(input) {
const endpoint = buildMcpUrl("/api/get_metadata");
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 {
const body = {};
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",
payables_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",
list_receivables_counterparties: "counterparty_list",
list_contracts_by_counterparty: "contract_list",
+157 -42
View File
@@ -16,9 +16,15 @@ const ADDRESS_CONFIRMED_PAYABLES_MIN_LIMIT = 200;
const COUNTERPARTY_CATALOG_LOOKUP_LIMIT = 1000;
const COUNTERPARTY_CATALOG_CACHE_TTL_MS = 120_000;
const VAT_METADATA_PROBE_LIMIT = 100;
const VAT_SOURCE_PROBE_MAX_OBJECTS = 8;
const VAT_METADATA_PROBE_TYPES = ["РегистрНакопления", "РегистрСведений", "Документ"];
const VAT_METADATA_PROBE_MASKS = ["ндс", "книгапродаж", "книгапокупок", "счетфактур", "вычет", "восстанов"];
const VAT_SOURCE_PROBE_MAX_OBJECTS = 6;
const VAT_METADATA_PROBE_TYPES = ["РегистрНакопления", "Документ"];
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([
"ооо",
"ао",
@@ -212,6 +218,62 @@ function extractVatMetadataObjects(rows) {
}
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) {
const fullName = item.fullName.toLowerCase();
const synonym = String(item.synonym ?? "").toLowerCase();
@@ -239,8 +301,10 @@ function scoreVatMetadataObject(item) {
}
return score;
}
function buildVatObjectProbeQuery(object, asOfExpr) {
function buildVatObjectProbeQuery(object, asOfExpr, mode = "latest") {
const orderClause = mode === "latest" ? "\nУПОРЯДОЧИТЬ ПО\n Движения.Период УБЫВ" : "";
if (object.objectType === "document") {
const documentOrderClause = mode === "latest" ? "\nУПОРЯДОЧИТЬ ПО\n Док.Дата УБЫВ" : "";
return `
ВЫБРАТЬ ПЕРВЫЕ 1
Док.Дата КАК Период,
@@ -252,9 +316,8 @@ function buildVatObjectProbeQuery(object, asOfExpr) {
${object.fullName} КАК Док
ГДЕ
Док.Дата <= ${asOfExpr}
УПОРЯДОЧИТЬ ПО
Док.Дата УБЫВ
`.trim();
${documentOrderClause}
`.trim().replace(/\n{3,}/g, "\n\n");
}
return `
ВЫБРАТЬ ПЕРВЫЕ 1
@@ -267,9 +330,8 @@ function buildVatObjectProbeQuery(object, asOfExpr) {
${object.fullName} КАК Движения
ГДЕ
Движения.Период <= ${asOfExpr}
УПОРЯДОЧИТЬ ПО
Движения.Период УБЫВ
`.trim();
${orderClause}
`.trim().replace(/\n{3,}/g, "\n\n");
}
async function probeVatDirectSources(filters) {
const asOfDate = normalizeIsoDateForQuery(filters.as_of_date) ??
@@ -301,17 +363,30 @@ async function probeVatDirectSources(filters) {
name_mask: nameMask,
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 metadataObjectsBuffer = [];
for (const [index, response] of metadataResponses.entries()) {
const request = metadataRequests[index];
for (const { request, response } of metadataOutcomes) {
if (response.error) {
metadataErrors.push(`${request.meta_type}:${request.name_mask}:${response.error}`);
continue;
}
const currentSuccessCount = successfulMetadataByType.get(request.meta_type) ?? 0;
successfulMetadataByType.set(request.meta_type, currentSuccessCount + 1);
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();
for (const item of metadataObjectsBuffer) {
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 probeRows = [];
for (const object of metadataObjects) {
const probeQuery = buildVatObjectProbeQuery(object, asOfExpr);
const probeResult = await (0, addressMcpClient_1.executeAddressMcpQuery)({
query: probeQuery,
limit: 1
const probeRows = await mapWithConcurrency(metadataObjects, VAT_OBJECT_PROBE_CONCURRENCY, async (object) => {
let probeResult = await (0, addressMcpClient_1.executeAddressMcpQuery)({
query: buildVatObjectProbeQuery(object, asOfExpr, "latest"),
limit: 1,
timeout_ms: VAT_OBJECT_PROBE_TIMEOUT_MS
});
let fallbackUsed = false;
if (probeResult.error) {
probeRows.push({
fullName: object.fullName,
synonym: object.synonym,
objectType: object.objectType,
status: "error",
rowsFetched: probeResult.fetched_rows,
error: 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 (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 lastPeriod = firstRow !== null
? valueAsString(firstRow.Период ?? firstRow.period).trim() ||
null
? valueAsString(firstRow.Период ?? firstRow.period).trim() || null
: null;
const sampleRegistrator = firstRow !== null
? valueAsString(firstRow.Регистратор ??
firstRow.registrator ??
firstRow.Registrator).trim() || null
: null;
probeRows.push({
return {
fullName: object.fullName,
synonym: object.synonym,
objectType: object.objectType,
status: probeResult.raw_rows.length > 0 ? "ok" : "empty",
rowsFetched: probeResult.fetched_rows,
lastPeriod,
lastPeriod: fallbackUsed ? null : lastPeriod,
sampleRegistrator
});
}
};
});
const status = metadataResponses.every((item) => item.error) ? "error" : "ok";
const allErrors = [
...metadataErrors,
@@ -909,7 +1005,8 @@ function isConfirmedBalanceIntent(intent) {
intent === "documents_forming_balance" ||
intent === "payables_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) {
const asOfDate = normalizeAnalysisDateHint(filters.as_of_date);
@@ -1566,6 +1663,9 @@ function buildLimitedOffers(input) {
else if (input.intent === "vat_payable_confirmed_as_of_date") {
offers.push("показать подтвержденную сумму НДС к уплате на дату среза по счетам 68*");
}
else if (input.intent === "vat_liability_confirmed_for_tax_period") {
offers.push("показать подтвержденный расчет НДС к уплате за налоговый период по книгам продаж/покупок");
}
else if (input.intent === "payables_confirmed_as_of_date") {
offers.push("показать подтвержденный реестр открытых обязательств на дату среза по 60/76");
}
@@ -1613,7 +1713,8 @@ function buildLimitedIntentSignalLine(input) {
list_payables_counterparties: "Сигнал запроса: нужен ранжированный список кредиторов.",
receivables_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 = {
AGGREGATE_LOOKUP: "Сигнал запроса: агрегатный вопрос по периоду/срезу.",
@@ -1745,7 +1846,9 @@ function buildLimitedExecutionResult(input) {
? "exact_receivables_mode_limited_response"
: input.intent.intent === "vat_payable_confirmed_as_of_date"
? "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)
? [...reasonsWithConfirmedFallback, exactLimitedReason]
: reasonsWithConfirmedFallback;
@@ -1973,6 +2076,10 @@ class AddressQueryService {
!baseReasons.includes("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" &&
recipeIntent === "open_items_by_counterparty_or_contract" &&
!baseReasons.includes("confirmed_balance_unavailable_fallback_to_heuristic_candidates")) {
@@ -2899,13 +3006,15 @@ class AddressQueryService {
});
}
const vatProbeRequired = composeIntent === "vat_payable_confirmed_as_of_date" ||
composeIntent === "vat_liability_confirmed_for_tax_period" ||
(composeIntent === "vat_payable_forecast" && shouldProbeVatSourcesForForecast(userMessage));
const vatDirectSourceProbe = vatProbeRequired ? await probeVatDirectSources(executionFilters) : null;
const shouldEmphasizeNumbers = composeIntent === "vat_payable_forecast" ||
composeIntent === "vat_payable_confirmed_as_of_date" ||
composeIntent === "vat_liability_confirmed_for_tax_period" ||
composeIntent === "payables_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, {
vatDirectSourceProbe,
emphasizeNumbers: shouldEmphasizeNumbers,
@@ -2969,13 +3078,17 @@ class AddressQueryService {
}
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 === "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) {
const exactModeName = intent.intent === "payables_confirmed_as_of_date"
? "payables"
: intent.intent === "receivables_confirmed_as_of_date"
? "receivables"
: "vat_payable";
: intent.intent === "vat_liability_confirmed_for_tax_period"
? "vat_tax_period"
: "vat_payable";
return buildLimitedExecutionResult({
mode,
shape,
@@ -3002,7 +3115,9 @@ class AddressQueryService {
reasonText: `exact ${exactModeName} mode: confirmed balance was not proven for the requested as-of slice`,
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/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`],
reasons: [...baseReasons, `exact_${exactModeName}_mode_unconfirmed_output_blocked`],
capabilityAudit,
+88 -51
View File
@@ -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 = [
{
recipe_id: "address_period_coverage_profile_v1",
@@ -582,6 +605,16 @@ const BASE_RECIPES = [
account_scope_mode: "strict",
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",
intent: "list_contracts_by_counterparty",
@@ -887,6 +920,7 @@ function maxLimitForIntent(intent) {
intent === "supplier_payouts_profile" ||
intent === "contract_usage_and_value" ||
intent === "vat_payable_forecast" ||
intent === "vat_liability_confirmed_for_tax_period" ||
intent === "list_contracts_by_counterparty" ||
intent === "list_documents_by_counterparty" ||
intent === "bank_operations_by_counterparty" ||
@@ -926,7 +960,8 @@ function buildAddressRecipePlan(recipe, filters) {
recipe.query_template === "document_section_profile" ||
recipe.query_template === "counterparty_roles_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)
? Math.max(1, Math.min(maxLimit, Math.trunc(filters.limit)))
: recipe.default_limit;
@@ -993,45 +1028,29 @@ function buildAddressRecipePlan(recipe, filters) {
.replaceAll("__VAT68_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", config_1.VAT_PAYABLE_68_PREFIXES))
.replaceAll("__VAT19_DT_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"
? (() => {
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
? toDateTimeExpr(filters.as_of_date, true)
: null) ??
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
? toDateTimeExpr(filters.period_to, true)
: 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"
? (() => {
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
? toDateTimeExpr(filters.as_of_date, true)
: null) ??
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
? toDateTimeExpr(filters.period_from, 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)
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
? toDateTimeExpr(filters.period_to, true)
: null) ??
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
? toDateTimeExpr(filters.period_to, true)
: null) ??
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
? toDateTimeExpr(filters.period_from, true)
: null) ??
"ТЕКУЩАЯДАТА()";
return PAYABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
.replaceAll("__LIMIT__", String(resolvedLimit))
.replaceAll("__AS_OF_EXPR__", asOfExpr)
.replaceAll("__PAYABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"]))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
})()
: recipe.query_template === "receivables_confirmed_as_of_balance_profile"
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
? toDateTimeExpr(filters.period_from, 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)
@@ -1043,23 +1062,41 @@ function buildAddressRecipePlan(recipe, filters) {
? toDateTimeExpr(filters.period_from, true)
: null) ??
"ТЕКУЩАЯДАТА()";
return RECEIVABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
return PAYABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
.replaceAll("__LIMIT__", String(resolvedLimit))
.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));
})()
: MOVEMENTS_QUERY_TEMPLATE
.replace("__LIMIT__", String(resolvedLimit))
.replace("__WHERE_CLAUSE__", (() => {
const extraConditions = [];
const accountCondition = buildMovementAccountCondition(filters);
if (accountCondition) {
extraConditions.push(accountCondition);
}
return buildWhereClause(filters, "Движения.Период", extraConditions);
})())
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
: recipe.query_template === "receivables_confirmed_as_of_balance_profile"
? (() => {
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
? toDateTimeExpr(filters.as_of_date, true)
: null) ??
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
? toDateTimeExpr(filters.period_to, true)
: null) ??
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
? toDateTimeExpr(filters.period_from, true)
: null) ??
"ТЕКУЩАЯДАТА()";
return RECEIVABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
.replaceAll("__LIMIT__", String(resolvedLimit))
.replaceAll("__AS_OF_EXPR__", asOfExpr)
.replaceAll("__RECEIVABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["62", "76"]))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
})()
: MOVEMENTS_QUERY_TEMPLATE
.replace("__LIMIT__", String(resolvedLimit))
.replace("__WHERE_CLAUSE__", (() => {
const extraConditions = [];
const accountCondition = buildMovementAccountCondition(filters);
if (accountCondition) {
extraConditions.push(accountCondition);
}
return buildWhereClause(filters, "Движения.Период", extraConditions);
})())
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
return {
recipe,
query,
@@ -114,6 +114,9 @@ function emphasizeNumericTokens(line) {
if (!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);
return chunks
.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) => {
const before = offset > 0 ? source[offset - 1] : "";
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 === "*") {
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}**`;
});
})
@@ -1997,11 +2014,24 @@ function composeFactualReply(intent, rows, options = {}) {
];
if (vatProbe && vatProbe.status === "ok") {
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)}.`);
if (vatProbe.probedSources.length > 0) {
lines.push(...vatProbe.probedSources.slice(0, 6).map((item, index) => {
const statusRank = (status) => 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("", "Покрытие 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;
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) {
@@ -2044,6 +2074,61 @@ function composeFactualReply(intent, rows, options = {}) {
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") {
const asOfDate = resolvePayablesAsOfDate(options);
const confirmedRows = rows.filter((row) => {
@@ -36,6 +36,9 @@ function hasVatCue(text) {
function hasVatForecastCue(text) {
return /(?:прогноз|forecast|прикин|оцен|план)/iu.test(String(text ?? ""));
}
function hasVatTaxPaymentCue(text) {
return /(?:к\s+уплате|надо|нужно|заплатить|уплатить|плат[её]ж|в\s+налогов|в\s+бюджет)/iu.test(String(text ?? ""));
}
function hasDocumentSignal(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 currentHasPeriod = hasExplicitPeriodWindow(merged);
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 currentPeriodTo = toNonEmptyString(merged.period_to);
const todayIso = new Date().toISOString().slice(0, 10);
@@ -465,6 +471,7 @@ function resolveMissingRequiredFilters(intent, filters) {
payables_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_liability_confirmed_for_tax_period: ["period_from", "period_to"],
list_documents_by_counterparty: ["counterparty"],
bank_operations_by_counterparty: ["counterparty"],
list_contracts_by_counterparty: ["counterparty"],
@@ -497,9 +504,11 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
const hasAnyPartyAnchor = hasPreviousContract || hasPreviousCounterparty;
const isVatFollowup = hasVatCue(normalizedMessage);
if (detectedIntent.intent === "unknown" && isVatFollowup) {
const vatIntent = hasVatForecastCue(normalizedMessage)
? "vat_payable_forecast"
: "vat_payable_confirmed_as_of_date";
const vatIntent = hasVatTaxPaymentCue(normalizedMessage)
? "vat_liability_confirmed_for_tax_period"
: hasVatForecastCue(normalizedMessage)
? "vat_payable_forecast"
: "vat_payable_confirmed_as_of_date";
return {
intent: vatIntent,
confidence: "low",
@@ -95,7 +95,8 @@ function inferAggregationProfile(intent, shape) {
intent === "documents_forming_balance" ||
intent === "payables_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";
}
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_and_value",
"vat_payable_forecast",
"vat_liability_confirmed_for_tax_period",
"vat_payable_confirmed_as_of_date"
]);
function resolveAssistantOrchestrationDecision(input) {