Зафиксировать семантическую целостность VAT, debt mirror и trace-ответов
This commit is contained in:
parent
a5fa940953
commit
9c86407937
|
|
@ -692,6 +692,32 @@ function isBroadDebtPolarityQuestion(intent, text) {
|
||||||
}
|
}
|
||||||
return /(?:^|[\s,.;:!?()\-])(?:кто|кому|какие|какой|список|топ|все|всех|всего)(?=$|[\s,.;:!?()\-])/iu.test(normalized);
|
return /(?:^|[\s,.;:!?()\-])(?:кто|кому|какие|какой|список|топ|все|всех|всего)(?=$|[\s,.;:!?()\-])/iu.test(normalized);
|
||||||
}
|
}
|
||||||
|
function isShortDebtRoleMirrorFollowup(intent, text, followupContext) {
|
||||||
|
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, intent, userMessage, followupContext) {
|
function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
const merged = { ...current };
|
const merged = { ...current };
|
||||||
const reasons = [];
|
const reasons = [];
|
||||||
|
|
@ -862,7 +888,11 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
(followupContext.previous_anchor_type === "counterparty" ? previousAnchorValue : null);
|
(followupContext.previous_anchor_type === "counterparty" ? previousAnchorValue : null);
|
||||||
const currentCounterparty = toNonEmptyString(merged.counterparty);
|
const currentCounterparty = toNonEmptyString(merged.counterparty);
|
||||||
const suppressCounterpartyForBroadDebtQuestion = isBroadDebtPolarityQuestion(intent, userMessage) && !currentCounterparty;
|
const suppressCounterpartyForBroadDebtQuestion = isBroadDebtPolarityQuestion(intent, userMessage) && !currentCounterparty;
|
||||||
|
const suppressCounterpartyForShortDebtMirror = isShortDebtRoleMirrorFollowup(intent, userMessage, followupContext) &&
|
||||||
|
followupContext.previous_anchor_type !== "counterparty" &&
|
||||||
|
(!currentCounterparty || isLowQualityCounterpartyAnchor(currentCounterparty));
|
||||||
const shouldInheritCounterparty = !suppressCounterpartyForBroadDebtQuestion &&
|
const shouldInheritCounterparty = !suppressCounterpartyForBroadDebtQuestion &&
|
||||||
|
!suppressCounterpartyForShortDebtMirror &&
|
||||||
(!currentCounterparty ||
|
(!currentCounterparty ||
|
||||||
(Boolean(inheritedCounterparty) &&
|
(Boolean(inheritedCounterparty) &&
|
||||||
isLowQualityCounterpartyAnchor(currentCounterparty) &&
|
isLowQualityCounterpartyAnchor(currentCounterparty) &&
|
||||||
|
|
@ -870,6 +900,9 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
if (inheritedCounterparty && suppressCounterpartyForBroadDebtQuestion) {
|
if (inheritedCounterparty && suppressCounterpartyForBroadDebtQuestion) {
|
||||||
reasons.push("counterparty_carryover_suppressed_for_broad_debt_polarity_question");
|
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) {
|
if (inheritedCounterparty && shouldInheritCounterparty) {
|
||||||
merged.counterparty = inheritedCounterparty;
|
merged.counterparty = inheritedCounterparty;
|
||||||
reasons.push(currentCounterparty ? "counterparty_replaced_from_followup_context" : "counterparty_from_followup_context");
|
reasons.push(currentCounterparty ? "counterparty_replaced_from_followup_context" : "counterparty_from_followup_context");
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.composeInventoryReply = composeInventoryReply;
|
exports.composeInventoryReply = composeInventoryReply;
|
||||||
const replyContracts_1 = require("./replyContracts");
|
const replyContracts_1 = require("./replyContracts");
|
||||||
const inventoryReplyPresentation_1 = require("./inventoryReplyPresentation");
|
const inventoryReplyPresentation_1 = require("./inventoryReplyPresentation");
|
||||||
|
const INVENTORY_TRACE_EVIDENCE_ROW_LIMIT = 3;
|
||||||
function cleanupInventoryRequestedParty(value) {
|
function cleanupInventoryRequestedParty(value) {
|
||||||
const cleaned = String(value ?? "")
|
const cleaned = String(value ?? "")
|
||||||
.replace(/\s*(?:->|=>|→)\s*(?:товар|позици|номенклатур|покупател|buyer|customer|item|product|sku)[\s\S]*$/iu, "")
|
.replace(/\s*(?:->|=>|→)\s*(?:товар|позици|номенклатур|покупател|buyer|customer|item|product|sku)[\s\S]*$/iu, "")
|
||||||
|
|
@ -236,7 +237,10 @@ function composeInventoryReply(intent, rows, options, deps) {
|
||||||
lines.push(`- Для ответа проверены закупочные документы не позже ${boundedAsOfLabel}.`);
|
lines.push(`- Для ответа проверены закупочные документы не позже ${boundedAsOfLabel}.`);
|
||||||
}
|
}
|
||||||
if (summary.documents.length > 0) {
|
if (summary.documents.length > 0) {
|
||||||
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Опорные документы:", deps.formatInventoryTraceRows(purchaseRows, 8));
|
(0, inventoryReplyPresentation_1.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 (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(purchaseRows.length > 0 ? (summary.counterparties.length === 1 ? "strong" : "medium") : "medium", purchaseRows.length > 0));
|
return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(purchaseRows.length > 0 ? (summary.counterparties.length === 1 ? "strong" : "medium") : "medium", purchaseRows.length > 0));
|
||||||
}
|
}
|
||||||
|
|
@ -353,9 +357,9 @@ function composeInventoryReply(intent, rows, options, deps) {
|
||||||
const itemLabel = requestedItemHint || (summary.item ?? "товар не определен");
|
const itemLabel = requestedItemHint || (summary.item ?? "товар не определен");
|
||||||
const excludedCounterpartyTokens = [itemLabel];
|
const excludedCounterpartyTokens = [itemLabel];
|
||||||
const directAnswerLine = summary.counterparties.length === 1
|
const directAnswerLine = summary.counterparties.length === 1
|
||||||
? `По товару ${itemLabel} покупатель определен: ${summary.counterparties[0]}.`
|
? `По номенклатуре ${itemLabel} в документах выбытия покупатель: ${summary.counterparties[0]}. Это подтверждает продажный след по номенклатуре, но без партионного учета не доказывает, что продали именно выбранный остаток/лот.`
|
||||||
: summary.counterparties.length > 1
|
: summary.counterparties.length > 1
|
||||||
? `По товару ${itemLabel} найдено несколько покупателей: ${summary.counterparties.slice(0, 4).join("; ")}.`
|
? `По номенклатуре ${itemLabel} в документах выбытия найдено несколько покупателей: ${summary.counterparties.slice(0, 4).join("; ")}. Это подтверждает продажный след по номенклатуре, но без партионного учета не доказывает связь с конкретным остатком/лотом.`
|
||||||
: `По товару ${itemLabel} покупатель в доступных данных не выделен.`;
|
: `По товару ${itemLabel} покупатель в доступных данных не выделен.`;
|
||||||
const lines = [directAnswerLine, "", "Подтверждение:"];
|
const lines = [directAnswerLine, "", "Подтверждение:"];
|
||||||
lines.push(`- Первая найденная дата выбытия: ${deps.inventoryTraceDateLabel(summary.firstPeriod)}.`);
|
lines.push(`- Первая найденная дата выбытия: ${deps.inventoryTraceDateLabel(summary.firstPeriod)}.`);
|
||||||
|
|
@ -363,17 +367,23 @@ function composeInventoryReply(intent, rows, options, deps) {
|
||||||
lines.push(`- Документов выбытия: ${deps.formatNumberWithDots(summary.documents.length)}.`);
|
lines.push(`- Документов выбытия: ${deps.formatNumberWithDots(summary.documents.length)}.`);
|
||||||
lines.push(`- Операций выбытия: ${deps.formatNumberWithDots(saleRows.length)}.`);
|
lines.push(`- Операций выбытия: ${deps.formatNumberWithDots(saleRows.length)}.`);
|
||||||
if (summary.counterparties.length === 1) {
|
if (summary.counterparties.length === 1) {
|
||||||
lines.push(`- По доступным движениям товар отгружался покупателю: ${summary.counterparties[0]}.`);
|
lines.push(`- По доступным движениям номенклатура отгружалась покупателю: ${summary.counterparties[0]}.`);
|
||||||
}
|
}
|
||||||
else if (summary.counterparties.length > 1) {
|
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) {
|
else if (saleRows.length > 0) {
|
||||||
lines.push("- Документы выбытия найдены, но покупатель не выделен отдельным полем в доступных данных.");
|
lines.push("- Документы выбытия найдены, но покупатель не выделен отдельным полем в доступных данных.");
|
||||||
}
|
}
|
||||||
|
if (saleRows.length > 0) {
|
||||||
|
lines.push("- Без партионного учета этот ответ подтверждает продажи по номенклатуре, а не юридически точную продажу конкретного остатка или партии.");
|
||||||
|
}
|
||||||
lines.push("", "Документы выбытия:");
|
lines.push("", "Документы выбытия:");
|
||||||
if (saleRows.length > 0) {
|
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 {
|
else {
|
||||||
lines.push("- По выбранному товару не найдено проводок выбытия в доступных данных.");
|
lines.push("- По выбранному товару не найдено проводок выбытия в доступных данных.");
|
||||||
|
|
|
||||||
|
|
@ -415,6 +415,7 @@ function sameCounterpartyCandidate(left, right) {
|
||||||
function readGroundedDiscoveryCounterparty(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
function readGroundedDiscoveryCounterparty(debug, toNonEmptyString = fallbackToNonEmptyString) {
|
||||||
const discoveryPilotScope = readAssistantMcpDiscoveryPilotScope(debug, toNonEmptyString);
|
const discoveryPilotScope = readAssistantMcpDiscoveryPilotScope(debug, toNonEmptyString);
|
||||||
const suppressDiscoveryEntityCarryover = discoveryPilotScope === "metadata_inspection_v1" ||
|
const suppressDiscoveryEntityCarryover = discoveryPilotScope === "metadata_inspection_v1" ||
|
||||||
|
readAssistantMcpDiscoveryTurnMeaning(debug)?.stale_replay_forbidden === true ||
|
||||||
readAssistantMcpDiscoveryLoopSubjectResolutionOptional(debug);
|
readAssistantMcpDiscoveryLoopSubjectResolutionOptional(debug);
|
||||||
if (suppressDiscoveryEntityCarryover) {
|
if (suppressDiscoveryEntityCarryover) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -943,7 +943,8 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||||
}
|
}
|
||||||
if (rankingNeed) {
|
if (rankingNeed) {
|
||||||
const incomingLeader = strongestIncomingYear(overview);
|
const incomingLeader = strongestIncomingYear(overview);
|
||||||
const netLeader = strongestNetYear(overview);
|
const canRankYearlyNet = !limitLine;
|
||||||
|
const netLeader = canRankYearlyNet ? strongestNetYear(overview) : null;
|
||||||
const leaderYear = toNonEmptyString(incomingLeader?.year_bucket);
|
const leaderYear = toNonEmptyString(incomingLeader?.year_bucket);
|
||||||
const leaderAmount = moneyText(incomingLeader?.incoming_total_amount_human_ru);
|
const leaderAmount = moneyText(incomingLeader?.incoming_total_amount_human_ru);
|
||||||
const leaderRows = Number(incomingLeader?.incoming_rows_with_amount);
|
const leaderRows = Number(incomingLeader?.incoming_rows_with_amount);
|
||||||
|
|
@ -964,7 +965,10 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||||
if (requestedFinancialBoundaryLine) {
|
if (requestedFinancialBoundaryLine) {
|
||||||
lines.push(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) {
|
if (yearRows) {
|
||||||
lines.push(yearRows);
|
lines.push(yearRows);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,12 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
function normalizeFollowupText(value) {
|
function normalizeFollowupText(value) {
|
||||||
return deps.compactWhitespace(deps.repairAddressMojibake(String(value ?? "")).toLowerCase()).replace(/ё/g, "е");
|
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) {
|
function hasBankOperationsPivotCue(text) {
|
||||||
const normalized = normalizeFollowupText(text);
|
const normalized = normalizeFollowupText(text);
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
|
|
@ -955,7 +961,21 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
hasInventoryRootRestatementAlternate ||
|
hasInventoryRootRestatementAlternate ||
|
||||||
hasSelectedObjectInventorySignalPrimary ||
|
hasSelectedObjectInventorySignalPrimary ||
|
||||||
hasSelectedObjectInventorySignalAlternate));
|
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 = (0, assistantContinuityPolicy_1.resolveFollowupTargetIntent)(inventoryPurchaseDateVatBridge, selectedObjectRetargetIntent, explicitIntentForCarryover, sourceIntent, followupSelectionMode, deps.toNonEmptyString(inventoryRootFrame?.intent), displayedEntityTargetIntent, previousIntent, explicitInventorySameDatePivot);
|
const carryoverTargetIntent = (0, assistantContinuityPolicy_1.resolveFollowupTargetIntent)(inventoryPurchaseDateVatBridge, selectedObjectRetargetIntent, explicitIntentForCarryover, sourceIntent, followupSelectionMode, deps.toNonEmptyString(inventoryRootFrame?.intent), displayedEntityTargetIntent, previousIntent, explicitInventorySameDatePivot);
|
||||||
return {
|
return {
|
||||||
followupContext: {
|
followupContext: {
|
||||||
|
|
|
||||||
|
|
@ -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(
|
function mergeFollowupFilters(
|
||||||
current: AddressFilterSet,
|
current: AddressFilterSet,
|
||||||
intent: AddressIntent,
|
intent: AddressIntent,
|
||||||
|
|
@ -1104,8 +1139,13 @@ function mergeFollowupFilters(
|
||||||
(followupContext.previous_anchor_type === "counterparty" ? previousAnchorValue : null);
|
(followupContext.previous_anchor_type === "counterparty" ? previousAnchorValue : null);
|
||||||
const currentCounterparty = toNonEmptyString(merged.counterparty);
|
const currentCounterparty = toNonEmptyString(merged.counterparty);
|
||||||
const suppressCounterpartyForBroadDebtQuestion = isBroadDebtPolarityQuestion(intent, userMessage) && !currentCounterparty;
|
const suppressCounterpartyForBroadDebtQuestion = isBroadDebtPolarityQuestion(intent, userMessage) && !currentCounterparty;
|
||||||
|
const suppressCounterpartyForShortDebtMirror =
|
||||||
|
isShortDebtRoleMirrorFollowup(intent, userMessage, followupContext) &&
|
||||||
|
followupContext.previous_anchor_type !== "counterparty" &&
|
||||||
|
(!currentCounterparty || isLowQualityCounterpartyAnchor(currentCounterparty));
|
||||||
const shouldInheritCounterparty =
|
const shouldInheritCounterparty =
|
||||||
!suppressCounterpartyForBroadDebtQuestion &&
|
!suppressCounterpartyForBroadDebtQuestion &&
|
||||||
|
!suppressCounterpartyForShortDebtMirror &&
|
||||||
(!currentCounterparty ||
|
(!currentCounterparty ||
|
||||||
(Boolean(inheritedCounterparty) &&
|
(Boolean(inheritedCounterparty) &&
|
||||||
isLowQualityCounterpartyAnchor(currentCounterparty) &&
|
isLowQualityCounterpartyAnchor(currentCounterparty) &&
|
||||||
|
|
@ -1113,6 +1153,9 @@ function mergeFollowupFilters(
|
||||||
if (inheritedCounterparty && suppressCounterpartyForBroadDebtQuestion) {
|
if (inheritedCounterparty && suppressCounterpartyForBroadDebtQuestion) {
|
||||||
reasons.push("counterparty_carryover_suppressed_for_broad_debt_polarity_question");
|
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) {
|
if (inheritedCounterparty && shouldInheritCounterparty) {
|
||||||
merged.counterparty = inheritedCounterparty;
|
merged.counterparty = inheritedCounterparty;
|
||||||
reasons.push(currentCounterparty ? "counterparty_replaced_from_followup_context" : "counterparty_from_followup_context");
|
reasons.push(currentCounterparty ? "counterparty_replaced_from_followup_context" : "counterparty_from_followup_context");
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,8 @@ interface InventoryTraceSummary {
|
||||||
totalAmount: number;
|
totalAmount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const INVENTORY_TRACE_EVIDENCE_ROW_LIMIT = 3;
|
||||||
|
|
||||||
interface InventoryAgingByItemAggregate {
|
interface InventoryAgingByItemAggregate {
|
||||||
item: string;
|
item: string;
|
||||||
warehouse: string | null;
|
warehouse: string | null;
|
||||||
|
|
@ -341,7 +343,16 @@ export function composeInventoryReply(
|
||||||
lines.push(`- Для ответа проверены закупочные документы не позже ${boundedAsOfLabel}.`);
|
lines.push(`- Для ответа проверены закупочные документы не позже ${boundedAsOfLabel}.`);
|
||||||
}
|
}
|
||||||
if (summary.documents.length > 0) {
|
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(
|
return buildFactualSummaryReply(
|
||||||
lines,
|
lines,
|
||||||
|
|
@ -486,9 +497,9 @@ export function composeInventoryReply(
|
||||||
const excludedCounterpartyTokens = [itemLabel];
|
const excludedCounterpartyTokens = [itemLabel];
|
||||||
const directAnswerLine =
|
const directAnswerLine =
|
||||||
summary.counterparties.length === 1
|
summary.counterparties.length === 1
|
||||||
? `По товару ${itemLabel} покупатель определен: ${summary.counterparties[0]}.`
|
? `По номенклатуре ${itemLabel} в документах выбытия покупатель: ${summary.counterparties[0]}. Это подтверждает продажный след по номенклатуре, но без партионного учета не доказывает, что продали именно выбранный остаток/лот.`
|
||||||
: summary.counterparties.length > 1
|
: summary.counterparties.length > 1
|
||||||
? `По товару ${itemLabel} найдено несколько покупателей: ${summary.counterparties.slice(0, 4).join("; ")}.`
|
? `По номенклатуре ${itemLabel} в документах выбытия найдено несколько покупателей: ${summary.counterparties.slice(0, 4).join("; ")}. Это подтверждает продажный след по номенклатуре, но без партионного учета не доказывает связь с конкретным остатком/лотом.`
|
||||||
: `По товару ${itemLabel} покупатель в доступных данных не выделен.`;
|
: `По товару ${itemLabel} покупатель в доступных данных не выделен.`;
|
||||||
const lines: string[] = [directAnswerLine, "", "Подтверждение:"];
|
const lines: string[] = [directAnswerLine, "", "Подтверждение:"];
|
||||||
lines.push(`- Первая найденная дата выбытия: ${deps.inventoryTraceDateLabel(summary.firstPeriod)}.`);
|
lines.push(`- Первая найденная дата выбытия: ${deps.inventoryTraceDateLabel(summary.firstPeriod)}.`);
|
||||||
|
|
@ -496,15 +507,27 @@ export function composeInventoryReply(
|
||||||
lines.push(`- Документов выбытия: ${deps.formatNumberWithDots(summary.documents.length)}.`);
|
lines.push(`- Документов выбытия: ${deps.formatNumberWithDots(summary.documents.length)}.`);
|
||||||
lines.push(`- Операций выбытия: ${deps.formatNumberWithDots(saleRows.length)}.`);
|
lines.push(`- Операций выбытия: ${deps.formatNumberWithDots(saleRows.length)}.`);
|
||||||
if (summary.counterparties.length === 1) {
|
if (summary.counterparties.length === 1) {
|
||||||
lines.push(`- По доступным движениям товар отгружался покупателю: ${summary.counterparties[0]}.`);
|
lines.push(`- По доступным движениям номенклатура отгружалась покупателю: ${summary.counterparties[0]}.`);
|
||||||
} else if (summary.counterparties.length > 1) {
|
} 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) {
|
} else if (saleRows.length > 0) {
|
||||||
lines.push("- Документы выбытия найдены, но покупатель не выделен отдельным полем в доступных данных.");
|
lines.push("- Документы выбытия найдены, но покупатель не выделен отдельным полем в доступных данных.");
|
||||||
}
|
}
|
||||||
|
if (saleRows.length > 0) {
|
||||||
|
lines.push(
|
||||||
|
"- Без партионного учета этот ответ подтверждает продажи по номенклатуре, а не юридически точную продажу конкретного остатка или партии."
|
||||||
|
);
|
||||||
|
}
|
||||||
lines.push("", "Документы выбытия:");
|
lines.push("", "Документы выбытия:");
|
||||||
if (saleRows.length > 0) {
|
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 {
|
} else {
|
||||||
lines.push("- По выбранному товару не найдено проводок выбытия в доступных данных.");
|
lines.push("- По выбранному товару не найдено проводок выбытия в доступных данных.");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -634,6 +634,7 @@ function readGroundedDiscoveryCounterparty(
|
||||||
const discoveryPilotScope = readAssistantMcpDiscoveryPilotScope(debug, toNonEmptyString);
|
const discoveryPilotScope = readAssistantMcpDiscoveryPilotScope(debug, toNonEmptyString);
|
||||||
const suppressDiscoveryEntityCarryover =
|
const suppressDiscoveryEntityCarryover =
|
||||||
discoveryPilotScope === "metadata_inspection_v1" ||
|
discoveryPilotScope === "metadata_inspection_v1" ||
|
||||||
|
readAssistantMcpDiscoveryTurnMeaning(debug)?.stale_replay_forbidden === true ||
|
||||||
readAssistantMcpDiscoveryLoopSubjectResolutionOptional(debug);
|
readAssistantMcpDiscoveryLoopSubjectResolutionOptional(debug);
|
||||||
if (suppressDiscoveryEntityCarryover) {
|
if (suppressDiscoveryEntityCarryover) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -1129,7 +1129,8 @@ function buildCompactBusinessOverviewReply(
|
||||||
|
|
||||||
if (rankingNeed) {
|
if (rankingNeed) {
|
||||||
const incomingLeader = strongestIncomingYear(overview);
|
const incomingLeader = strongestIncomingYear(overview);
|
||||||
const netLeader = strongestNetYear(overview);
|
const canRankYearlyNet = !limitLine;
|
||||||
|
const netLeader = canRankYearlyNet ? strongestNetYear(overview) : null;
|
||||||
const leaderYear = toNonEmptyString(incomingLeader?.year_bucket);
|
const leaderYear = toNonEmptyString(incomingLeader?.year_bucket);
|
||||||
const leaderAmount = moneyText(incomingLeader?.incoming_total_amount_human_ru);
|
const leaderAmount = moneyText(incomingLeader?.incoming_total_amount_human_ru);
|
||||||
const leaderRows = Number(incomingLeader?.incoming_rows_with_amount);
|
const leaderRows = Number(incomingLeader?.incoming_rows_with_amount);
|
||||||
|
|
@ -1152,7 +1153,12 @@ function buildCompactBusinessOverviewReply(
|
||||||
if (requestedFinancialBoundaryLine) {
|
if (requestedFinancialBoundaryLine) {
|
||||||
lines.push(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) {
|
if (yearRows) {
|
||||||
lines.push(yearRows);
|
lines.push(yearRows);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,18 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
return deps.compactWhitespace(deps.repairAddressMojibake(String(value ?? "")).toLowerCase()).replace(/ё/g, "е");
|
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) {
|
function hasBankOperationsPivotCue(text) {
|
||||||
const normalized = normalizeFollowupText(text);
|
const normalized = normalizeFollowupText(text);
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
|
|
@ -1332,7 +1344,24 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
hasSelectedObjectInventorySignalPrimary ||
|
hasSelectedObjectInventorySignalPrimary ||
|
||||||
hasSelectedObjectInventorySignalAlternate)
|
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(
|
const carryoverTargetIntent = resolveFollowupTargetIntent(
|
||||||
inventoryPurchaseDateVatBridge,
|
inventoryPurchaseDateVatBridge,
|
||||||
selectedObjectRetargetIntent,
|
selectedObjectRetargetIntent,
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,51 @@ describe("address follow-up temporal regressions", () => {
|
||||||
expect(result?.filters.extracted_filters.counterparty).toBeUndefined();
|
expect(result?.filters.extracted_filters.counterparty).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("suppresses stale counterparty filters from organization-scoped short debt mirror follow-up", () => {
|
||||||
|
const result = runAddressDecomposeStage("а нам?", {
|
||||||
|
previous_intent: "payables_confirmed_as_of_date",
|
||||||
|
target_intent: "receivables_confirmed_as_of_date",
|
||||||
|
previous_filters: {
|
||||||
|
as_of_date: "2026-05-17",
|
||||||
|
organization: "ООО Альтернатива Плюс",
|
||||||
|
counterparty: "нас самый доходный клиент"
|
||||||
|
},
|
||||||
|
previous_anchor_type: "organization",
|
||||||
|
previous_anchor_value: "ООО Альтернатива Плюс"
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result?.intent.intent).toBe("receivables_confirmed_as_of_date");
|
||||||
|
expect(result?.filters.extracted_filters.as_of_date).toBe("2026-05-17");
|
||||||
|
expect(result?.filters.extracted_filters.organization).toBe("ООО Альтернатива Плюс");
|
||||||
|
expect(result?.filters.extracted_filters.counterparty).toBeUndefined();
|
||||||
|
expect(result?.baseReasons).toContain("counterparty_carryover_suppressed_for_short_debt_mirror");
|
||||||
|
expect(result?.baseReasons).not.toContain("counterparty_from_followup_context");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps resolved counterparty focus for short debt mirror follow-up", () => {
|
||||||
|
const result = runAddressDecomposeStage("а нам?", {
|
||||||
|
previous_intent: "payables_confirmed_as_of_date",
|
||||||
|
target_intent: "receivables_confirmed_as_of_date",
|
||||||
|
previous_filters: {
|
||||||
|
as_of_date: "2026-05-17",
|
||||||
|
organization: "ООО Альтернатива Плюс",
|
||||||
|
counterparty: "Группа СВК"
|
||||||
|
},
|
||||||
|
previous_anchor_type: "counterparty",
|
||||||
|
previous_anchor_value: "Группа СВК",
|
||||||
|
resolved_counterparty_from_display: true
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result?.intent.intent).toBe("receivables_confirmed_as_of_date");
|
||||||
|
expect(result?.filters.extracted_filters.as_of_date).toBe("2026-05-17");
|
||||||
|
expect(result?.filters.extracted_filters.organization).toBe("ООО Альтернатива Плюс");
|
||||||
|
expect(result?.filters.extracted_filters.counterparty).toBe("Группа СВК");
|
||||||
|
expect(result?.baseReasons).toContain("counterparty_from_followup_context");
|
||||||
|
expect(result?.baseReasons).not.toContain("counterparty_carryover_suppressed_for_short_debt_mirror");
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps same-date inventory pivot anchored to the previous VAT date", () => {
|
it("keeps same-date inventory pivot anchored to the previous VAT date", () => {
|
||||||
const result = runAddressDecomposeStage("какие остатки по складу на эту же дату", {
|
const result = runAddressDecomposeStage("какие остатки по складу на эту же дату", {
|
||||||
previous_intent: "vat_payable_confirmed_as_of_date",
|
previous_intent: "vat_payable_confirmed_as_of_date",
|
||||||
|
|
|
||||||
|
|
@ -703,6 +703,7 @@ describe("inventory selected-object follow-up", () => {
|
||||||
expect(result?.debug.extracted_filters?.as_of_date).toBe("2020-05-31");
|
expect(result?.debug.extracted_filters?.as_of_date).toBe("2020-05-31");
|
||||||
expect(result?.debug.reasons).toContain("inventory_selected_object_sale_trace_signal_detected");
|
expect(result?.debug.reasons).toContain("inventory_selected_object_sale_trace_signal_detected");
|
||||||
expect(String(result?.reply_text ?? "").split("\n")[0]).toContain("ООО \\Ромашка\\");
|
expect(String(result?.reply_text ?? "").split("\n")[0]).toContain("ООО \\Ромашка\\");
|
||||||
|
expect(String(result?.reply_text ?? "").split("\n")[0]).toContain("без партионного учета не доказывает");
|
||||||
expect(String(result?.reply_text ?? "")).toContain("Документы выбытия");
|
expect(String(result?.reply_text ?? "")).toContain("Документы выбытия");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -750,6 +751,8 @@ describe("inventory selected-object follow-up", () => {
|
||||||
expect(result?.debug.extracted_filters?.item).toBe("Кромка с клеем 33 альмандин 137 м");
|
expect(result?.debug.extracted_filters?.item).toBe("Кромка с клеем 33 альмандин 137 м");
|
||||||
expect(result?.debug.extracted_filters?.as_of_date).toBe("2020-03-31");
|
expect(result?.debug.extracted_filters?.as_of_date).toBe("2020-03-31");
|
||||||
expect(String(result?.reply_text ?? "")).toContain("ООО \\Покупатель\\");
|
expect(String(result?.reply_text ?? "")).toContain("ООО \\Покупатель\\");
|
||||||
|
expect(String(result?.reply_text ?? "")).not.toContain("покупатель определен");
|
||||||
|
expect(String(result?.reply_text ?? "")).toContain("продажный след по номенклатуре");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("detaches snapshot date from execution query during sale-trace history recovery", async () => {
|
it("detaches snapshot date from execution query during sale-trace history recovery", async () => {
|
||||||
|
|
|
||||||
|
|
@ -479,6 +479,42 @@ describe("assistantContinuityPolicy organization authority", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not carry MCP discovery counterparty from a stale replay-forbidden turn", () => {
|
||||||
|
const filters = resolveAddressDebugCarryoverFilters({
|
||||||
|
detected_intent: "payables_confirmed_as_of_date",
|
||||||
|
extracted_filters: {
|
||||||
|
organization: "Org Alt",
|
||||||
|
as_of_date: "2026-05-17"
|
||||||
|
},
|
||||||
|
mcp_discovery_response_applied: true,
|
||||||
|
assistant_mcp_discovery_entry_point_v1: {
|
||||||
|
entry_status: "bridge_executed",
|
||||||
|
turn_input: {
|
||||||
|
turn_meaning_ref: {
|
||||||
|
explicit_entity_candidates: ["old ranking counterparty"],
|
||||||
|
metadata_scope_hint: "old ranking counterparty",
|
||||||
|
stale_replay_forbidden: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bridge: {
|
||||||
|
bridge_status: "answer_draft_ready",
|
||||||
|
business_fact_answer_allowed: true,
|
||||||
|
answer_draft: {
|
||||||
|
answer_mode: "confirmed_with_bounded_inference"
|
||||||
|
},
|
||||||
|
pilot: {
|
||||||
|
pilot_scope: "counterparty_value_flow_query_movements_v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(filters).toEqual({
|
||||||
|
organization: "Org Alt",
|
||||||
|
as_of_date: "2026-05-17"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("hydrates inventory root-frame state from navigation scope and preserves derived current frame kind", () => {
|
it("hydrates inventory root-frame state from navigation scope and preserves derived current frame kind", () => {
|
||||||
const state = hydrateInventoryRootFrameState(
|
const state = hydrateInventoryRootFrameState(
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -445,9 +445,12 @@ describe("assistant MCP discovery response candidate", () => {
|
||||||
expect(candidate.reply_text).toContain("нельзя автоматически читать как обычного клиента или поставщика");
|
expect(candidate.reply_text).toContain("нельзя автоматически читать как обычного клиента или поставщика");
|
||||||
expect(candidate.reply_text).toContain("не полный бухгалтерский рейтинг доходности");
|
expect(candidate.reply_text).toContain("не полный бухгалтерский рейтинг доходности");
|
||||||
expect(candidate.reply_text).toContain("не как чистую бухгалтерскую прибыль");
|
expect(candidate.reply_text).toContain("не как чистую бухгалтерскую прибыль");
|
||||||
|
expect(candidate.reply_text).toContain("Годовое операционное нетто в широком срезе не ранжирую");
|
||||||
expect(candidate.reply_text).toContain("проверка достигла лимита строк");
|
expect(candidate.reply_text).toContain("проверка достигла лимита строк");
|
||||||
expect(candidate.reply_text).toContain("выбрать конкретный год или квартал для дозапроса");
|
expect(candidate.reply_text).toContain("выбрать конкретный год или квартал для дозапроса");
|
||||||
expect(candidate.reply_text).toContain("без выдачи непроверенного итога");
|
expect(candidate.reply_text).toContain("без выдачи непроверенного итога");
|
||||||
|
expect(candidate.reply_text).not.toContain("По расчетному операционному нетто лучший год");
|
||||||
|
expect(candidate.reply_text).not.toContain("По годам:");
|
||||||
expect(candidate.reply_text).not.toContain("лимит выборки MCP");
|
expect(candidate.reply_text).not.toContain("лимит выборки MCP");
|
||||||
expect(candidate.reply_text).not.toContain("MCP-срез");
|
expect(candidate.reply_text).not.toContain("MCP-срез");
|
||||||
expect(candidate.reply_text).not.toContain("Что подтверждено:");
|
expect(candidate.reply_text).not.toContain("Что подтверждено:");
|
||||||
|
|
|
||||||
|
|
@ -657,8 +657,8 @@ describe("assistant MCP discovery runtime bridge", () => {
|
||||||
expect(overview?.accounting_financial_result?.net_margin_to_revenue_pct).toBe(-59.41);
|
expect(overview?.accounting_financial_result?.net_margin_to_revenue_pct).toBe(-59.41);
|
||||||
expect(missingFamilies).not.toContain("accounting_profit_margin");
|
expect(missingFamilies).not.toContain("accounting_profit_margin");
|
||||||
expect(userFacing).toContain("90/91/99");
|
expect(userFacing).toContain("90/91/99");
|
||||||
expect(userFacing).toContain("90.01");
|
|
||||||
expect(userFacing).toContain("7 136 815,85");
|
expect(userFacing).toContain("7 136 815,85");
|
||||||
|
expect(userFacing).not.toContain("90.01");
|
||||||
expect(userFacing).not.toContain("operating-flow/trading-margin proxy");
|
expect(userFacing).not.toContain("operating-flow/trading-margin proxy");
|
||||||
expect(result.reason_codes).toContain("pilot_derived_business_overview_accounting_financial_result_from_confirmed_rows");
|
expect(result.reason_codes).toContain("pilot_derived_business_overview_accounting_financial_result_from_confirmed_rows");
|
||||||
expect(result.reason_codes).toContain("answer_contains_business_overview_accounting_financial_result");
|
expect(result.reason_codes).toContain("answer_contains_business_overview_accounting_financial_result");
|
||||||
|
|
@ -850,7 +850,7 @@ describe("assistant MCP discovery runtime bridge", () => {
|
||||||
evidence_status: "financial_institution_leads_outgoing_cash"
|
evidence_status: "financial_institution_leads_outgoing_cash"
|
||||||
});
|
});
|
||||||
expect(missingFamilies).not.toContain("vendor_risk_procurement_quality");
|
expect(missingFamilies).not.toContain("vendor_risk_procurement_quality");
|
||||||
expect(userFacing).toContain("procurement-concentration route");
|
expect(userFacing).not.toContain("procurement-concentration route");
|
||||||
expect(userFacing).toContain("банк/финансовая организация");
|
expect(userFacing).toContain("банк/финансовая организация");
|
||||||
expect(userFacing).toContain("Поставщик А");
|
expect(userFacing).toContain("Поставщик А");
|
||||||
expect(userFacing).toContain("Надежность поставщиков");
|
expect(userFacing).toContain("Надежность поставщиков");
|
||||||
|
|
|
||||||
|
|
@ -1861,6 +1861,62 @@ describe("assistantTransitionPolicy", () => {
|
||||||
period_to: "2017-05-31"
|
period_to: "2017-05-31"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("retargets same-period VAT payable follow-up away from current-date payable snapshots", () => {
|
||||||
|
const policy = buildPolicy({
|
||||||
|
findLastAddressAssistantItem: () => ({
|
||||||
|
text: "Receivables as of 2017-05-31 were collected for May.",
|
||||||
|
debug: {
|
||||||
|
detected_intent: "receivables_confirmed_as_of_date",
|
||||||
|
extracted_filters: {
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
as_of_date: "2017-05-31",
|
||||||
|
period_from: "2017-05-01",
|
||||||
|
period_to: "2017-05-31"
|
||||||
|
},
|
||||||
|
anchor_type: "organization",
|
||||||
|
anchor_value_resolved: 'ООО "Альтернатива Плюс"'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
hasAddressFollowupContextSignal: () => true,
|
||||||
|
hasReferentialPointer: () => true,
|
||||||
|
resolveAddressIntent: () => ({ intent: "unknown" }),
|
||||||
|
resolveAddressIntentFamily: (intent: unknown) => {
|
||||||
|
if (String(intent ?? "").startsWith("receivables_")) return "receivables";
|
||||||
|
if (String(intent ?? "").startsWith("vat_")) return "vat";
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
resolveAssistantTurnMeaning: () => ({
|
||||||
|
schema_version: "assistant_turn_meaning_v1",
|
||||||
|
asked_domain_family: "vat",
|
||||||
|
asked_action_family: "confirmed_snapshot",
|
||||||
|
explicit_intent_candidate: "vat_payable_confirmed_as_of_date",
|
||||||
|
explicit_entity_candidates: [],
|
||||||
|
intent_override_strength: "explicit_current_turn_intent",
|
||||||
|
stale_replay_forbidden: false
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||||||
|
"а какой ндс мы должны примерно заплатить за этот период?",
|
||||||
|
[],
|
||||||
|
"Какой НДС мы должны заплатить за текущий период?",
|
||||||
|
{
|
||||||
|
predecomposeContract: {
|
||||||
|
intent: "vat_payable_confirmed_as_of_date"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(carryover?.followupContext?.previous_intent).toBe("receivables_confirmed_as_of_date");
|
||||||
|
expect(carryover?.followupContext?.target_intent).toBe("vat_liability_confirmed_for_tax_period");
|
||||||
|
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
period_from: "2017-05-01",
|
||||||
|
period_to: "2017-05-31"
|
||||||
|
});
|
||||||
|
});
|
||||||
it("carries metadata-scoped subjectless loop state through follow-up context", () => {
|
it("carries metadata-scoped subjectless loop state through follow-up context", () => {
|
||||||
const policy = buildPolicy({
|
const policy = buildPolicy({
|
||||||
findLastAddressAssistantItem: () => ({
|
findLastAddressAssistantItem: () => ({
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue