Post-F: добить semantic integrity M23 по VAT и scope recovery
This commit is contained in:
@@ -162,13 +162,13 @@ export function resolveAddressAsOfDateBasis(
|
||||
filters: AddressFilterSet,
|
||||
semanticFrame?: AddressSemanticFrame | null
|
||||
): AddressAsOfDateBasis | null {
|
||||
if (semanticFrame?.date_basis_hint) {
|
||||
return semanticFrame.date_basis_hint;
|
||||
}
|
||||
const asOfDate = normalizeIsoDateHint(filters.as_of_date);
|
||||
if (asOfDate) {
|
||||
return "explicit_as_of_date";
|
||||
}
|
||||
if (semanticFrame?.date_basis_hint) {
|
||||
return semanticFrame.date_basis_hint;
|
||||
}
|
||||
const periodFrom = normalizeIsoDateHint(filters.period_from);
|
||||
const periodTo = normalizeIsoDateHint(filters.period_to);
|
||||
if (periodFrom && periodTo) {
|
||||
|
||||
@@ -2377,6 +2377,17 @@ function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolutio
|
||||
|
||||
if (/(?:ндс|vat)/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) ||
|
||||
(!hasVatDebtCue && /(?:надо|нужно)\s+(?:заплат|оплат|уплат)/iu.test(normalized))
|
||||
@@ -2385,7 +2396,7 @@ function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolutio
|
||||
}
|
||||
if (/(?:долг|подтвержд|скольк|скока|надо|нужно|заплат|уплат|оплат)/iu.test(normalized)) {
|
||||
return unicodeBridgeResolution(
|
||||
/(?:налогов|бюджет|декларац|квартал|\b[1-4]\s*кв)/iu.test(normalized)
|
||||
hasTaxPeriodCue
|
||||
? "vat_liability_confirmed_for_tax_period"
|
||||
: "vat_payable_confirmed_as_of_date",
|
||||
"high",
|
||||
|
||||
@@ -329,6 +329,26 @@ function normalizeIsoDateForQuery(value: unknown): string | null {
|
||||
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 {
|
||||
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||
if (!match) {
|
||||
@@ -3429,17 +3449,23 @@ export class AddressQueryService {
|
||||
const baseReasons = [...decompose.baseReasons];
|
||||
const analysisDate = normalizeAnalysisDateHint(options.analysisDateHint);
|
||||
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) ||
|
||||
(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)
|
||||
);
|
||||
if (!hasTemporalFilter) {
|
||||
if (!hasTemporalFilter || asOfWasDefaultedToday) {
|
||||
filters.extracted_filters = {
|
||||
...filters.extracted_filters,
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -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 =
|
||||
resolveAddressRequestedResultMode(intent.intent, filters.extracted_filters, semanticFrame) ?? undefined;
|
||||
const confirmedBalancePayablesIntent =
|
||||
@@ -3525,6 +3569,10 @@ export class AddressQueryService {
|
||||
payablesConfirmedExecution?.asOfDerived &&
|
||||
!(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")) {
|
||||
filters.warnings.push("as_of_date_derived_for_confirmed_payables");
|
||||
}
|
||||
@@ -3536,6 +3584,10 @@ export class AddressQueryService {
|
||||
receivablesConfirmedExecution?.asOfDerived &&
|
||||
!(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")) {
|
||||
filters.warnings.push("as_of_date_derived_for_confirmed_receivables");
|
||||
}
|
||||
@@ -3547,6 +3599,10 @@ export class AddressQueryService {
|
||||
vatPayableConfirmedExecution?.asOfDerived &&
|
||||
!(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")) {
|
||||
filters.warnings.push("as_of_date_derived_for_confirmed_vat_payable");
|
||||
}
|
||||
@@ -3558,6 +3614,10 @@ export class AddressQueryService {
|
||||
inventoryConfirmedExecution?.asOfDerived &&
|
||||
!(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")) {
|
||||
filters.warnings.push("as_of_date_derived_for_inventory_on_hand");
|
||||
}
|
||||
@@ -3724,8 +3784,10 @@ export class AddressQueryService {
|
||||
const futureGuardReferenceDate = resolveFutureGuardReferenceDate(analysisDate, executionFilters);
|
||||
const debtLifecycleReceivablesScenario =
|
||||
intent.intent === "list_receivables_counterparties" &&
|
||||
Array.isArray(intent.reasons) &&
|
||||
intent.reasons.includes("receivables_debt_lifecycle_signal_detected");
|
||||
((Array.isArray(intent.reasons) && intent.reasons.includes("receivables_debt_lifecycle_signal_detected")) ||
|
||||
/(?:долгожител|задолженн(?:ост|остям).*(?:давн|долго)|срок[а-я\s]+жизн[а-я\s]+задолженн)/iu.test(
|
||||
String(userMessage ?? "")
|
||||
));
|
||||
const debtLifecyclePayablesScenario =
|
||||
intent.intent === "list_payables_counterparties" &&
|
||||
Array.isArray(intent.reasons) &&
|
||||
@@ -3881,10 +3943,6 @@ export class AddressQueryService {
|
||||
if (shouldAttemptCounterpartyCatalogResolution(intent.intent, filters.extracted_filters)) {
|
||||
const catalogResolution = await resolveCounterpartyViaCatalog(rawCounterpartyAnchor);
|
||||
if (catalogResolution.resolvedValue) {
|
||||
filters.extracted_filters = {
|
||||
...filters.extracted_filters,
|
||||
counterparty: catalogResolution.resolvedValue
|
||||
};
|
||||
executionFilters = {
|
||||
...executionFilters,
|
||||
counterparty: catalogResolution.resolvedValue
|
||||
@@ -4631,9 +4689,12 @@ export class AddressQueryService {
|
||||
...executionFilters,
|
||||
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) {
|
||||
const expandedPlan = buildAddressRecipePlan(expandedSelection.selected_recipe, expandedLimitFilters);
|
||||
const expandedPlan = enforceStrictAccountScopeForIntent(
|
||||
buildAddressRecipePlan(expandedSelection.selected_recipe, expandedLimitFilters),
|
||||
intent.intent
|
||||
);
|
||||
if (expandedPlan.limit > currentLimit) {
|
||||
const expandedMcp = await executeAddressMcpQuery({
|
||||
query: expandedPlan.query,
|
||||
@@ -4757,9 +4818,12 @@ export class AddressQueryService {
|
||||
: 0
|
||||
);
|
||||
}
|
||||
const broadenedSelection = selectAddressRecipe(intent.intent, autoBroadenedFilters);
|
||||
const broadenedSelection = selectAddressRecipe(recipeIntent, autoBroadenedFilters);
|
||||
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({
|
||||
query: broadenedPlan.query,
|
||||
limit: broadenedPlan.limit
|
||||
@@ -4899,9 +4963,12 @@ export class AddressQueryService {
|
||||
sort: invertSort(filters.extracted_filters.sort),
|
||||
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) {
|
||||
const historicalPlan = buildAddressRecipePlan(historicalSelection.selected_recipe, historicalFilters);
|
||||
const historicalPlan = enforceStrictAccountScopeForIntent(
|
||||
buildAddressRecipePlan(historicalSelection.selected_recipe, historicalFilters),
|
||||
intent.intent
|
||||
);
|
||||
const historicalMcp = await executeAddressMcpQuery({
|
||||
query: historicalPlan.query,
|
||||
limit: historicalPlan.limit
|
||||
|
||||
@@ -3589,6 +3589,7 @@ function composeFactualReplyBody(
|
||||
|
||||
const lines: string[] = [
|
||||
`Коротко: на ${formatDateRu(asOfDate)} подтверждено открытых договоров с коммерческим остатком ${openContractNetBalanceDirectionLabel(commercialNetTotal)} ${formatMoneyRub(Math.abs(commercialNetTotal))}.`,
|
||||
"Результат: подтвержденный срез договоров с открытыми взаиморасчетами на дату.",
|
||||
`Брутто по коммерческим компонентам: ${formatMoneyRub(commercialGrossTotal)}.`,
|
||||
`Отдельно вынесены специальные финансовые позиции: ${formatNumberWithDots(specialProfiles.length)} на ${formatMoneyRub(specialTotal)}.`,
|
||||
`Спорные или некачественно нормализованные позиции: ${formatNumberWithDots(dirtyProfiles.length)} на ${formatMoneyRub(dirtyTotal)}.`,
|
||||
@@ -3802,7 +3803,7 @@ function composeFactualReplyBody(
|
||||
|
||||
const lines: string[] = [
|
||||
`Коротко: подтвержденный долг к оплате на ${formatDateRu(payablesAsOfDate)} — ${formatMoneyRub(totalOutstandingAmount)}.`,
|
||||
"Это подтвержденный срез обязательств к оплате, а не эвристический shortlist."
|
||||
"Это подтвержденный срез обязательств к оплате по точному остатку."
|
||||
];
|
||||
|
||||
lines.push("");
|
||||
|
||||
+14
-10
@@ -148,9 +148,9 @@ export function composeCounterpartyAnalyticsReply(
|
||||
const includeRoles = focus === "full_profile" || focus === "roles_only";
|
||||
const directLead =
|
||||
focus === "suppliers_only"
|
||||
? `Контрагентов только в роли поставщика: ${supplierOnly}.`
|
||||
? `Поставщиков (только supplier-роль): ${supplierOnly}.`
|
||||
: focus === "customers_only"
|
||||
? `Контрагентов только в роли заказчика: ${customerOnly}.`
|
||||
? `Заказчиков (только customer-роль): ${customerOnly}.`
|
||||
: focus === "mixed_only"
|
||||
? `Контрагентов со смешанной ролью: ${mixedActive}.`
|
||||
: includeTotal && totalCounterparties > 0
|
||||
@@ -175,10 +175,10 @@ export function composeCounterpartyAnalyticsReply(
|
||||
|
||||
if (includeRoles) {
|
||||
if (resolvedActive > 0 || activeCounterparties > 0) {
|
||||
lines.push("Распределение ролей по активности:");
|
||||
lines.push(`1. Только заказчики: ${customerOnly}.`);
|
||||
lines.push(`2. Только поставщики: ${supplierOnly}.`);
|
||||
lines.push(`3. И заказчики, и поставщики: ${mixedActive}.`);
|
||||
lines.push("Роли контрагентов по активности:");
|
||||
lines.push(`Заказчики (только customer-роль): ${customerOnly}.`);
|
||||
lines.push(`Поставщики (только supplier-роль): ${supplierOnly}.`);
|
||||
lines.push(`Смешанные (и покупатель, и поставщик): ${mixedActive}.`);
|
||||
lines.push(`4. Всего активных контрагентов: ${activeCounterparties}.`);
|
||||
if (otherCounterparties !== null) {
|
||||
lines.push(`5. Прочие или неактивные в выбранном окне: ${otherCounterparties}.`);
|
||||
@@ -189,10 +189,10 @@ export function composeCounterpartyAnalyticsReply(
|
||||
}
|
||||
|
||||
if (focus === "suppliers_only") {
|
||||
lines.push(`Контрагентов только в роли поставщика: ${supplierOnly}.`);
|
||||
lines.push(`Поставщиков (только supplier-роль): ${supplierOnly}.`);
|
||||
}
|
||||
if (focus === "customers_only") {
|
||||
lines.push(`Контрагентов только в роли заказчика: ${customerOnly}.`);
|
||||
lines.push(`Заказчиков (только customer-роль): ${customerOnly}.`);
|
||||
}
|
||||
if (focus === "mixed_only") {
|
||||
lines.push(`Контрагентов со смешанной ролью: ${mixedActive}.`);
|
||||
@@ -439,6 +439,10 @@ export function composeCounterpartyAnalyticsReply(
|
||||
]
|
||||
: [
|
||||
`Коротко: активных заказчиков ${scopeLabel} — ${counterparties.length}.`,
|
||||
`Собран профиль активности заказчиков ${scopeLabel}.`,
|
||||
requestedYear
|
||||
? `Активные заказчики в ${requestedYear} году: ${counterparties.length}.`
|
||||
: `Активные заказчики ${scopeLabel}: ${counterparties.length}.`,
|
||||
`Оценка собрана по подтвержденным платежным документам: ${rows.length} строк в выборке.`
|
||||
];
|
||||
|
||||
@@ -724,7 +728,7 @@ export function composeCounterpartyAnalyticsReply(
|
||||
lines.push(
|
||||
...visible.map(
|
||||
(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);
|
||||
@@ -753,7 +757,7 @@ export function composeCounterpartyAnalyticsReply(
|
||||
const visible = rankedDealsTop.slice(0, limit);
|
||||
const heading = isSupplier
|
||||
? `Топ-${visible.length} самых крупных разовых выплат поставщикам:`
|
||||
: `Топ-${visible.length} самых крупных разовых поступлений:`;
|
||||
: `Топ-${visible.length} самых крупных разовых сделок по поступлениям:`;
|
||||
lines.unshift(heading);
|
||||
lines.push(
|
||||
...visible.map(
|
||||
|
||||
@@ -1788,6 +1788,15 @@ export function runAddressDecomposeStage(
|
||||
warnings: [...new Set([...extractedFilters.warnings, ...followupMerged.reasons])],
|
||||
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) &&
|
||||
(mode.reasons.includes("address_mode_from_followup_context") ||
|
||||
@@ -1799,6 +1808,7 @@ export function runAddressDecomposeStage(
|
||||
...shape.reasons,
|
||||
...intent.reasons,
|
||||
...followupMerged.reasons,
|
||||
...filters.warnings.filter((reason) => reason === "as_of_date_derived_from_period_for_open_contracts"),
|
||||
...(followupContextApplied ? ["address_followup_context_applied"] : [])
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user