Зафиксировать семантическую целостность VAT, debt mirror и trace-ответов
This commit is contained in:
@@ -894,6 +894,41 @@ function isBroadDebtPolarityQuestion(intent: AddressIntent, text: string): boole
|
||||
);
|
||||
}
|
||||
|
||||
function isShortDebtRoleMirrorFollowup(
|
||||
intent: AddressIntent,
|
||||
text: string,
|
||||
followupContext: AddressFollowupContext | null
|
||||
): boolean {
|
||||
if (intent !== "payables_confirmed_as_of_date" && intent !== "receivables_confirmed_as_of_date") {
|
||||
return false;
|
||||
}
|
||||
const previousIntent = followupContext?.previous_intent ?? null;
|
||||
const previousIsPayables =
|
||||
previousIntent === "payables_confirmed_as_of_date" || previousIntent === "list_payables_counterparties";
|
||||
const previousIsReceivables =
|
||||
previousIntent === "receivables_confirmed_as_of_date" || previousIntent === "list_receivables_counterparties";
|
||||
const normalized = textWithRepairedVariant(String(text ?? ""))
|
||||
.toLowerCase()
|
||||
.replace(/ё/g, "е")
|
||||
.replace(/[^\p{L}0-9]+/giu, " ")
|
||||
.trim();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const tokens = normalized.split(/\s+/u).filter(Boolean);
|
||||
if (tokens.length > 4) {
|
||||
return false;
|
||||
}
|
||||
const semanticTokens = /^(?:а|a|и|i)$/iu.test(tokens[0] ?? "") ? tokens.slice(1) : tokens;
|
||||
const phrase = semanticTokens.join(" ");
|
||||
const asksReceivables = phrase === "нам" || phrase === "нам кто" || phrase === "кто нам";
|
||||
const asksPayables = phrase === "кому" || phrase === "мы кому" || phrase === "кому мы";
|
||||
return (
|
||||
(intent === "receivables_confirmed_as_of_date" && previousIsPayables && asksReceivables) ||
|
||||
(intent === "payables_confirmed_as_of_date" && previousIsReceivables && asksPayables)
|
||||
);
|
||||
}
|
||||
|
||||
function mergeFollowupFilters(
|
||||
current: AddressFilterSet,
|
||||
intent: AddressIntent,
|
||||
@@ -1104,8 +1139,13 @@ function mergeFollowupFilters(
|
||||
(followupContext.previous_anchor_type === "counterparty" ? previousAnchorValue : null);
|
||||
const currentCounterparty = toNonEmptyString(merged.counterparty);
|
||||
const suppressCounterpartyForBroadDebtQuestion = isBroadDebtPolarityQuestion(intent, userMessage) && !currentCounterparty;
|
||||
const suppressCounterpartyForShortDebtMirror =
|
||||
isShortDebtRoleMirrorFollowup(intent, userMessage, followupContext) &&
|
||||
followupContext.previous_anchor_type !== "counterparty" &&
|
||||
(!currentCounterparty || isLowQualityCounterpartyAnchor(currentCounterparty));
|
||||
const shouldInheritCounterparty =
|
||||
!suppressCounterpartyForBroadDebtQuestion &&
|
||||
!suppressCounterpartyForShortDebtMirror &&
|
||||
(!currentCounterparty ||
|
||||
(Boolean(inheritedCounterparty) &&
|
||||
isLowQualityCounterpartyAnchor(currentCounterparty) &&
|
||||
@@ -1113,6 +1153,9 @@ function mergeFollowupFilters(
|
||||
if (inheritedCounterparty && suppressCounterpartyForBroadDebtQuestion) {
|
||||
reasons.push("counterparty_carryover_suppressed_for_broad_debt_polarity_question");
|
||||
}
|
||||
if (inheritedCounterparty && suppressCounterpartyForShortDebtMirror) {
|
||||
reasons.push("counterparty_carryover_suppressed_for_short_debt_mirror");
|
||||
}
|
||||
if (inheritedCounterparty && shouldInheritCounterparty) {
|
||||
merged.counterparty = inheritedCounterparty;
|
||||
reasons.push(currentCounterparty ? "counterparty_replaced_from_followup_context" : "counterparty_from_followup_context");
|
||||
|
||||
@@ -43,6 +43,8 @@ interface InventoryTraceSummary {
|
||||
totalAmount: number;
|
||||
}
|
||||
|
||||
const INVENTORY_TRACE_EVIDENCE_ROW_LIMIT = 3;
|
||||
|
||||
interface InventoryAgingByItemAggregate {
|
||||
item: string;
|
||||
warehouse: string | null;
|
||||
@@ -341,7 +343,16 @@ export function composeInventoryReply(
|
||||
lines.push(`- Для ответа проверены закупочные документы не позже ${boundedAsOfLabel}.`);
|
||||
}
|
||||
if (summary.documents.length > 0) {
|
||||
appendInventorySection(lines, "Опорные документы:", deps.formatInventoryTraceRows(purchaseRows, 8));
|
||||
appendInventorySection(
|
||||
lines,
|
||||
"Опорные документы:",
|
||||
deps.formatInventoryTraceRows(purchaseRows, INVENTORY_TRACE_EVIDENCE_ROW_LIMIT)
|
||||
);
|
||||
if (purchaseRows.length > INVENTORY_TRACE_EVIDENCE_ROW_LIMIT) {
|
||||
lines.push(
|
||||
`- Показаны первые ${INVENTORY_TRACE_EVIDENCE_ROW_LIMIT} из ${deps.formatNumberWithDots(purchaseRows.length)} найденных строк; полный след остается в подтвержденном срезе.`
|
||||
);
|
||||
}
|
||||
}
|
||||
return buildFactualSummaryReply(
|
||||
lines,
|
||||
@@ -486,9 +497,9 @@ export function composeInventoryReply(
|
||||
const excludedCounterpartyTokens = [itemLabel];
|
||||
const directAnswerLine =
|
||||
summary.counterparties.length === 1
|
||||
? `По товару ${itemLabel} покупатель определен: ${summary.counterparties[0]}.`
|
||||
? `По номенклатуре ${itemLabel} в документах выбытия покупатель: ${summary.counterparties[0]}. Это подтверждает продажный след по номенклатуре, но без партионного учета не доказывает, что продали именно выбранный остаток/лот.`
|
||||
: summary.counterparties.length > 1
|
||||
? `По товару ${itemLabel} найдено несколько покупателей: ${summary.counterparties.slice(0, 4).join("; ")}.`
|
||||
? `По номенклатуре ${itemLabel} в документах выбытия найдено несколько покупателей: ${summary.counterparties.slice(0, 4).join("; ")}. Это подтверждает продажный след по номенклатуре, но без партионного учета не доказывает связь с конкретным остатком/лотом.`
|
||||
: `По товару ${itemLabel} покупатель в доступных данных не выделен.`;
|
||||
const lines: string[] = [directAnswerLine, "", "Подтверждение:"];
|
||||
lines.push(`- Первая найденная дата выбытия: ${deps.inventoryTraceDateLabel(summary.firstPeriod)}.`);
|
||||
@@ -496,15 +507,27 @@ export function composeInventoryReply(
|
||||
lines.push(`- Документов выбытия: ${deps.formatNumberWithDots(summary.documents.length)}.`);
|
||||
lines.push(`- Операций выбытия: ${deps.formatNumberWithDots(saleRows.length)}.`);
|
||||
if (summary.counterparties.length === 1) {
|
||||
lines.push(`- По доступным движениям товар отгружался покупателю: ${summary.counterparties[0]}.`);
|
||||
lines.push(`- По доступным движениям номенклатура отгружалась покупателю: ${summary.counterparties[0]}.`);
|
||||
} else if (summary.counterparties.length > 1) {
|
||||
lines.push(`- По доступным движениям найдено несколько покупателей: ${summary.counterparties.slice(0, 4).join("; ")}.`);
|
||||
lines.push(`- По доступным движениям найдено несколько покупателей номенклатуры: ${summary.counterparties.slice(0, 4).join("; ")}.`);
|
||||
} else if (saleRows.length > 0) {
|
||||
lines.push("- Документы выбытия найдены, но покупатель не выделен отдельным полем в доступных данных.");
|
||||
}
|
||||
if (saleRows.length > 0) {
|
||||
lines.push(
|
||||
"- Без партионного учета этот ответ подтверждает продажи по номенклатуре, а не юридически точную продажу конкретного остатка или партии."
|
||||
);
|
||||
}
|
||||
lines.push("", "Документы выбытия:");
|
||||
if (saleRows.length > 0) {
|
||||
lines.push(...deps.formatInventoryTraceRows(saleRows, 12, excludedCounterpartyTokens));
|
||||
lines.push(
|
||||
...deps.formatInventoryTraceRows(saleRows, INVENTORY_TRACE_EVIDENCE_ROW_LIMIT, excludedCounterpartyTokens)
|
||||
);
|
||||
if (saleRows.length > INVENTORY_TRACE_EVIDENCE_ROW_LIMIT) {
|
||||
lines.push(
|
||||
`- Показаны первые ${INVENTORY_TRACE_EVIDENCE_ROW_LIMIT} из ${deps.formatNumberWithDots(saleRows.length)} найденных строк; полный след остается в подтвержденном срезе.`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
lines.push("- По выбранному товару не найдено проводок выбытия в доступных данных.");
|
||||
}
|
||||
|
||||
@@ -634,6 +634,7 @@ function readGroundedDiscoveryCounterparty(
|
||||
const discoveryPilotScope = readAssistantMcpDiscoveryPilotScope(debug, toNonEmptyString);
|
||||
const suppressDiscoveryEntityCarryover =
|
||||
discoveryPilotScope === "metadata_inspection_v1" ||
|
||||
readAssistantMcpDiscoveryTurnMeaning(debug)?.stale_replay_forbidden === true ||
|
||||
readAssistantMcpDiscoveryLoopSubjectResolutionOptional(debug);
|
||||
if (suppressDiscoveryEntityCarryover) {
|
||||
return null;
|
||||
|
||||
@@ -1129,7 +1129,8 @@ function buildCompactBusinessOverviewReply(
|
||||
|
||||
if (rankingNeed) {
|
||||
const incomingLeader = strongestIncomingYear(overview);
|
||||
const netLeader = strongestNetYear(overview);
|
||||
const canRankYearlyNet = !limitLine;
|
||||
const netLeader = canRankYearlyNet ? strongestNetYear(overview) : null;
|
||||
const leaderYear = toNonEmptyString(incomingLeader?.year_bucket);
|
||||
const leaderAmount = moneyText(incomingLeader?.incoming_total_amount_human_ru);
|
||||
const leaderRows = Number(incomingLeader?.incoming_rows_with_amount);
|
||||
@@ -1152,7 +1153,12 @@ function buildCompactBusinessOverviewReply(
|
||||
if (requestedFinancialBoundaryLine) {
|
||||
lines.push(requestedFinancialBoundaryLine);
|
||||
}
|
||||
const yearRows = businessOverviewYearRowsLine(overview);
|
||||
if (!canRankYearlyNet && Array.isArray(overview.yearly_breakdown) && overview.yearly_breakdown.length > 0) {
|
||||
lines.push(
|
||||
"Годовое операционное нетто в широком срезе не ранжирую: по одному из направлений достигнут лимит строк, поэтому безопаснее дозапросить конкретный год или квартал."
|
||||
);
|
||||
}
|
||||
const yearRows = canRankYearlyNet ? businessOverviewYearRowsLine(overview) : null;
|
||||
if (yearRows) {
|
||||
lines.push(yearRows);
|
||||
}
|
||||
|
||||
@@ -47,6 +47,18 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
return deps.compactWhitespace(deps.repairAddressMojibake(String(value ?? "")).toLowerCase()).replace(/ё/g, "е");
|
||||
}
|
||||
|
||||
function hasSamePeriodReferenceCue(...values) {
|
||||
return values
|
||||
.map((value) => normalizeFollowupText(value))
|
||||
.some(
|
||||
(normalized) =>
|
||||
normalized &&
|
||||
/(?:\b(?:same|this|that)\s+period\b|(?:за|на)\s+(?:этот|тот|такой\s+же|тот\s+же)\s+период|(?:Р·Р°|РЅР°)\s+(?:этот|тот|такой\s+Р¶Рµ|тот\s+Р¶Рµ)\s+период)/iu.test(
|
||||
normalized
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function hasBankOperationsPivotCue(text) {
|
||||
const normalized = normalizeFollowupText(text);
|
||||
if (!normalized) {
|
||||
@@ -1332,7 +1344,24 @@ export function createAssistantTransitionPolicy(deps) {
|
||||
hasSelectedObjectInventorySignalPrimary ||
|
||||
hasSelectedObjectInventorySignalAlternate)
|
||||
);
|
||||
const explicitIntentForCarryover = debtRoleSwapIntent ? debtRoleSwapIntent : explicitIntent;
|
||||
const previousHasPeriodWindow = Boolean(
|
||||
deps.toNonEmptyString(previousFilters.period_from) || deps.toNonEmptyString(previousFilters.period_to)
|
||||
);
|
||||
const predecomposeIntent = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
|
||||
const shouldRetargetVatSamePeriod =
|
||||
previousHasPeriodWindow &&
|
||||
hasSamePeriodReferenceCue(userMessage, alternateMessage) &&
|
||||
(explicitIntent === "vat_payable_confirmed_as_of_date" ||
|
||||
explicitIntent === "vat_payable_forecast" ||
|
||||
explicitIntent === "vat_liability_confirmed_for_tax_period" ||
|
||||
predecomposeIntent === "vat_payable_confirmed_as_of_date" ||
|
||||
predecomposeIntent === "vat_liability_confirmed_for_tax_period" ||
|
||||
deps.resolveAddressIntentFamily(explicitIntent) === "vat");
|
||||
const explicitIntentForCarryover = shouldRetargetVatSamePeriod
|
||||
? "vat_liability_confirmed_for_tax_period"
|
||||
: debtRoleSwapIntent
|
||||
? debtRoleSwapIntent
|
||||
: explicitIntent;
|
||||
const carryoverTargetIntent = resolveFollowupTargetIntent(
|
||||
inventoryPurchaseDateVatBridge,
|
||||
selectedObjectRetargetIntent,
|
||||
|
||||
Reference in New Issue
Block a user