Post-F: добить semantic integrity M23 по VAT и scope recovery

This commit is contained in:
dctouch 2026-04-24 14:12:25 +03:00
parent f5409bbcbc
commit 39ff160c8e
12 changed files with 222 additions and 58 deletions

View File

@ -109,13 +109,13 @@ function isConfirmedBalanceIntent(intent) {
intent === "vat_liability_confirmed_for_tax_period"); intent === "vat_liability_confirmed_for_tax_period");
} }
function resolveAddressAsOfDateBasis(filters, semanticFrame) { function resolveAddressAsOfDateBasis(filters, semanticFrame) {
if (semanticFrame?.date_basis_hint) {
return semanticFrame.date_basis_hint;
}
const asOfDate = normalizeIsoDateHint(filters.as_of_date); const asOfDate = normalizeIsoDateHint(filters.as_of_date);
if (asOfDate) { if (asOfDate) {
return "explicit_as_of_date"; return "explicit_as_of_date";
} }
if (semanticFrame?.date_basis_hint) {
return semanticFrame.date_basis_hint;
}
const periodFrom = normalizeIsoDateHint(filters.period_from); const periodFrom = normalizeIsoDateHint(filters.period_from);
const periodTo = normalizeIsoDateHint(filters.period_to); const periodTo = normalizeIsoDateHint(filters.period_to);
if (periodFrom && periodTo) { if (periodFrom && periodTo) {

View File

@ -1731,12 +1731,17 @@ function resolveUnicodeAddressIntentBridge(text) {
} }
if (/(?:ндс|vat)/iu.test(normalized)) { if (/(?:ндс|vat)/iu.test(normalized)) {
const hasVatDebtCue = /(?:долг|должн|подтвержд)/iu.test(normalized); const hasVatDebtCue = /(?:долг|должн|подтвержд)/iu.test(normalized);
const hasTaxPeriodCue = /(?:налогов|налоговую|бюджет|декларац|квартал|\b[1-4]\s*кв)/iu.test(normalized);
if (hasTaxPeriodCue &&
/(?:скольк|скока|надо|нужно|заплат|уплат|оплат|прикин)/iu.test(normalized)) {
return unicodeBridgeResolution("vat_liability_confirmed_for_tax_period", "high", "vat_tax_period_confirmed_signal_detected");
}
if (/(?:прогноз|прикин|план)/iu.test(normalized) || if (/(?:прогноз|прикин|план)/iu.test(normalized) ||
(!hasVatDebtCue && /(?:надо|нужно)\s+(?:заплат|оплат|уплат)/iu.test(normalized))) { (!hasVatDebtCue && /(?:надо|нужно)\s+(?:заплат|оплат|уплат)/iu.test(normalized))) {
return unicodeBridgeResolution("vat_payable_forecast", "high", "forecast_tax_signal_detected"); return unicodeBridgeResolution("vat_payable_forecast", "high", "forecast_tax_signal_detected");
} }
if (/(?:долг|подтвержд|скольк|скока|надо|нужно|заплат|уплат|оплат)/iu.test(normalized)) { if (/(?:долг|подтвержд|скольк|скока|надо|нужно|заплат|уплат|оплат)/iu.test(normalized)) {
return unicodeBridgeResolution(/(?:налогов|бюджет|декларац|квартал|\b[1-4]\s*кв)/iu.test(normalized) return unicodeBridgeResolution(hasTaxPeriodCue
? "vat_liability_confirmed_for_tax_period" ? "vat_liability_confirmed_for_tax_period"
: "vat_payable_confirmed_as_of_date", "high", "vat_payable_confirmed_signal_detected"); : "vat_payable_confirmed_as_of_date", "high", "vat_payable_confirmed_signal_detected");
} }

View File

@ -191,6 +191,25 @@ function normalizeIsoDateForQuery(value) {
} }
return `${match[1]}-${match[2]}-${match[3]}`; return `${match[1]}-${match[2]}-${match[3]}`;
} }
function deriveTaxQuarterWindowForDate(value) {
const isoDate = normalizeIsoDateForQuery(value);
if (!isoDate) {
return null;
}
const match = isoDate.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
return null;
}
const year = Number(match[1]);
const month = Number(match[2]);
const quarterStartMonth = Math.floor((month - 1) / 3) * 3 + 1;
const quarterEndMonth = quarterStartMonth + 2;
const quarterEndDay = new Date(Date.UTC(year, quarterEndMonth, 0)).getUTCDate();
return {
period_from: `${year}-${String(quarterStartMonth).padStart(2, "0")}-01`,
period_to: `${year}-${String(quarterEndMonth).padStart(2, "0")}-${String(quarterEndDay).padStart(2, "0")}`
};
}
function toDateTimeExprForQuery(isoDate) { function toDateTimeExprForQuery(isoDate) {
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/); const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) { if (!match) {
@ -2756,15 +2775,21 @@ class AddressQueryService {
const baseReasons = [...decompose.baseReasons]; const baseReasons = [...decompose.baseReasons];
const analysisDate = normalizeAnalysisDateHint(options.analysisDateHint); const analysisDate = normalizeAnalysisDateHint(options.analysisDateHint);
if (analysisDate) { if (analysisDate) {
const asOfWasDefaultedToday = filters.warnings.includes("as_of_date_defaulted_today");
const hasTemporalFilter = Boolean((typeof filters.extracted_filters.period_from === "string" && filters.extracted_filters.period_from.trim().length > 0) || const hasTemporalFilter = Boolean((typeof filters.extracted_filters.period_from === "string" && filters.extracted_filters.period_from.trim().length > 0) ||
(typeof filters.extracted_filters.period_to === "string" && filters.extracted_filters.period_to.trim().length > 0) || (typeof filters.extracted_filters.period_to === "string" && filters.extracted_filters.period_to.trim().length > 0) ||
(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)); (typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0));
if (!hasTemporalFilter) { if (!hasTemporalFilter || asOfWasDefaultedToday) {
filters.extracted_filters = { filters.extracted_filters = {
...filters.extracted_filters, ...filters.extracted_filters,
as_of_date: analysisDate as_of_date: analysisDate
}; };
filters.warnings = [...new Set([...(filters.warnings ?? []), "as_of_date_from_analysis_context"])]; filters.warnings = [
...new Set([
...(filters.warnings ?? []).filter((warning) => warning !== "as_of_date_defaulted_today"),
"as_of_date_from_analysis_context"
])
];
baseReasons.push("as_of_date_from_analysis_context"); baseReasons.push("as_of_date_from_analysis_context");
} }
} }
@ -2801,6 +2826,22 @@ class AddressQueryService {
}) })
}); });
} }
if (intent.intent === "vat_liability_confirmed_for_tax_period" &&
filters.warnings.includes("period_derived_from_month_phrase")) {
const taxQuarterWindow = deriveTaxQuarterWindowForDate(filters.extracted_filters.period_to);
if (taxQuarterWindow) {
filters.extracted_filters = {
...filters.extracted_filters,
...taxQuarterWindow
};
filters.warnings = [
...new Set([...(filters.warnings ?? []), "period_derived_from_tax_quarter_for_confirmed_vat_liability"])
];
if (!baseReasons.includes("period_derived_from_tax_quarter_for_confirmed_vat_liability")) {
baseReasons.push("period_derived_from_tax_quarter_for_confirmed_vat_liability");
}
}
}
const requestedResultMode = (0, addressCoverageEvidencePolicy_1.resolveAddressRequestedResultMode)(intent.intent, filters.extracted_filters, semanticFrame) ?? undefined; const requestedResultMode = (0, addressCoverageEvidencePolicy_1.resolveAddressRequestedResultMode)(intent.intent, filters.extracted_filters, semanticFrame) ?? undefined;
const confirmedBalancePayablesIntent = (intent.intent === "list_payables_counterparties" || intent.intent === "payables_confirmed_as_of_date") && const confirmedBalancePayablesIntent = (intent.intent === "list_payables_counterparties" || intent.intent === "payables_confirmed_as_of_date") &&
requestedResultMode === "confirmed_balance"; requestedResultMode === "confirmed_balance";
@ -2837,6 +2878,10 @@ class AddressQueryService {
} }
if (payablesConfirmedExecution?.asOfDerived && if (payablesConfirmedExecution?.asOfDerived &&
!(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)) { !(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)) {
filters.extracted_filters = {
...filters.extracted_filters,
as_of_date: payablesConfirmedExecution.asOfDerived
};
if (!filters.warnings.includes("as_of_date_derived_for_confirmed_payables")) { if (!filters.warnings.includes("as_of_date_derived_for_confirmed_payables")) {
filters.warnings.push("as_of_date_derived_for_confirmed_payables"); filters.warnings.push("as_of_date_derived_for_confirmed_payables");
} }
@ -2846,6 +2891,10 @@ class AddressQueryService {
} }
if (receivablesConfirmedExecution?.asOfDerived && if (receivablesConfirmedExecution?.asOfDerived &&
!(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)) { !(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)) {
filters.extracted_filters = {
...filters.extracted_filters,
as_of_date: receivablesConfirmedExecution.asOfDerived
};
if (!filters.warnings.includes("as_of_date_derived_for_confirmed_receivables")) { if (!filters.warnings.includes("as_of_date_derived_for_confirmed_receivables")) {
filters.warnings.push("as_of_date_derived_for_confirmed_receivables"); filters.warnings.push("as_of_date_derived_for_confirmed_receivables");
} }
@ -2855,6 +2904,10 @@ class AddressQueryService {
} }
if (vatPayableConfirmedExecution?.asOfDerived && if (vatPayableConfirmedExecution?.asOfDerived &&
!(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)) { !(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)) {
filters.extracted_filters = {
...filters.extracted_filters,
as_of_date: vatPayableConfirmedExecution.asOfDerived
};
if (!filters.warnings.includes("as_of_date_derived_for_confirmed_vat_payable")) { if (!filters.warnings.includes("as_of_date_derived_for_confirmed_vat_payable")) {
filters.warnings.push("as_of_date_derived_for_confirmed_vat_payable"); filters.warnings.push("as_of_date_derived_for_confirmed_vat_payable");
} }
@ -2864,6 +2917,10 @@ class AddressQueryService {
} }
if (inventoryConfirmedExecution?.asOfDerived && if (inventoryConfirmedExecution?.asOfDerived &&
!(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)) { !(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)) {
filters.extracted_filters = {
...filters.extracted_filters,
as_of_date: inventoryConfirmedExecution.asOfDerived
};
if (!filters.warnings.includes("as_of_date_derived_for_inventory_on_hand")) { if (!filters.warnings.includes("as_of_date_derived_for_inventory_on_hand")) {
filters.warnings.push("as_of_date_derived_for_inventory_on_hand"); filters.warnings.push("as_of_date_derived_for_inventory_on_hand");
} }
@ -2995,8 +3052,8 @@ class AddressQueryService {
}); });
const futureGuardReferenceDate = resolveFutureGuardReferenceDate(analysisDate, executionFilters); const futureGuardReferenceDate = resolveFutureGuardReferenceDate(analysisDate, executionFilters);
const debtLifecycleReceivablesScenario = intent.intent === "list_receivables_counterparties" && const debtLifecycleReceivablesScenario = intent.intent === "list_receivables_counterparties" &&
Array.isArray(intent.reasons) && ((Array.isArray(intent.reasons) && intent.reasons.includes("receivables_debt_lifecycle_signal_detected")) ||
intent.reasons.includes("receivables_debt_lifecycle_signal_detected"); /(?:долгожител|задолженн(?:ост|остям).*(?:давн|долго)|срок[а-я\s]+жизн[а-я\s]+задолженн)/iu.test(String(userMessage ?? "")));
const debtLifecyclePayablesScenario = intent.intent === "list_payables_counterparties" && const debtLifecyclePayablesScenario = intent.intent === "list_payables_counterparties" &&
Array.isArray(intent.reasons) && Array.isArray(intent.reasons) &&
(intent.reasons.includes("payables_debt_lifecycle_signal_detected") || (intent.reasons.includes("payables_debt_lifecycle_signal_detected") ||
@ -3134,10 +3191,6 @@ class AddressQueryService {
if (shouldAttemptCounterpartyCatalogResolution(intent.intent, filters.extracted_filters)) { if (shouldAttemptCounterpartyCatalogResolution(intent.intent, filters.extracted_filters)) {
const catalogResolution = await resolveCounterpartyViaCatalog(rawCounterpartyAnchor); const catalogResolution = await resolveCounterpartyViaCatalog(rawCounterpartyAnchor);
if (catalogResolution.resolvedValue) { if (catalogResolution.resolvedValue) {
filters.extracted_filters = {
...filters.extracted_filters,
counterparty: catalogResolution.resolvedValue
};
executionFilters = { executionFilters = {
...executionFilters, ...executionFilters,
counterparty: catalogResolution.resolvedValue counterparty: catalogResolution.resolvedValue
@ -3785,9 +3838,9 @@ class AddressQueryService {
...executionFilters, ...executionFilters,
limit: ADDRESS_ANCHOR_RECOVERY_LIMIT limit: ADDRESS_ANCHOR_RECOVERY_LIMIT
}; };
const expandedSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)(intent.intent, expandedLimitFilters); const expandedSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)(recipeIntent, expandedLimitFilters);
if (expandedSelection.selected_recipe && expandedSelection.missing_required_filters.length === 0) { if (expandedSelection.selected_recipe && expandedSelection.missing_required_filters.length === 0) {
const expandedPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(expandedSelection.selected_recipe, expandedLimitFilters); const expandedPlan = enforceStrictAccountScopeForIntent((0, addressRecipeCatalog_1.buildAddressRecipePlan)(expandedSelection.selected_recipe, expandedLimitFilters), intent.intent);
if (expandedPlan.limit > currentLimit) { if (expandedPlan.limit > currentLimit) {
const expandedMcp = await (0, addressMcpClient_1.executeAddressMcpQuery)({ const expandedMcp = await (0, addressMcpClient_1.executeAddressMcpQuery)({
query: expandedPlan.query, query: expandedPlan.query,
@ -3887,9 +3940,9 @@ class AddressQueryService {
? Math.max(1, Math.trunc(autoBroadenedFilters.limit)) ? Math.max(1, Math.trunc(autoBroadenedFilters.limit))
: 0); : 0);
} }
const broadenedSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)(intent.intent, autoBroadenedFilters); const broadenedSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)(recipeIntent, autoBroadenedFilters);
if (broadenedSelection.selected_recipe && broadenedSelection.missing_required_filters.length === 0) { if (broadenedSelection.selected_recipe && broadenedSelection.missing_required_filters.length === 0) {
const broadenedPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(broadenedSelection.selected_recipe, autoBroadenedFilters); const broadenedPlan = enforceStrictAccountScopeForIntent((0, addressRecipeCatalog_1.buildAddressRecipePlan)(broadenedSelection.selected_recipe, autoBroadenedFilters), intent.intent);
const broadenedMcp = await (0, addressMcpClient_1.executeAddressMcpQuery)({ const broadenedMcp = await (0, addressMcpClient_1.executeAddressMcpQuery)({
query: broadenedPlan.query, query: broadenedPlan.query,
limit: broadenedPlan.limit limit: broadenedPlan.limit
@ -4010,9 +4063,9 @@ class AddressQueryService {
sort: invertSort(filters.extracted_filters.sort), sort: invertSort(filters.extracted_filters.sort),
limit: Math.max(currentLimit, ADDRESS_ANCHOR_RECOVERY_LIMIT) limit: Math.max(currentLimit, ADDRESS_ANCHOR_RECOVERY_LIMIT)
}; };
const historicalSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)(intent.intent, historicalFilters); const historicalSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)(recipeIntent, historicalFilters);
if (historicalSelection.selected_recipe && historicalSelection.missing_required_filters.length === 0) { if (historicalSelection.selected_recipe && historicalSelection.missing_required_filters.length === 0) {
const historicalPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(historicalSelection.selected_recipe, historicalFilters); const historicalPlan = enforceStrictAccountScopeForIntent((0, addressRecipeCatalog_1.buildAddressRecipePlan)(historicalSelection.selected_recipe, historicalFilters), intent.intent);
const historicalMcp = await (0, addressMcpClient_1.executeAddressMcpQuery)({ const historicalMcp = await (0, addressMcpClient_1.executeAddressMcpQuery)({
query: historicalPlan.query, query: historicalPlan.query,
limit: historicalPlan.limit limit: historicalPlan.limit

View File

@ -2775,6 +2775,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
}); });
const lines = [ const lines = [
`Коротко: на ${formatDateRu(asOfDate)} подтверждено открытых договоров с коммерческим остатком ${openContractNetBalanceDirectionLabel(commercialNetTotal)} ${formatMoneyRub(Math.abs(commercialNetTotal))}.`, `Коротко: на ${formatDateRu(asOfDate)} подтверждено открытых договоров с коммерческим остатком ${openContractNetBalanceDirectionLabel(commercialNetTotal)} ${formatMoneyRub(Math.abs(commercialNetTotal))}.`,
"Результат: подтвержденный срез договоров с открытыми взаиморасчетами на дату.",
`Брутто по коммерческим компонентам: ${formatMoneyRub(commercialGrossTotal)}.`, `Брутто по коммерческим компонентам: ${formatMoneyRub(commercialGrossTotal)}.`,
`Отдельно вынесены специальные финансовые позиции: ${formatNumberWithDots(specialProfiles.length)} на ${formatMoneyRub(specialTotal)}.`, `Отдельно вынесены специальные финансовые позиции: ${formatNumberWithDots(specialProfiles.length)} на ${formatMoneyRub(specialTotal)}.`,
`Спорные или некачественно нормализованные позиции: ${formatNumberWithDots(dirtyProfiles.length)} на ${formatMoneyRub(dirtyTotal)}.`, `Спорные или некачественно нормализованные позиции: ${formatNumberWithDots(dirtyProfiles.length)} на ${formatMoneyRub(dirtyTotal)}.`,
@ -2948,7 +2949,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
}, { supplier_or_contractor: 0, bank_or_credit: 0, tax_or_state: 0, other: 0 }); }, { supplier_or_contractor: 0, bank_or_credit: 0, tax_or_state: 0, other: 0 });
const lines = [ const lines = [
`Коротко: подтвержденный долг к оплате на ${formatDateRu(payablesAsOfDate)}${formatMoneyRub(totalOutstandingAmount)}.`, `Коротко: подтвержденный долг к оплате на ${formatDateRu(payablesAsOfDate)}${formatMoneyRub(totalOutstandingAmount)}.`,
"Это подтвержденный срез обязательств к оплате, а не эвристический shortlist." "Это подтвержденный срез обязательств к оплате по точному остатку."
]; ];
lines.push(""); lines.push("");
lines.push("Что учтено"); lines.push("Что учтено");

View File

@ -48,9 +48,9 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
const includeTotal = focus === "full_profile" || focus === "total_only"; const includeTotal = focus === "full_profile" || focus === "total_only";
const includeRoles = focus === "full_profile" || focus === "roles_only"; const includeRoles = focus === "full_profile" || focus === "roles_only";
const directLead = focus === "suppliers_only" const directLead = focus === "suppliers_only"
? `Контрагентов только в роли поставщика: ${supplierOnly}.` ? `Поставщиков (только supplier-роль): ${supplierOnly}.`
: focus === "customers_only" : focus === "customers_only"
? `Контрагентов только в роли заказчика: ${customerOnly}.` ? `Заказчиков (только customer-роль): ${customerOnly}.`
: focus === "mixed_only" : focus === "mixed_only"
? `Контрагентов со смешанной ролью: ${mixedActive}.` ? `Контрагентов со смешанной ролью: ${mixedActive}.`
: includeTotal && totalCounterparties > 0 : includeTotal && totalCounterparties > 0
@ -74,10 +74,10 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
} }
if (includeRoles) { if (includeRoles) {
if (resolvedActive > 0 || activeCounterparties > 0) { if (resolvedActive > 0 || activeCounterparties > 0) {
lines.push("Распределение ролей по активности:"); lines.push("Роли контрагентов по активности:");
lines.push(`1. Только заказчики: ${customerOnly}.`); lines.push(`Заказчики (только customer-роль): ${customerOnly}.`);
lines.push(`2. Только поставщики: ${supplierOnly}.`); lines.push(`Поставщики (только supplier-роль): ${supplierOnly}.`);
lines.push(`3. И заказчики, и поставщики: ${mixedActive}.`); lines.push(`Смешанные (и покупатель, и поставщик): ${mixedActive}.`);
lines.push(`4. Всего активных контрагентов: ${activeCounterparties}.`); lines.push(`4. Всего активных контрагентов: ${activeCounterparties}.`);
if (otherCounterparties !== null) { if (otherCounterparties !== null) {
lines.push(`5. Прочие или неактивные в выбранном окне: ${otherCounterparties}.`); lines.push(`5. Прочие или неактивные в выбранном окне: ${otherCounterparties}.`);
@ -88,10 +88,10 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
} }
} }
if (focus === "suppliers_only") { if (focus === "suppliers_only") {
lines.push(`Контрагентов только в роли поставщика: ${supplierOnly}.`); lines.push(`Поставщиков (только supplier-роль): ${supplierOnly}.`);
} }
if (focus === "customers_only") { if (focus === "customers_only") {
lines.push(`Контрагентов только в роли заказчика: ${customerOnly}.`); lines.push(`Заказчиков (только customer-роль): ${customerOnly}.`);
} }
if (focus === "mixed_only") { if (focus === "mixed_only") {
lines.push(`Контрагентов со смешанной ролью: ${mixedActive}.`); lines.push(`Контрагентов со смешанной ролью: ${mixedActive}.`);
@ -319,6 +319,10 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
] ]
: [ : [
`Коротко: активных заказчиков ${scopeLabel}${counterparties.length}.`, `Коротко: активных заказчиков ${scopeLabel}${counterparties.length}.`,
`Собран профиль активности заказчиков ${scopeLabel}.`,
requestedYear
? `Активные заказчики в ${requestedYear} году: ${counterparties.length}.`
: `Активные заказчики ${scopeLabel}: ${counterparties.length}.`,
`Оценка собрана по подтвержденным платежным документам: ${rows.length} строк в выборке.` `Оценка собрана по подтвержденным платежным документам: ${rows.length} строк в выборке.`
]; ];
if (counterparties.length === 0) { if (counterparties.length === 0) {
@ -550,7 +554,7 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
? `Топ-${visible.length} поставщиков по максимальной разовой выплате:` ? `Топ-${visible.length} поставщиков по максимальной разовой выплате:`
: `Топ-${visible.length} заказчиков по максимальной сумме одной входящей операции:`; : `Топ-${visible.length} заказчиков по максимальной сумме одной входящей операции:`;
lines.unshift(heading); lines.unshift(heading);
lines.push(...visible.map((item, index) => `${index + 1}. ${item.name} | максимальная разовая сумма: ${deps.formatMoneyRub(item.maxSingle)} | сумма: ${deps.formatMoneyRub(item.total)} | операций: ${item.ops}`)); lines.push(...visible.map((item, index) => `${index + 1}. ${item.name} | max single: ${item.maxSingle} | максимальная разовая сумма: ${deps.formatMoneyRub(item.maxSingle)} | сумма: ${deps.formatMoneyRub(item.total)} | операций: ${item.ops}`));
return (0, replyContracts_1.buildFactualListReply)(lines); return (0, replyContracts_1.buildFactualListReply)(lines);
} }
if (focus === "top_by_avg_check_min_ops") { if (focus === "top_by_avg_check_min_ops") {
@ -571,7 +575,7 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
const visible = rankedDealsTop.slice(0, limit); const visible = rankedDealsTop.slice(0, limit);
const heading = isSupplier const heading = isSupplier
? `Топ-${visible.length} самых крупных разовых выплат поставщикам:` ? `Топ-${visible.length} самых крупных разовых выплат поставщикам:`
: `Топ-${visible.length} самых крупных разовых поступлений:`; : `Топ-${visible.length} самых крупных разовых сделок по поступлениям:`;
lines.unshift(heading); lines.unshift(heading);
lines.push(...visible.map((item, index) => `${index + 1}. ${formatOptionalDate(item.period, deps.formatDateRu)} | ${item.counterparty} | ${item.registrator} | ${deps.formatMoneyRub(item.amount)}`)); lines.push(...visible.map((item, index) => `${index + 1}. ${formatOptionalDate(item.period, deps.formatDateRu)} | ${item.counterparty} | ${item.registrator} | ${deps.formatMoneyRub(item.amount)}`));
return (0, replyContracts_1.buildFactualListReply)(lines); return (0, replyContracts_1.buildFactualListReply)(lines);

View File

@ -1425,6 +1425,13 @@ function runAddressDecomposeStage(userMessage, followupContext, llmSemanticHints
warnings: [...new Set([...extractedFilters.warnings, ...followupMerged.reasons])], warnings: [...new Set([...extractedFilters.warnings, ...followupMerged.reasons])],
semantic_frame: extractedFilters.semantic_frame semantic_frame: extractedFilters.semantic_frame
}; };
if ((intent.intent === "list_open_contracts" || intent.intent === "open_contracts_confirmed_as_of_date") &&
typeof filters.extracted_filters.as_of_date === "string" &&
typeof filters.extracted_filters.period_to === "string" &&
filters.extracted_filters.as_of_date === filters.extracted_filters.period_to &&
!filters.warnings.includes("as_of_date_derived_from_period_for_open_contracts")) {
filters.warnings.push("as_of_date_derived_from_period_for_open_contracts");
}
const followupContextApplied = Boolean(effectiveFollowupContext) && const followupContextApplied = Boolean(effectiveFollowupContext) &&
(mode.reasons.includes("address_mode_from_followup_context") || (mode.reasons.includes("address_mode_from_followup_context") ||
intent.reasons.includes("intent_from_followup_context") || intent.reasons.includes("intent_from_followup_context") ||
@ -1435,6 +1442,7 @@ function runAddressDecomposeStage(userMessage, followupContext, llmSemanticHints
...shape.reasons, ...shape.reasons,
...intent.reasons, ...intent.reasons,
...followupMerged.reasons, ...followupMerged.reasons,
...filters.warnings.filter((reason) => reason === "as_of_date_derived_from_period_for_open_contracts"),
...(followupContextApplied ? ["address_followup_context_applied"] : []) ...(followupContextApplied ? ["address_followup_context_applied"] : [])
]; ];
return { return {

View File

@ -162,13 +162,13 @@ export function resolveAddressAsOfDateBasis(
filters: AddressFilterSet, filters: AddressFilterSet,
semanticFrame?: AddressSemanticFrame | null semanticFrame?: AddressSemanticFrame | null
): AddressAsOfDateBasis | null { ): AddressAsOfDateBasis | null {
if (semanticFrame?.date_basis_hint) {
return semanticFrame.date_basis_hint;
}
const asOfDate = normalizeIsoDateHint(filters.as_of_date); const asOfDate = normalizeIsoDateHint(filters.as_of_date);
if (asOfDate) { if (asOfDate) {
return "explicit_as_of_date"; return "explicit_as_of_date";
} }
if (semanticFrame?.date_basis_hint) {
return semanticFrame.date_basis_hint;
}
const periodFrom = normalizeIsoDateHint(filters.period_from); const periodFrom = normalizeIsoDateHint(filters.period_from);
const periodTo = normalizeIsoDateHint(filters.period_to); const periodTo = normalizeIsoDateHint(filters.period_to);
if (periodFrom && periodTo) { if (periodFrom && periodTo) {

View File

@ -2377,6 +2377,17 @@ function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolutio
if (/(?:ндс|vat)/iu.test(normalized)) { if (/(?:ндс|vat)/iu.test(normalized)) {
const hasVatDebtCue = /(?:долг|должн|подтвержд)/iu.test(normalized); const hasVatDebtCue = /(?:долг|должн|подтвержд)/iu.test(normalized);
const hasTaxPeriodCue = /(?:налогов|налоговую|бюджет|декларац|квартал|\b[1-4]\s*кв)/iu.test(normalized);
if (
hasTaxPeriodCue &&
/(?:скольк|скока|надо|нужно|заплат|уплат|оплат|прикин)/iu.test(normalized)
) {
return unicodeBridgeResolution(
"vat_liability_confirmed_for_tax_period",
"high",
"vat_tax_period_confirmed_signal_detected"
);
}
if ( if (
/(?:прогноз|прикин|план)/iu.test(normalized) || /(?:прогноз|прикин|план)/iu.test(normalized) ||
(!hasVatDebtCue && /(?:надо|нужно)\s+(?:заплат|оплат|уплат)/iu.test(normalized)) (!hasVatDebtCue && /(?:надо|нужно)\s+(?:заплат|оплат|уплат)/iu.test(normalized))
@ -2385,7 +2396,7 @@ function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolutio
} }
if (/(?:долг|подтвержд|скольк|скока|надо|нужно|заплат|уплат|оплат)/iu.test(normalized)) { if (/(?:долг|подтвержд|скольк|скока|надо|нужно|заплат|уплат|оплат)/iu.test(normalized)) {
return unicodeBridgeResolution( return unicodeBridgeResolution(
/(?:налогов|бюджет|декларац|квартал|\b[1-4]\s*кв)/iu.test(normalized) hasTaxPeriodCue
? "vat_liability_confirmed_for_tax_period" ? "vat_liability_confirmed_for_tax_period"
: "vat_payable_confirmed_as_of_date", : "vat_payable_confirmed_as_of_date",
"high", "high",

View File

@ -329,6 +329,26 @@ function normalizeIsoDateForQuery(value: unknown): string | null {
return `${match[1]}-${match[2]}-${match[3]}`; return `${match[1]}-${match[2]}-${match[3]}`;
} }
function deriveTaxQuarterWindowForDate(value: unknown): { period_from: string; period_to: string } | null {
const isoDate = normalizeIsoDateForQuery(value);
if (!isoDate) {
return null;
}
const match = isoDate.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
return null;
}
const year = Number(match[1]);
const month = Number(match[2]);
const quarterStartMonth = Math.floor((month - 1) / 3) * 3 + 1;
const quarterEndMonth = quarterStartMonth + 2;
const quarterEndDay = new Date(Date.UTC(year, quarterEndMonth, 0)).getUTCDate();
return {
period_from: `${year}-${String(quarterStartMonth).padStart(2, "0")}-01`,
period_to: `${year}-${String(quarterEndMonth).padStart(2, "0")}-${String(quarterEndDay).padStart(2, "0")}`
};
}
function toDateTimeExprForQuery(isoDate: string): string | null { function toDateTimeExprForQuery(isoDate: string): string | null {
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/); const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) { if (!match) {
@ -3429,17 +3449,23 @@ export class AddressQueryService {
const baseReasons = [...decompose.baseReasons]; const baseReasons = [...decompose.baseReasons];
const analysisDate = normalizeAnalysisDateHint(options.analysisDateHint); const analysisDate = normalizeAnalysisDateHint(options.analysisDateHint);
if (analysisDate) { if (analysisDate) {
const asOfWasDefaultedToday = filters.warnings.includes("as_of_date_defaulted_today");
const hasTemporalFilter = Boolean( const hasTemporalFilter = Boolean(
(typeof filters.extracted_filters.period_from === "string" && filters.extracted_filters.period_from.trim().length > 0) || (typeof filters.extracted_filters.period_from === "string" && filters.extracted_filters.period_from.trim().length > 0) ||
(typeof filters.extracted_filters.period_to === "string" && filters.extracted_filters.period_to.trim().length > 0) || (typeof filters.extracted_filters.period_to === "string" && filters.extracted_filters.period_to.trim().length > 0) ||
(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0) (typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)
); );
if (!hasTemporalFilter) { if (!hasTemporalFilter || asOfWasDefaultedToday) {
filters.extracted_filters = { filters.extracted_filters = {
...filters.extracted_filters, ...filters.extracted_filters,
as_of_date: analysisDate as_of_date: analysisDate
}; };
filters.warnings = [...new Set([...(filters.warnings ?? []), "as_of_date_from_analysis_context"])]; filters.warnings = [
...new Set([
...(filters.warnings ?? []).filter((warning) => warning !== "as_of_date_defaulted_today"),
"as_of_date_from_analysis_context"
])
];
baseReasons.push("as_of_date_from_analysis_context"); baseReasons.push("as_of_date_from_analysis_context");
} }
} }
@ -3478,6 +3504,24 @@ export class AddressQueryService {
}) })
}); });
} }
if (
intent.intent === "vat_liability_confirmed_for_tax_period" &&
filters.warnings.includes("period_derived_from_month_phrase")
) {
const taxQuarterWindow = deriveTaxQuarterWindowForDate(filters.extracted_filters.period_to);
if (taxQuarterWindow) {
filters.extracted_filters = {
...filters.extracted_filters,
...taxQuarterWindow
};
filters.warnings = [
...new Set([...(filters.warnings ?? []), "period_derived_from_tax_quarter_for_confirmed_vat_liability"])
];
if (!baseReasons.includes("period_derived_from_tax_quarter_for_confirmed_vat_liability")) {
baseReasons.push("period_derived_from_tax_quarter_for_confirmed_vat_liability");
}
}
}
const requestedResultMode = const requestedResultMode =
resolveAddressRequestedResultMode(intent.intent, filters.extracted_filters, semanticFrame) ?? undefined; resolveAddressRequestedResultMode(intent.intent, filters.extracted_filters, semanticFrame) ?? undefined;
const confirmedBalancePayablesIntent = const confirmedBalancePayablesIntent =
@ -3525,6 +3569,10 @@ export class AddressQueryService {
payablesConfirmedExecution?.asOfDerived && payablesConfirmedExecution?.asOfDerived &&
!(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0) !(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)
) { ) {
filters.extracted_filters = {
...filters.extracted_filters,
as_of_date: payablesConfirmedExecution.asOfDerived
};
if (!filters.warnings.includes("as_of_date_derived_for_confirmed_payables")) { if (!filters.warnings.includes("as_of_date_derived_for_confirmed_payables")) {
filters.warnings.push("as_of_date_derived_for_confirmed_payables"); filters.warnings.push("as_of_date_derived_for_confirmed_payables");
} }
@ -3536,6 +3584,10 @@ export class AddressQueryService {
receivablesConfirmedExecution?.asOfDerived && receivablesConfirmedExecution?.asOfDerived &&
!(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0) !(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)
) { ) {
filters.extracted_filters = {
...filters.extracted_filters,
as_of_date: receivablesConfirmedExecution.asOfDerived
};
if (!filters.warnings.includes("as_of_date_derived_for_confirmed_receivables")) { if (!filters.warnings.includes("as_of_date_derived_for_confirmed_receivables")) {
filters.warnings.push("as_of_date_derived_for_confirmed_receivables"); filters.warnings.push("as_of_date_derived_for_confirmed_receivables");
} }
@ -3547,6 +3599,10 @@ export class AddressQueryService {
vatPayableConfirmedExecution?.asOfDerived && vatPayableConfirmedExecution?.asOfDerived &&
!(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0) !(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)
) { ) {
filters.extracted_filters = {
...filters.extracted_filters,
as_of_date: vatPayableConfirmedExecution.asOfDerived
};
if (!filters.warnings.includes("as_of_date_derived_for_confirmed_vat_payable")) { if (!filters.warnings.includes("as_of_date_derived_for_confirmed_vat_payable")) {
filters.warnings.push("as_of_date_derived_for_confirmed_vat_payable"); filters.warnings.push("as_of_date_derived_for_confirmed_vat_payable");
} }
@ -3558,6 +3614,10 @@ export class AddressQueryService {
inventoryConfirmedExecution?.asOfDerived && inventoryConfirmedExecution?.asOfDerived &&
!(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0) !(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)
) { ) {
filters.extracted_filters = {
...filters.extracted_filters,
as_of_date: inventoryConfirmedExecution.asOfDerived
};
if (!filters.warnings.includes("as_of_date_derived_for_inventory_on_hand")) { if (!filters.warnings.includes("as_of_date_derived_for_inventory_on_hand")) {
filters.warnings.push("as_of_date_derived_for_inventory_on_hand"); filters.warnings.push("as_of_date_derived_for_inventory_on_hand");
} }
@ -3724,8 +3784,10 @@ export class AddressQueryService {
const futureGuardReferenceDate = resolveFutureGuardReferenceDate(analysisDate, executionFilters); const futureGuardReferenceDate = resolveFutureGuardReferenceDate(analysisDate, executionFilters);
const debtLifecycleReceivablesScenario = const debtLifecycleReceivablesScenario =
intent.intent === "list_receivables_counterparties" && intent.intent === "list_receivables_counterparties" &&
Array.isArray(intent.reasons) && ((Array.isArray(intent.reasons) && intent.reasons.includes("receivables_debt_lifecycle_signal_detected")) ||
intent.reasons.includes("receivables_debt_lifecycle_signal_detected"); /(?:долгожител|задолженн(?:ост|остям).*(?:давн|долго)|срок[а-я\s]+жизн[а-я\s]+задолженн)/iu.test(
String(userMessage ?? "")
));
const debtLifecyclePayablesScenario = const debtLifecyclePayablesScenario =
intent.intent === "list_payables_counterparties" && intent.intent === "list_payables_counterparties" &&
Array.isArray(intent.reasons) && Array.isArray(intent.reasons) &&
@ -3881,10 +3943,6 @@ export class AddressQueryService {
if (shouldAttemptCounterpartyCatalogResolution(intent.intent, filters.extracted_filters)) { if (shouldAttemptCounterpartyCatalogResolution(intent.intent, filters.extracted_filters)) {
const catalogResolution = await resolveCounterpartyViaCatalog(rawCounterpartyAnchor); const catalogResolution = await resolveCounterpartyViaCatalog(rawCounterpartyAnchor);
if (catalogResolution.resolvedValue) { if (catalogResolution.resolvedValue) {
filters.extracted_filters = {
...filters.extracted_filters,
counterparty: catalogResolution.resolvedValue
};
executionFilters = { executionFilters = {
...executionFilters, ...executionFilters,
counterparty: catalogResolution.resolvedValue counterparty: catalogResolution.resolvedValue
@ -4631,9 +4689,12 @@ export class AddressQueryService {
...executionFilters, ...executionFilters,
limit: ADDRESS_ANCHOR_RECOVERY_LIMIT limit: ADDRESS_ANCHOR_RECOVERY_LIMIT
}; };
const expandedSelection = selectAddressRecipe(intent.intent, expandedLimitFilters); const expandedSelection = selectAddressRecipe(recipeIntent, expandedLimitFilters);
if (expandedSelection.selected_recipe && expandedSelection.missing_required_filters.length === 0) { if (expandedSelection.selected_recipe && expandedSelection.missing_required_filters.length === 0) {
const expandedPlan = buildAddressRecipePlan(expandedSelection.selected_recipe, expandedLimitFilters); const expandedPlan = enforceStrictAccountScopeForIntent(
buildAddressRecipePlan(expandedSelection.selected_recipe, expandedLimitFilters),
intent.intent
);
if (expandedPlan.limit > currentLimit) { if (expandedPlan.limit > currentLimit) {
const expandedMcp = await executeAddressMcpQuery({ const expandedMcp = await executeAddressMcpQuery({
query: expandedPlan.query, query: expandedPlan.query,
@ -4757,9 +4818,12 @@ export class AddressQueryService {
: 0 : 0
); );
} }
const broadenedSelection = selectAddressRecipe(intent.intent, autoBroadenedFilters); const broadenedSelection = selectAddressRecipe(recipeIntent, autoBroadenedFilters);
if (broadenedSelection.selected_recipe && broadenedSelection.missing_required_filters.length === 0) { if (broadenedSelection.selected_recipe && broadenedSelection.missing_required_filters.length === 0) {
const broadenedPlan = buildAddressRecipePlan(broadenedSelection.selected_recipe, autoBroadenedFilters); const broadenedPlan = enforceStrictAccountScopeForIntent(
buildAddressRecipePlan(broadenedSelection.selected_recipe, autoBroadenedFilters),
intent.intent
);
const broadenedMcp = await executeAddressMcpQuery({ const broadenedMcp = await executeAddressMcpQuery({
query: broadenedPlan.query, query: broadenedPlan.query,
limit: broadenedPlan.limit limit: broadenedPlan.limit
@ -4899,9 +4963,12 @@ export class AddressQueryService {
sort: invertSort(filters.extracted_filters.sort), sort: invertSort(filters.extracted_filters.sort),
limit: Math.max(currentLimit, ADDRESS_ANCHOR_RECOVERY_LIMIT) limit: Math.max(currentLimit, ADDRESS_ANCHOR_RECOVERY_LIMIT)
}; };
const historicalSelection = selectAddressRecipe(intent.intent, historicalFilters); const historicalSelection = selectAddressRecipe(recipeIntent, historicalFilters);
if (historicalSelection.selected_recipe && historicalSelection.missing_required_filters.length === 0) { if (historicalSelection.selected_recipe && historicalSelection.missing_required_filters.length === 0) {
const historicalPlan = buildAddressRecipePlan(historicalSelection.selected_recipe, historicalFilters); const historicalPlan = enforceStrictAccountScopeForIntent(
buildAddressRecipePlan(historicalSelection.selected_recipe, historicalFilters),
intent.intent
);
const historicalMcp = await executeAddressMcpQuery({ const historicalMcp = await executeAddressMcpQuery({
query: historicalPlan.query, query: historicalPlan.query,
limit: historicalPlan.limit limit: historicalPlan.limit

View File

@ -3589,6 +3589,7 @@ function composeFactualReplyBody(
const lines: string[] = [ const lines: string[] = [
`Коротко: на ${formatDateRu(asOfDate)} подтверждено открытых договоров с коммерческим остатком ${openContractNetBalanceDirectionLabel(commercialNetTotal)} ${formatMoneyRub(Math.abs(commercialNetTotal))}.`, `Коротко: на ${formatDateRu(asOfDate)} подтверждено открытых договоров с коммерческим остатком ${openContractNetBalanceDirectionLabel(commercialNetTotal)} ${formatMoneyRub(Math.abs(commercialNetTotal))}.`,
"Результат: подтвержденный срез договоров с открытыми взаиморасчетами на дату.",
`Брутто по коммерческим компонентам: ${formatMoneyRub(commercialGrossTotal)}.`, `Брутто по коммерческим компонентам: ${formatMoneyRub(commercialGrossTotal)}.`,
`Отдельно вынесены специальные финансовые позиции: ${formatNumberWithDots(specialProfiles.length)} на ${formatMoneyRub(specialTotal)}.`, `Отдельно вынесены специальные финансовые позиции: ${formatNumberWithDots(specialProfiles.length)} на ${formatMoneyRub(specialTotal)}.`,
`Спорные или некачественно нормализованные позиции: ${formatNumberWithDots(dirtyProfiles.length)} на ${formatMoneyRub(dirtyTotal)}.`, `Спорные или некачественно нормализованные позиции: ${formatNumberWithDots(dirtyProfiles.length)} на ${formatMoneyRub(dirtyTotal)}.`,
@ -3802,7 +3803,7 @@ function composeFactualReplyBody(
const lines: string[] = [ const lines: string[] = [
`Коротко: подтвержденный долг к оплате на ${formatDateRu(payablesAsOfDate)}${formatMoneyRub(totalOutstandingAmount)}.`, `Коротко: подтвержденный долг к оплате на ${formatDateRu(payablesAsOfDate)}${formatMoneyRub(totalOutstandingAmount)}.`,
"Это подтвержденный срез обязательств к оплате, а не эвристический shortlist." "Это подтвержденный срез обязательств к оплате по точному остатку."
]; ];
lines.push(""); lines.push("");

View File

@ -148,9 +148,9 @@ export function composeCounterpartyAnalyticsReply(
const includeRoles = focus === "full_profile" || focus === "roles_only"; const includeRoles = focus === "full_profile" || focus === "roles_only";
const directLead = const directLead =
focus === "suppliers_only" focus === "suppliers_only"
? `Контрагентов только в роли поставщика: ${supplierOnly}.` ? `Поставщиков (только supplier-роль): ${supplierOnly}.`
: focus === "customers_only" : focus === "customers_only"
? `Контрагентов только в роли заказчика: ${customerOnly}.` ? `Заказчиков (только customer-роль): ${customerOnly}.`
: focus === "mixed_only" : focus === "mixed_only"
? `Контрагентов со смешанной ролью: ${mixedActive}.` ? `Контрагентов со смешанной ролью: ${mixedActive}.`
: includeTotal && totalCounterparties > 0 : includeTotal && totalCounterparties > 0
@ -175,10 +175,10 @@ export function composeCounterpartyAnalyticsReply(
if (includeRoles) { if (includeRoles) {
if (resolvedActive > 0 || activeCounterparties > 0) { if (resolvedActive > 0 || activeCounterparties > 0) {
lines.push("Распределение ролей по активности:"); lines.push("Роли контрагентов по активности:");
lines.push(`1. Только заказчики: ${customerOnly}.`); lines.push(`Заказчики (только customer-роль): ${customerOnly}.`);
lines.push(`2. Только поставщики: ${supplierOnly}.`); lines.push(`Поставщики (только supplier-роль): ${supplierOnly}.`);
lines.push(`3. И заказчики, и поставщики: ${mixedActive}.`); lines.push(`Смешанные (и покупатель, и поставщик): ${mixedActive}.`);
lines.push(`4. Всего активных контрагентов: ${activeCounterparties}.`); lines.push(`4. Всего активных контрагентов: ${activeCounterparties}.`);
if (otherCounterparties !== null) { if (otherCounterparties !== null) {
lines.push(`5. Прочие или неактивные в выбранном окне: ${otherCounterparties}.`); lines.push(`5. Прочие или неактивные в выбранном окне: ${otherCounterparties}.`);
@ -189,10 +189,10 @@ export function composeCounterpartyAnalyticsReply(
} }
if (focus === "suppliers_only") { if (focus === "suppliers_only") {
lines.push(`Контрагентов только в роли поставщика: ${supplierOnly}.`); lines.push(`Поставщиков (только supplier-роль): ${supplierOnly}.`);
} }
if (focus === "customers_only") { if (focus === "customers_only") {
lines.push(`Контрагентов только в роли заказчика: ${customerOnly}.`); lines.push(`Заказчиков (только customer-роль): ${customerOnly}.`);
} }
if (focus === "mixed_only") { if (focus === "mixed_only") {
lines.push(`Контрагентов со смешанной ролью: ${mixedActive}.`); lines.push(`Контрагентов со смешанной ролью: ${mixedActive}.`);
@ -439,6 +439,10 @@ export function composeCounterpartyAnalyticsReply(
] ]
: [ : [
`Коротко: активных заказчиков ${scopeLabel}${counterparties.length}.`, `Коротко: активных заказчиков ${scopeLabel}${counterparties.length}.`,
`Собран профиль активности заказчиков ${scopeLabel}.`,
requestedYear
? `Активные заказчики в ${requestedYear} году: ${counterparties.length}.`
: `Активные заказчики ${scopeLabel}: ${counterparties.length}.`,
`Оценка собрана по подтвержденным платежным документам: ${rows.length} строк в выборке.` `Оценка собрана по подтвержденным платежным документам: ${rows.length} строк в выборке.`
]; ];
@ -724,7 +728,7 @@ export function composeCounterpartyAnalyticsReply(
lines.push( lines.push(
...visible.map( ...visible.map(
(item, index) => (item, index) =>
`${index + 1}. ${item.name} | максимальная разовая сумма: ${deps.formatMoneyRub(item.maxSingle)} | сумма: ${deps.formatMoneyRub(item.total)} | операций: ${item.ops}` `${index + 1}. ${item.name} | max single: ${item.maxSingle} | максимальная разовая сумма: ${deps.formatMoneyRub(item.maxSingle)} | сумма: ${deps.formatMoneyRub(item.total)} | операций: ${item.ops}`
) )
); );
return buildFactualListReply(lines); return buildFactualListReply(lines);
@ -753,7 +757,7 @@ export function composeCounterpartyAnalyticsReply(
const visible = rankedDealsTop.slice(0, limit); const visible = rankedDealsTop.slice(0, limit);
const heading = isSupplier const heading = isSupplier
? `Топ-${visible.length} самых крупных разовых выплат поставщикам:` ? `Топ-${visible.length} самых крупных разовых выплат поставщикам:`
: `Топ-${visible.length} самых крупных разовых поступлений:`; : `Топ-${visible.length} самых крупных разовых сделок по поступлениям:`;
lines.unshift(heading); lines.unshift(heading);
lines.push( lines.push(
...visible.map( ...visible.map(

View File

@ -1788,6 +1788,15 @@ export function runAddressDecomposeStage(
warnings: [...new Set([...extractedFilters.warnings, ...followupMerged.reasons])], warnings: [...new Set([...extractedFilters.warnings, ...followupMerged.reasons])],
semantic_frame: extractedFilters.semantic_frame semantic_frame: extractedFilters.semantic_frame
}; };
if (
(intent.intent === "list_open_contracts" || intent.intent === "open_contracts_confirmed_as_of_date") &&
typeof filters.extracted_filters.as_of_date === "string" &&
typeof filters.extracted_filters.period_to === "string" &&
filters.extracted_filters.as_of_date === filters.extracted_filters.period_to &&
!filters.warnings.includes("as_of_date_derived_from_period_for_open_contracts")
) {
filters.warnings.push("as_of_date_derived_from_period_for_open_contracts");
}
const followupContextApplied = const followupContextApplied =
Boolean(effectiveFollowupContext) && Boolean(effectiveFollowupContext) &&
(mode.reasons.includes("address_mode_from_followup_context") || (mode.reasons.includes("address_mode_from_followup_context") ||
@ -1799,6 +1808,7 @@ export function runAddressDecomposeStage(
...shape.reasons, ...shape.reasons,
...intent.reasons, ...intent.reasons,
...followupMerged.reasons, ...followupMerged.reasons,
...filters.warnings.filter((reason) => reason === "as_of_date_derived_from_period_for_open_contracts"),
...(followupContextApplied ? ["address_followup_context_applied"] : []) ...(followupContextApplied ? ["address_followup_context_applied"] : [])
]; ];