Закрепить складской provenance и supplier-overlap ассистента

This commit is contained in:
2026-06-03 07:38:35 +03:00
parent 4d32b7fad5
commit 2cec57888a
28 changed files with 2274 additions and 162 deletions
@@ -726,17 +726,21 @@ function extractKnownFinancialCounterpartyAnchor(text: string): string | undefin
return undefined;
}
function isLowQualityCounterpartyAnchorValue(rawValue: string): boolean {
export function isLowQualityCounterpartyAnchorValue(rawValue: string): boolean {
const value = String(rawValue ?? "")
.trim()
.toLowerCase()
.replace(/ё/g, "е");
.replace(/ё/g, "е")
.replace(/^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu, "");
if (!value) {
return true;
}
if (/(?:за\s+вс[её]\s+время|за\s+всю\s+истори(?:ю|и)|all\s+time|entire\s+period|full\s+history)/iu.test(value)) {
return true;
}
if (/^(?:товар(?:ы|а|ов|у|ом|ами|ах)?|номенклатур(?:а|ы|у|ой|ами)?|позици(?:я|и|ю|ей)|остат(?:ок|ки|ка|ков|кам|ками)?|склад|складе|складу)$/iu.test(value)) {
return true;
}
if (
/^(?:или|это|там|может|можно|обычн\p{L}*|клиентск\p{L}*|банковск\p{L}*(?:\/|\s+и\s+)?финансов\p{L}*)\b/iu.test(
value
@@ -2465,6 +2465,16 @@ function asksForUnresolvedInventorySupplierLink(userMessage: string | null | und
);
}
function messagePairAsksForUnresolvedInventorySupplierLink(
userMessage: string | null | undefined,
rawUserMessage: string | null | undefined
): boolean {
return (
asksForUnresolvedInventorySupplierLink(userMessage) ||
asksForUnresolvedInventorySupplierLink(rawUserMessage)
);
}
function canAutoBroadenPeriodWindow(intent: AddressIntent, filters: AddressFilterSet): boolean {
if (Array.isArray((filters as { warnings?: unknown }).warnings) && (filters as { warnings?: string[] }).warnings?.includes("exact_historical_period_window_requested")) {
return false;
@@ -2530,7 +2540,12 @@ function shouldDetachLifecycleExecutionFromSnapshotContext(
return false;
}
const historyIntent =
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_profitability_for_item" ||
intent === "inventory_purchase_to_sale_chain";
return (
(historyIntent && reasons.includes("as_of_date_from_analysis_context")) ||
reasons.includes("period_window_semantic_from_inventory_snapshot_context") ||
reasons.includes("period_window_semantic_from_inventory_as_of_month") ||
reasons.includes("as_of_date_from_followup_context") ||
@@ -2629,6 +2644,90 @@ function injectNoticeAfterLeadLine(text: string, notice: string): string {
return [lines[0], normalizedNotice, ...lines.slice(1)].join("\n");
}
function injectNoticeBeforeLeadLine(text: string, notice: string): string {
const normalizedText = typeof text === "string" ? text : "";
const normalizedNotice = typeof notice === "string" ? notice.trim() : "";
if (!normalizedText.trim()) {
return normalizedNotice;
}
if (!normalizedNotice) {
return normalizedText;
}
return [normalizedNotice, normalizedText].join("\n");
}
function injectAutoBroadenedPeriodNotice(intent: string, text: string, notice: string): string {
if (intent === "inventory_supplier_stock_overlap_as_of_date") {
return injectNoticeBeforeLeadLine(text, notice);
}
return injectNoticeAfterLeadLine(text, notice);
}
function extractSupplierOverlapReplyCounterparties(text: string): string[] {
const counterparties: string[] = [];
const pattern = /\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442:\s*([^|\n]+)/giu;
for (const match of String(text ?? "").matchAll(pattern)) {
const value = String(match[1] ?? "").replace(/\s+/gu, " ").trim();
if (!value || /^(?:\u043d\u0435\s+\u0432\u044b\u0434\u0435\u043b\u0435\u043d|\u043d\/\u0434|unknown)$/iu.test(value)) {
continue;
}
counterparties.push(value);
}
return uniqueStrings(counterparties);
}
function formatSupplierOverlapCounterpartyLead(counterparties: string[], limit = 6): string {
const visible = counterparties.slice(0, limit);
const remaining = counterparties.length - visible.length;
const suffix = remaining > 0 ? `; и еще ${remaining}` : "";
return `${visible.join("; ")}${suffix}`;
}
function hasInventorySupplierOverlapDefensiveLead(firstLine: string): boolean {
const normalized = String(firstLine ?? "").toLocaleLowerCase("ru-RU").replace(/\u0451/gu, "\u0435");
const startsWithStockScope = /^\u043f\u043e\s+(?:\u0441\u043a\u043b\u0430\u0434\u0441\u043a\u043e\u043c\u0443\s+\u043e\u0441\u0442\u0430\u0442\u043a\u0443|\u043e\u043a\u043d\u0443)/u.test(
normalized
);
const hasDefensiveAttribution = /\u043e\u0434\u043d\u043e\u0437\u043d\u0430\u0447\u043d(?:\u044b\u0439\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0430\u044f\s+\u0430\u0442\u0440\u0438\u0431\u0443\u0446\u0438\u044f).*?\u043d\u0435\s+\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d/u.test(
normalized
);
return startsWithStockScope && hasDefensiveAttribution;
}
export function ensureInventorySupplierOverlapLeadLine(intent: string, text: string): string {
if (intent !== "inventory_supplier_stock_overlap_as_of_date") {
return text;
}
const normalizedText = typeof text === "string" ? text : "";
const lines = normalizedText.split(/\r?\n/u);
const firstLine = lines[0]?.trim() ?? "";
const displayedCounterparties = extractSupplierOverlapReplyCounterparties(normalizedText);
if (
displayedCounterparties.length > 0 &&
hasInventorySupplierOverlapDefensiveLead(firstLine)
) {
const repairedLead =
displayedCounterparties.length === 1
? `По складскому остатку найден поставщик закупочного следа: ${displayedCounterparties[0]}.`
: `По складскому остатку найдено несколько поставщиков закупочного следа: ${formatSupplierOverlapCounterpartyLead(displayedCounterparties)}.`;
return [repairedLead, firstLine, ...lines.slice(1)].join("\n");
}
if (
!normalizedText.trim() ||
/^По\s+(?:складскому\s+остатку|окну)\b/iu.test(firstLine) ||
/^В\s+текущем\s+складском\s+срезе\b/iu.test(firstLine)
) {
return normalizedText;
}
if (!/^(?:Что\s+проверили:|Часть\s+закупочных\s+операций|Опорные\s+документы:)/iu.test(firstLine)) {
return normalizedText;
}
return [
"По складскому остатку однозначный поставщик текущего остатка не подтвержден; ниже показан проверенный закупочный след и ограничения.",
normalizedText
].join("\n");
}
function runtimeReadinessForLimitedCategory(category: AddressLimitedReasonCategory): AddressRuntimeReadiness {
if (category === "empty_match" || category === "missing_anchor") {
return "LIVE_QUERYABLE_WITH_LIMITS";
@@ -3684,7 +3783,7 @@ export class AddressQueryService {
intent.intent === "inventory_supplier_stock_overlap_as_of_date" &&
!toNonEmptyFilterValue(filters.extracted_filters.period_from) &&
!toNonEmptyFilterValue(filters.extracted_filters.period_to) &&
asksForUnresolvedInventorySupplierLink(userMessage) &&
messagePairAsksForUnresolvedInventorySupplierLink(userMessage, options.rawUserMessage) &&
!/(?:за\s+вс[её]\s+время|за\s+любой\s+период|all[\s-]?time|all\s+periods?)/iu.test(userMessage)
) {
const monthWindow = deriveMonthWindowForDate(filters.extracted_filters.as_of_date);
@@ -3808,6 +3907,21 @@ export class AddressQueryService {
receivablesConfirmedExecution?.executionFilters ??
vatPayableConfirmedExecution?.executionFilters ??
filters.extracted_filters;
if (
intent.intent === "inventory_aging_by_purchase_date" &&
toNonEmptyFilterValue(executionFilters.as_of_date) &&
(toNonEmptyFilterValue(executionFilters.period_from) || toNonEmptyFilterValue(executionFilters.period_to))
) {
executionFilters = { ...executionFilters };
delete executionFilters.period_from;
delete executionFilters.period_to;
if (!filters.warnings.includes("period_window_detached_for_inventory_aging_execution")) {
filters.warnings.push("period_window_detached_for_inventory_aging_execution");
}
if (!baseReasons.includes("period_window_detached_for_inventory_aging_execution")) {
baseReasons.push("period_window_detached_for_inventory_aging_execution");
}
}
if (
intent.intent === "counterparty_activity_lifecycle" &&
typeof executionFilters.counterparty === "string" &&
@@ -3992,6 +4106,7 @@ export class AddressQueryService {
: typeof filterSet.account === "string"
? filterSet.account
: undefined,
warehouseHint: typeof filterSet.warehouse === "string" ? filterSet.warehouse : undefined,
periodFrom: typeof filterSet.period_from === "string" ? filterSet.period_from : undefined,
periodTo: typeof filterSet.period_to === "string" ? filterSet.period_to : undefined,
asOfDate: typeof filterSet.as_of_date === "string" ? filterSet.as_of_date : undefined,
@@ -4703,7 +4818,11 @@ export class AddressQueryService {
);
return {
handled: true,
reply_text: repairCounterpartyReplyLabel(replyText, intent.intent, filters.extracted_filters),
reply_text: repairCounterpartyReplyLabel(
ensureInventorySupplierOverlapLeadLine(intent.intent, replyText),
intent.intent,
filters.extracted_filters
),
reply_type: inferReplyType(responseType),
response_type: responseType,
debug: debugPayload
@@ -4871,7 +4990,7 @@ export class AddressQueryService {
return {
handled: true,
reply_text: repairCounterpartyReplyLabel(
input.replyText,
ensureInventorySupplierOverlapLeadLine(intent.intent, input.replyText),
intent.intent,
(input.extractedFilters ?? filters.extracted_filters) as AddressFilterSet
),
@@ -5207,7 +5326,7 @@ export class AddressQueryService {
resultMode: broadenedCoverageEvidence.result_mode ?? undefined
});
return buildFactualExecutionResult({
replyText: injectNoticeAfterLeadLine(broadenedFactual.text, broadenedPrefix),
replyText: injectAutoBroadenedPeriodNotice(intent.intent, broadenedFactual.text, broadenedPrefix),
responseType: broadenedFactual.responseType,
responseSemantics: broadenedFactual.semantics,
selectedRecipe: broadenedSelection.selected_recipe.recipe_id,
@@ -1552,20 +1552,11 @@ function buildInventoryAgingByPurchaseDateQuery(filters: AddressFilterSet, resol
buildInventoryItemReferenceCondition(filters, ["Остатки.Субконто1"])
];
const onHandWhereClause = buildStaticWhereClause(onHandScopeConditions);
const currentStockItemSubquery = [
"(ВЫБРАТЬ РАЗЛИЧНЫЕ",
" Остатки.Субконто1",
" ИЗ",
` РегистрБухгалтерии.Хозрасчетный.Остатки(${asOfExpr}, , , ) КАК Остатки`,
buildStaticWhereClause(onHandScopeConditions, " "),
")"
].join("\n");
const purchaseWhereClause = buildStaticWhereClause([
"Товары.Ссылка.Проведен = ИСТИНА",
`Товары.Ссылка.Дата <= ${asOfExpr}`,
buildOrganizationReferenceCondition(filters, ["Товары.Ссылка.Организация"]),
buildInventoryItemReferenceCondition(filters, ["Товары.Номенклатура"]),
`Товары.Номенклатура В ${currentStockItemSubquery}`
buildInventoryItemReferenceCondition(filters, ["Товары.Номенклатура"])
]);
return `
@@ -693,6 +693,9 @@ function shouldRestoreInventoryRootFrame(
if (!canReenterInventoryRoot) {
return false;
}
if (intent === "inventory_aging_by_purchase_date") {
return false;
}
if (intent !== "unknown" && !isInventoryIntent(intent) && !hasInventoryRootRestatementCue) {
return false;
}
@@ -723,20 +726,20 @@ function shouldRestoreInventoryRootFrame(
function hasSelectedObjectInventorySignal(text: string): boolean {
const repairedSelectedObjectText = textWithRepairedVariant(String(text ?? ""));
if (
/(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(
/(?:по\s+выбранному\s+объекту|выбранн(?:ому|ым)\s+(?:объекту|товару|товаром|позиции|позицией)|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(
repairedSelectedObjectText
)
) {
return true;
}
if (
/(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(
/(?:по\s+выбранному\s+объекту|выбранн(?:ому|ым)\s+(?:объекту|товару|товаром|позиции|позицией)|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(
String(text ?? "")
)
) {
return true;
}
return /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(
return /(?:по\s+выбранному\s+объекту|выбранн(?:ому|ым)\s+(?:объекту|товару|товаром|позиции|позицией)|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(
String(text ?? "")
);
}
@@ -775,6 +778,7 @@ export function hasInventoryPurchaseDocumentsFollowupCue(text: string): boolean
/(?:по\s+каким\s+документам\s+(?:это|его|ее|её|этот\s+товар|эту\s+позицию)\s+купили|по\s+каким\s+документам\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|какими\s+документами\s+(?:был\s+)?куплен|каким\s+документом\s+(?:это|его|ее|её|она|он|этот\s+товар|эта\s+позиция|эту\s+позицию)?\s*(?:подтвержден[оа]?|подтверждена\s+закупка|доказан[оа]?|закрыт[оа]?)|закупк[аи]\s+подтвержден[аы]?\s+документ|покажи\s+документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему))/iu.test(
value
) ||
/(?:документ[а-яё]*[\s\S]{0,40}поступлен[а-яё]*[\s\S]{0,140}(?:выбранн|товар|позици)|какие\s+документ[а-яё]*[\s\S]{0,40}поступлен[а-яё]*[\s\S]{0,140}(?:выбранн|товар|позици))/iu.test(value) ||
/(?:по\s+каким\s+документам\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|по\s+каким\s+документам\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|какими\s+документами\s+(?:был\s+)?куплен|каким\s+документом\s+(?:это|его|её|ее|она|он|этот\s+товар|эта\s+позиция|эту\s+позицию)?\s*(?:подтвержден[оа]?|доказан[оа]?|закрыт[оа]?)|покажи\s+документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(
value
) ||
@@ -1062,6 +1066,22 @@ function mergeFollowupFilters(
reasons.push("item_cleared_for_stock_slice_aging");
}
};
const clearInventoryOnHandWarehouseAliasItem = (): void => {
if (intent !== "inventory_on_hand_as_of_date") {
return;
}
const currentItem = toNonEmptyString(merged.item);
const currentWarehouse = toNonEmptyString(merged.warehouse);
if (!currentItem || !currentWarehouse) {
return;
}
const normalizedItem = normalizeOrganizationScopeSearchText(currentItem);
const normalizedWarehouse = normalizeOrganizationScopeSearchText(currentWarehouse);
if (normalizedItem && normalizedWarehouse && normalizedItem === normalizedWarehouse) {
delete merged.item;
reasons.push("item_cleared_as_warehouse_scope_alias_for_inventory_snapshot");
}
};
if (!followupContext) {
if (
(intent === "list_open_contracts" ||
@@ -1081,18 +1101,21 @@ function mergeFollowupFilters(
);
}
}
clearInventoryOnHandWarehouseAliasItem();
clearInventoryAgingOrganizationAliasItem(null);
return { filters: merged, reasons };
}
const previous = followupContext.previous_filters ?? {};
const root = followupContext.root_filters ?? {};
const previousAnchorValue = toNonEmptyString(followupContext.previous_anchor_value);
const previousCounterparty = toNonEmptyString(previous.counterparty);
const previousContract = toNonEmptyString(previous.contract);
const previousAccount = toNonEmptyString(previous.account);
const previousItem = toNonEmptyString(previous.item);
const previousAnchorItem = followupContext.previous_anchor_type === "item" ? previousAnchorValue : null;
const previousOrganization = toNonEmptyString(previous.organization);
const previousOrganization = toNonEmptyString(previous.organization) ?? toNonEmptyString(root.organization);
const previousWarehouse = toNonEmptyString(previous.warehouse) ?? toNonEmptyString(root.warehouse);
const previousAsOfDate = toNonEmptyString(previous.as_of_date);
const previousPeriodFrom = toNonEmptyString(previous.period_from);
const previousPeriodTo = toNonEmptyString(previous.period_to);
@@ -1539,6 +1562,10 @@ function mergeFollowupFilters(
reasons.push("period_derived_from_inventory_root_frame_year");
}
if (intent === "inventory_aging_by_purchase_date") {
if (previousWarehouse && !toNonEmptyString(merged.warehouse)) {
merged.warehouse = previousWarehouse;
reasons.push("warehouse_from_followup_context");
}
clearInventoryAgingOrganizationAliasItem(previousOrganization);
}
if (
@@ -1565,6 +1592,7 @@ function mergeFollowupFilters(
delete merged.period_to;
reasons.push("period_cleared_by_all_time_followup");
}
clearInventoryOnHandWarehouseAliasItem();
return { filters: merged, reasons };
}
@@ -1755,6 +1783,7 @@ function mergeFollowupFilters(
}
}
clearInventoryOnHandWarehouseAliasItem();
return { filters: merged, reasons };
}
@@ -1788,18 +1817,56 @@ function deriveIntentWithFollowupContext(
userMessage: string,
followupContext: AddressFollowupContext | null
): AddressIntentResolution {
const normalizedMessage = String(userMessage ?? "");
const hasSelectedObjectReference = hasSelectedObjectInventorySignal(normalizedMessage);
if (!followupContext || (!followupContext.previous_intent && !followupContext.target_intent)) {
if (hasSelectedObjectReference && hasInventoryPurchaseDocumentsFollowupCue(normalizedMessage)) {
if (
detectedIntent.intent === "unknown" ||
detectedIntent.intent === "list_documents_by_counterparty" ||
detectedIntent.intent === "list_documents_by_contract" ||
detectedIntent.intent === "inventory_on_hand_as_of_date"
) {
return {
intent: "inventory_purchase_documents_for_item",
confidence: "low",
reasons: [...detectedIntent.reasons, "intent_adjusted_to_inline_selected_object_inventory_documents"]
};
}
}
if (hasSelectedObjectReference && hasInventorySupplierFollowupCue(normalizedMessage)) {
if (
detectedIntent.intent === "unknown" ||
detectedIntent.intent === "list_documents_by_counterparty" ||
detectedIntent.intent === "list_documents_by_contract" ||
detectedIntent.intent === "inventory_on_hand_as_of_date"
) {
return {
intent: "inventory_purchase_provenance_for_item",
confidence: "low",
reasons: [...detectedIntent.reasons, "intent_adjusted_to_inline_selected_object_inventory_supplier"]
};
}
}
if (hasSelectedObjectReference && hasInventoryPurchaseDateFollowupCue(normalizedMessage)) {
if (detectedIntent.intent === "unknown" || detectedIntent.intent === "inventory_on_hand_as_of_date") {
return {
intent: "inventory_purchase_provenance_for_item",
confidence: "low",
reasons: [...detectedIntent.reasons, "intent_adjusted_to_inline_selected_object_inventory_purchase_date"]
};
}
}
return detectedIntent;
}
const normalizedMessage = String(userMessage ?? "");
const genericFollowupSignal = hasAddressFollowupContextSignal(normalizedMessage);
const previousFilters = followupContext.previous_filters ?? {};
const purchaseBridgeContinuationSignal =
followupContext.previous_intent === "vat_liability_confirmed_for_tax_period" &&
Boolean(toNonEmptyString(previousFilters.purchase_date_bridge_selected)) &&
hasInventoryPurchaseDateVatBridgeContinuationCue(normalizedMessage);
const hasFollowupSignal = genericFollowupSignal || purchaseBridgeContinuationSignal;
const hasFollowupSignal = genericFollowupSignal || purchaseBridgeContinuationSignal || hasSelectedObjectReference;
if (!hasFollowupSignal) {
return detectedIntent;
}
@@ -1834,7 +1901,6 @@ function deriveIntentWithFollowupContext(
followupContext.root_anchor_type === "item" ||
followupContext.current_frame_kind === "inventory_root" ||
followupContext.current_frame_kind === "inventory_drilldown";
const hasSelectedObjectReference = hasSelectedObjectInventorySignal(normalizedMessage);
const inventorySelectedObjectFollowup =
inventoryLineageActive && (hasSelectedObjectReference || (previousIsInventoryFamily && hasFollowupSignal));
const previousCounterpartyLaneActive =
@@ -14,7 +14,11 @@ import type { ComposeStageRow } from "./composeStage";
interface InventoryComposeOptions {
userMessage?: string;
rawUserMessage?: string;
itemHint?: string;
counterpartyHint?: string;
accountHint?: string;
warehouseHint?: string;
asOfDate?: string;
periodFrom?: string;
periodTo?: string;
@@ -134,6 +138,60 @@ function inventoryPartyListOrUnknown(parties: string[]): string {
return parties.length > 0 ? parties.slice(0, 4).join("; ") : "не выделен отдельным полем";
}
function inventoryPartyLeadList(parties: string[], limit = 6): string {
const visible = parties.slice(0, limit);
const remaining = parties.length - visible.length;
const suffix = remaining > 0 ? `; и еще ${remaining}` : "";
return `${visible.join("; ")}${suffix}`;
}
function inventoryItemLeadList(items: string[], limit = 6): string {
const visible = items.slice(0, limit);
const remaining = items.length - visible.length;
const suffix = remaining > 0 ? `; и еще ${remaining}` : "";
return `${visible.join("; ")}${suffix}`;
}
function collectInventoryTraceItems(rows: ComposeStageRow[]): string[] {
const result: string[] = [];
const seen = new Set<string>();
for (const row of rows) {
const item = String(row.item ?? "").trim();
if (!item) {
continue;
}
const comparable = normalizeInventoryReplyEntityToken(item);
if (!comparable || seen.has(comparable)) {
continue;
}
seen.add(comparable);
result.push(item);
}
return result;
}
function extractInventoryCounterpartiesFromEvidenceLines(lines: string[]): string[] {
const counterparties: string[] = [];
for (const line of lines) {
const match = /\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442:\s*([^|]+)/iu.exec(line);
const value = String(match?.[1] ?? "").replace(/\s+/gu, " ").trim();
if (value) {
counterparties.push(value);
}
}
return counterparties;
}
function hasSupplierScopedItemListQuestion(options: InventoryComposeOptions): boolean {
if (!String(options.counterpartyHint ?? "").trim()) {
return false;
}
const text = `${options.rawUserMessage ?? ""} ${options.userMessage ?? ""}`;
return /(?:какие?\s+товар|товар(?:ы|ов)?\s+(?:от|у)\s+поставщик|позици(?:и|я|ю)|номенклатур|лежат|остат(?:ок|ки|ка|ков))/iu.test(
text
);
}
function omitAmountFromInventoryTraceEvidence(lines: string[]): string[] {
return lines.map((line) => String(line ?? "").replace(/\s+\|\s+сумма:\s+[^|]+(?=\s+\||$)/gu, ""));
}
@@ -216,6 +274,43 @@ function sumInventoryRowQuantity(rows: ComposeStageRow[]): number {
return rows.reduce((sum, row) => sum + (typeof row.quantity === "number" && Number.isFinite(row.quantity) ? row.quantity : 0), 0);
}
function cleanInventoryScopeHint(value: string | null | undefined): string | null {
const cleaned = String(value ?? "").replace(/\s+/gu, " ").trim();
return cleaned.length > 0 ? cleaned : null;
}
function inventoryOnHandAccountScopeLabel(options: InventoryComposeOptions): string | null {
const accountHint = cleanInventoryScopeHint(options.accountHint);
if (accountHint) {
return `по счету ${accountHint}`;
}
const text = `${options.rawUserMessage ?? ""} ${options.userMessage ?? ""}`;
return /(?:^|[^\d])41(?:[.,]\d+)?(?:[^\d]|$)|41\s*сч[её]т/iu.test(text) ? "по счету 41" : null;
}
function inventoryOnHandWarehouseScopeLabel(
options: InventoryComposeOptions,
uniqueWarehouses: string[]
): string | null {
const warehouseHint = cleanInventoryScopeHint(options.warehouseHint);
if (warehouseHint) {
return `по складу ${warehouseHint}`;
}
const text = `${options.rawUserMessage ?? ""} ${options.userMessage ?? ""}`;
if (uniqueWarehouses.length === 1 && /(?:по|на)\s+склад[уе]?/iu.test(text)) {
return `по складу ${uniqueWarehouses[0]}`;
}
return null;
}
function inventoryOnHandScopePhrase(options: InventoryComposeOptions, uniqueWarehouses: string[]): string {
const scopes = [
inventoryOnHandAccountScopeLabel(options),
inventoryOnHandWarehouseScopeLabel(options, uniqueWarehouses)
].filter((item): item is string => Boolean(item));
return scopes.length > 0 ? scopes.join(" и ") : "на складе";
}
function formatInventoryPercent(value: number | null, formatNumberWithDots: (value: number, fractionDigits?: number) => string): string {
return value === null || !Number.isFinite(value) ? "не подтверждена" : `${formatNumberWithDots(value, 2)}%`;
}
@@ -354,10 +449,11 @@ export function composeInventoryReply(
positions.map((item) => String(item.warehouse ?? "").trim()).filter((item) => item.length > 0)
);
const totalAmount = positions.reduce((sum, item) => sum + item.amount, 0);
const scopePhrase = inventoryOnHandScopePhrase(options, uniqueWarehouses);
const directAnswerLine =
positions.length > 0
? `На ${deps.formatDateRu(asOfDate)} на складе подтверждено ${deps.formatNumberWithDots(positions.length)} позиций на ${deps.formatMoneyRub(totalAmount)}.`
: `На ${deps.formatDateRu(asOfDate)} подтвержденных товарных остатков по счету 41.01 не найдено.`;
? `На ${deps.formatDateRu(asOfDate)} ${scopePhrase} подтверждено ${deps.formatNumberWithDots(positions.length)} позиций на ${deps.formatMoneyRub(totalAmount)}.`
: `На ${deps.formatDateRu(asOfDate)} ${scopePhrase} подтвержденных товарных остатков не найдено.`;
const lines: string[] = [directAnswerLine];
if (positions.length > 0) {
@@ -392,13 +488,22 @@ export function composeInventoryReply(
]);
}
appendInventoryBulletSection(lines, "Сводка:", [
const summaryBullets = [
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
`Позиции с остатком: ${deps.formatNumberWithDots(positions.length)}.`,
`Уникальных товаров: ${deps.formatNumberWithDots(uniqueItems.length)}.`,
`Уникальных складов: ${deps.formatNumberWithDots(uniqueWarehouses.length)}.`,
"Общее количество не свожу в один управленческий показатель, потому что в остатках смешаны разнородные позиции."
]);
];
const accountScope = inventoryOnHandAccountScopeLabel(options);
const warehouseScope = inventoryOnHandWarehouseScopeLabel(options, uniqueWarehouses);
if (accountScope) {
summaryBullets.splice(1, 0, `Срез ${accountScope}.`);
}
if (warehouseScope) {
summaryBullets.splice(accountScope ? 2 : 1, 0, `Срез ${warehouseScope}.`);
}
appendInventoryBulletSection(lines, "Сводка:", summaryBullets);
if (rows.length !== positions.length) {
lines.push(`- Проверенных строк движения: ${deps.formatNumberWithDots(rows.length)}.`);
}
@@ -561,22 +666,50 @@ export function composeInventoryReply(
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
normalizeInventoryTraceSummaryCounterparties(summary);
const unresolvedRows = purchaseRows.filter((row) => deps.extractInventoryCounterpartyCandidates(row).length === 0);
const purchaseRowCounterparties = purchaseRows.map((row) =>
deps.uniqueStrings([
...deps.extractInventoryCounterpartyCandidates(row),
...extractInventoryCounterpartiesFromEvidenceLines(deps.formatInventoryTraceRows([row], 1))
])
);
const observedCounterparties = deps.uniqueStrings([
...summary.counterparties,
...purchaseRowCounterparties.flat()
]);
const unresolvedRows = purchaseRows.filter((_, index) => (purchaseRowCounterparties[index]?.length ?? 0) === 0);
const unresolvedSupplierQuestionText = `${String(options.userMessage ?? "")}\n${String(options.rawUserMessage ?? "")}`;
const unresolvedSupplierQuestion =
/(?:\u0431\u0435\u0437\s+\u043f\u043e\u043d\u044f\u0442\u043d[^\s]*\s+\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0431\u0435\u0437\s+(?:\u044f\u0432\u043d[^\s]*\s+)?\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043d\u0435\s+\u0438\u043c\u0435\u044e\u0442\s+\u044f\u0432\u043d[^\s]*\s+\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043d\u0435\u0442\s+\u044f\u0432\u043d[^\s]*\s+\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|unresolved\s+supplier\s+link)/iu.test(
String(options.userMessage ?? "")
unresolvedSupplierQuestionText
);
if (unresolvedSupplierQuestion) {
const unresolvedItems = collectInventoryTraceItems(unresolvedRows);
const unresolvedItemPreviewLimit = 3;
const unresolvedDocumentPreviewLimit = 2;
const directAnswerLine =
unresolvedRows.length > 0
? `В текущем складском срезе найдено операций без явно выделенного поставщика: ${deps.formatNumberWithDots(unresolvedRows.length)}.`
? unresolvedItems.length > 0
? `В текущем складском срезе на ${deps.formatDateRu(asOfDate)} найдено ${deps.formatNumberWithDots(unresolvedItems.length)} позиций без надежной привязки к поставщику; первые примеры ниже.`
: `В текущем складском срезе на ${deps.formatDateRu(asOfDate)} найдено операций без явно выделенного поставщика: ${deps.formatNumberWithDots(unresolvedRows.length)}.`
: "В текущем складском срезе товары без явно выделенной привязки к поставщику в доступных данных не найдены.";
const lines: string[] = [directAnswerLine];
if (unresolvedItems.length > 0) {
appendInventorySection(
lines,
"Позиции без надежной привязки к поставщику:",
unresolvedItems.slice(0, unresolvedItemPreviewLimit).map((item, index) => `${index + 1}. ${item}`)
);
if (unresolvedItems.length > unresolvedItemPreviewLimit) {
lines.push(
`- Показаны первые ${unresolvedItemPreviewLimit} из ${deps.formatNumberWithDots(unresolvedItems.length)} таких позиций.`
);
}
}
appendInventoryBulletSection(lines, "Что проверили:", [
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
`Закупочных операций в выборке: ${deps.formatNumberWithDots(purchaseRows.length)}.`,
`Операций без явно выделенного поставщика: ${deps.formatNumberWithDots(unresolvedRows.length)}.`,
`Поставщиков, выделенных в остальных операциях: ${deps.formatNumberWithDots(summary.counterparties.length)}.`
`Поставщиков, выделенных в остальных операциях: ${deps.formatNumberWithDots(observedCounterparties.length)}.`
]);
appendInventoryBulletSection(lines, "Ограничения:", [
"Без партионного учета это проверка доступного закупочного следа по складскому срезу, а не юридическое доказательство владельца каждой партии."
@@ -584,11 +717,16 @@ export function composeInventoryReply(
if (unresolvedRows.length > 0) {
appendInventorySection(
lines,
"Позиции без явно выделенного поставщика:",
deps.formatInventoryTraceRows(unresolvedRows, 12)
"Опорные документы:",
deps.formatInventoryTraceRows(unresolvedRows, unresolvedDocumentPreviewLimit)
);
} else if (summary.counterparties.length > 0) {
lines.push(`- В доступном закупочном следе встречаются поставщики: ${summary.counterparties.slice(0, 6).join("; ")}.`);
if (unresolvedRows.length > unresolvedDocumentPreviewLimit) {
lines.push(
`- Показаны первые ${unresolvedDocumentPreviewLimit} из ${deps.formatNumberWithDots(unresolvedRows.length)} строк без выделенного поставщика.`
);
}
} else if (observedCounterparties.length > 0) {
lines.push(`- В доступном закупочном следе встречаются поставщики: ${observedCounterparties.slice(0, 6).join("; ")}.`);
}
return buildFactualSummaryReply(
lines,
@@ -596,35 +734,100 @@ export function composeInventoryReply(
);
}
const warehouseLabel = summary.warehouses[0] ?? "не указанного склада";
const purchasePeriodLabel =
summary.firstPeriod && summary.lastPeriod
? `${deps.inventoryTraceDateLabel(summary.firstPeriod)}..${deps.inventoryTraceDateLabel(summary.lastPeriod)}`
: "даты закупки не выделены";
const supplierScopedItems = collectInventoryTraceItems(purchaseRows);
const supplierScopedItemQuestion = hasSupplierScopedItemListQuestion(options);
if (supplierScopedItemQuestion) {
const requestedSupplierLabel = summary.counterparties[0] ?? String(options.counterpartyHint ?? "").trim();
const directAnswerLine =
supplierScopedItems.length > 0
? `По поставщику ${requestedSupplierLabel} в складском срезе ${warehouseLabel} на ${deps.formatDateRu(asOfDate)} подтверждены позиции: ${inventoryItemLeadList(supplierScopedItems)}.`
: `По поставщику ${requestedSupplierLabel} в складском срезе ${warehouseLabel} на ${deps.formatDateRu(asOfDate)} позиции не подтверждены в доступном закупочном следе.`;
const lines: string[] = [directAnswerLine];
if (supplierScopedItems.length > 0) {
appendInventorySection(
lines,
"Позиции:",
supplierScopedItems.slice(0, 8).map((item, index) => `${index + 1}. ${item}`)
);
if (supplierScopedItems.length > 8) {
lines.push(`- Показаны первые 8 из ${deps.formatNumberWithDots(supplierScopedItems.length)} позиций.`);
}
}
appendInventoryBulletSection(lines, "Что проверили:", [
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
`Период найденного закупочного следа: ${purchasePeriodLabel}.`,
`Закупочных документов / операций в выборке: ${deps.formatNumberWithDots(summary.documents.length)} / ${deps.formatNumberWithDots(purchaseRows.length)}.`
]);
appendInventoryBulletSection(lines, "Ограничения:", [
"Без партионного учета этот ответ показывает закупочный след текущего остатка, но не доказывает владельца каждой конкретной партии."
]);
if (observedCounterparties.length > 0) {
lines.push(`- Найденные поставщики: ${inventoryPartyLeadList(observedCounterparties)}.`);
}
if (purchaseRows.length > 0) {
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,
buildConfirmedBalanceSemantics(
purchaseRows.length > 0 ? (supplierScopedItems.length > 0 ? "strong" : "medium") : "medium",
purchaseRows.length > 0
)
);
}
const directAnswerLine =
summary.counterparties.length === 1
? `По складскому остатку ${warehouseLabel} выявлен поставщик: ${summary.counterparties[0]}.`
: summary.counterparties.length > 1
? `По складскому остатку ${warehouseLabel} найдено несколько поставщиков: ${summary.counterparties.slice(0, 6).join("; ")}.`
: `По складскому остатку ${warehouseLabel} поставщик в доступных данных не выделен.`;
observedCounterparties.length === 1
? `По складскому остатку ${warehouseLabel} на ${deps.formatDateRu(asOfDate)} выявлен поставщик закупочного следа: ${observedCounterparties[0]}.`
: observedCounterparties.length > 1
? `По складскому остатку ${warehouseLabel} на ${deps.formatDateRu(asOfDate)} найдено несколько поставщиков закупочного следа: ${inventoryPartyLeadList(observedCounterparties)}.`
: unresolvedRows.length > 0
? `По складскому остатку ${warehouseLabel} на ${deps.formatDateRu(asOfDate)} однозначный поставщик текущего остатка не подтвержден: в ${deps.formatNumberWithDots(unresolvedRows.length)} закупочных операциях поставщик не выделен.`
: `По складскому остатку ${warehouseLabel} на ${deps.formatDateRu(asOfDate)} поставщик в доступных данных не выделен.`;
const lines: string[] = [directAnswerLine];
if (unresolvedRows.length > 0) {
lines.push(
`По складскому остатку ${warehouseLabel} на ${deps.formatDateRu(asOfDate)} однозначная атрибуция части текущего остатка не подтверждена: ${deps.formatNumberWithDots(unresolvedRows.length)} закупочных операций без явно выделенного поставщика.`
);
}
appendInventoryBulletSection(lines, "Что проверили:", [
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
`Первая найденная дата закупки: ${deps.inventoryTraceDateLabel(summary.firstPeriod)}.`,
`Последняя найденная дата закупки: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`,
`Закупочных документов в выборке: ${deps.formatNumberWithDots(summary.documents.length)}.`,
`Закупочных операций в выборке: ${deps.formatNumberWithDots(purchaseRows.length)}.`
`Период найденного закупочного следа: ${purchasePeriodLabel}.`,
`Закупочных документов / операций в выборке: ${deps.formatNumberWithDots(summary.documents.length)} / ${deps.formatNumberWithDots(purchaseRows.length)}.`
]);
appendInventoryBulletSection(lines, "Ограничения:", [
"Без партионного учета этот ответ показывает закупочный след текущего остатка, но не доказывает владельца каждой конкретной партии."
]);
if (summary.counterparties.length > 0) {
lines.push(`- Найденные поставщики: ${summary.counterparties.slice(0, 6).join("; ")}.`);
if (observedCounterparties.length > 0) {
lines.push(`- Найденные поставщики: ${inventoryPartyLeadList(observedCounterparties)}.`);
} else if (purchaseRows.length > 0) {
lines.push("- Закупочные движения найдены, но поставщик не выделен отдельным полем в доступных данных.");
} else {
lines.push("- В доступных данных не найдено закупочных движений по выбранному складскому срезу.");
}
if (unresolvedRows.length > 0) {
lines.push(`- Операций без явно выделенного поставщика: ${deps.formatNumberWithDots(unresolvedRows.length)}.`);
}
if (purchaseRows.length > 0) {
appendInventorySection(lines, "Опорные документы:", deps.formatInventoryTraceRows(purchaseRows, 10));
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,
@@ -1025,12 +1228,16 @@ export function composeInventoryReply(
const supplierMatches = inventoryRequestedPartyMatches(requestedSupplier, purchaseSummary.counterparties);
const buyerMatches = inventoryRequestedPartyMatches(requestedBuyer, saleSummary.counterparties);
const mismatchParts: string[] = [];
if (requestedSupplier && purchaseRows.length > 0 && !supplierMatches) {
if (requestedSupplier && purchaseRows.length === 0) {
mismatchParts.push(`закупка у запрошенного поставщика ${requestedSupplier} не найдена`);
} else if (requestedSupplier && purchaseRows.length > 0 && !supplierMatches) {
mismatchParts.push(
`запрошенный поставщик ${requestedSupplier} не совпал с найденным поставщиком: ${inventoryPartyListOrUnknown(purchaseSummary.counterparties)}`
);
}
if (requestedBuyer && saleRows.length > 0 && !buyerMatches) {
if (requestedBuyer && saleRows.length === 0) {
mismatchParts.push(`продажа/выбытие запрошенному покупателю ${requestedBuyer} не найдены`);
} else if (requestedBuyer && saleRows.length > 0 && !buyerMatches) {
mismatchParts.push(
`запрошенный покупатель ${requestedBuyer} не совпал с найденным покупателем: ${inventoryPartyListOrUnknown(saleSummary.counterparties)}`
);
@@ -1038,9 +1245,15 @@ export function composeInventoryReply(
const directAnswerLine =
mismatchParts.length > 0
? `Запрошенная цепочка по товару ${itemLabel} полностью не подтверждена: ${mismatchParts.join("; ")}.`
: purchaseSummary.counterparties.length === 1 && saleSummary.counterparties.length === 1
: purchaseRows.length > 0 && saleRows.length > 0 && purchaseSummary.counterparties.length === 1 && saleSummary.counterparties.length === 1
? `По товару ${itemLabel} цепочка поставки и продажи связана с поставщиком ${purchaseSummary.counterparties[0]} и покупателем ${saleSummary.counterparties[0]}.`
: `По товару ${itemLabel} цепочка поставки и продажи подтверждена частично или разнообразно: детали идут следом.`;
: purchaseRows.length > 0 && saleRows.length > 0
? `По товару ${itemLabel} найдены обе стороны цепочки поставки и продажи; участников несколько или они неоднозначны, детали ниже.`
: purchaseRows.length > 0
? `По товару ${itemLabel} закупочная часть цепочки подтверждена, но продажа/выбытие со счета 41.01 в доступных данных не найдены.`
: saleRows.length > 0
? `По товару ${itemLabel} найдена продажа/выбытие со счета 41.01, но закупочная часть цепочки в доступных данных не подтверждена.`
: `По товару ${itemLabel} цепочка поставки и продажи не подтверждена: движений по счету 41.01 в доступных данных не найдено.`;
const lines: string[] = [directAnswerLine, "", "Подтверждение:"];
lines.push(`- Строк закупки на 41.01: ${deps.formatNumberWithDots(purchaseRows.length)}.`);
lines.push(`- Строк продажи со счета 41.01: ${deps.formatNumberWithDots(saleRows.length)}.`);
@@ -7,6 +7,7 @@ export interface ComposeFactualReplyOptions<TVatDirectSourceProbe = unknown> {
counterpartyHint?: string;
organizationHint?: string;
accountHint?: string;
warehouseHint?: string;
periodFrom?: string;
periodTo?: string;
asOfDate?: string;
@@ -4,7 +4,11 @@ import type {
AddressLlmSemanticHints,
AddressSemanticFrame
} from "../../types/addressQuery";
import { isInventoryItemAnchorDegradation, isLowQualityInventoryItemAnchorValue } from "../addressFilterExtractor";
import {
isInventoryItemAnchorDegradation,
isLowQualityCounterpartyAnchorValue,
isLowQualityInventoryItemAnchorValue
} from "../addressFilterExtractor";
function toNonEmptyString(value: unknown): string | null {
if (value === null || value === undefined) {
@@ -162,11 +166,16 @@ export function applyAddressLlmSemanticHintsToExtraction(
}
if (semanticHints.scope_target_kind === "counterparty" && scopeTargetText) {
extractedFilters.counterparty = scopeTargetText;
pushWarning(warnings, "counterparty_from_llm_semantics");
semanticFrame.scope_kind = "explicit_anchor";
semanticFrame.anchor_kind = "counterparty";
semanticFrame.anchor_value = scopeTargetText;
if (isLowQualityCounterpartyAnchorValue(scopeTargetText)) {
delete extractedFilters.counterparty;
pushWarning(warnings, "counterparty_cleared_low_quality_llm_semantics");
} else {
extractedFilters.counterparty = scopeTargetText;
pushWarning(warnings, "counterparty_from_llm_semantics");
semanticFrame.scope_kind = "explicit_anchor";
semanticFrame.anchor_kind = "counterparty";
semanticFrame.anchor_value = scopeTargetText;
}
}
if (semanticHints.scope_target_kind === "contract" && scopeTargetText) {
@@ -189,6 +198,13 @@ export function applyAddressLlmSemanticHintsToExtraction(
pushWarning(warnings, "item_llm_semantics_ignored");
}
}
if (
toNonEmptyString(extractedFilters.counterparty) &&
isLowQualityCounterpartyAnchorValue(String(extractedFilters.counterparty))
) {
delete extractedFilters.counterparty;
pushWarning(warnings, "counterparty_cleared_low_quality_llm_semantics");
}
return {
...extraction,
@@ -76,6 +76,222 @@ function normalizeAddressReplyType(value: unknown): AssistantReplyType {
return value === "factual" || value === "partial_coverage" ? value : "partial_coverage";
}
function uniqueNormalizedStrings(values: Array<string | null | undefined>): string[] {
const result: string[] = [];
const seen = new Set<string>();
for (const value of values) {
const text = String(value ?? "").trim();
if (!text) {
continue;
}
const key = text.toLocaleLowerCase("ru-RU").replace(/ё/g, "е");
if (seen.has(key)) {
continue;
}
seen.add(key);
result.push(text);
}
return result;
}
function extractInventorySupplierOverlapCounterpartiesFromReply(replyText: string): string[] {
const values: string[] = [];
const pattern = /\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442:\s*([^|\r\n]+)/giu;
for (const match of replyText.matchAll(pattern)) {
const value = toNullableString(match[1]?.replace(/[.;,\s]+$/u, ""));
if (!value || /^(?:\u043d\u0435\s+\u0432\u044b\u0434\u0435\u043b\u0435\u043d|\u043d\/\u0434|unknown)$/iu.test(value)) {
continue;
}
values.push(value);
}
return uniqueNormalizedStrings(values);
}
function formatInventorySupplierOverlapCounterpartyLead(counterparties: string[], limit = 6): string {
const visible = counterparties.slice(0, limit);
const remaining = counterparties.length - visible.length;
const suffix = remaining > 0 ? `; и еще ${remaining}` : "";
return `${visible.join("; ")}${suffix}`;
}
function hasInventorySupplierOverlapDefensiveLead(firstLine: string): boolean {
const normalized = String(firstLine ?? "").toLocaleLowerCase("ru-RU").replace(/\u0451/gu, "\u0435");
const startsWithStockScope = /^\u043f\u043e\s+(?:\u0441\u043a\u043b\u0430\u0434\u0441\u043a\u043e\u043c\u0443\s+\u043e\u0441\u0442\u0430\u0442\u043a\u0443|\u043e\u043a\u043d\u0443)/u.test(
normalized
);
const hasDefensiveAttribution = /\u043e\u0434\u043d\u043e\u0437\u043d\u0430\u0447\u043d(?:\u044b\u0439\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0430\u044f\s+\u0430\u0442\u0440\u0438\u0431\u0443\u0446\u0438\u044f).*?\u043d\u0435\s+\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d/u.test(
normalized
);
return startsWithStockScope && hasDefensiveAttribution;
}
function firstNonEmptyReplyLine(text: unknown): string {
return String(text ?? "")
.split(/\r?\n/u)
.map((line) => line.trim())
.find((line) => line.length > 0) ?? "";
}
function hasInventorySupplierOverlapCounterpartyLead(firstLine: string): boolean {
const normalized = String(firstLine ?? "").toLocaleLowerCase("ru-RU").replace(/\u0451/gu, "\u0435");
return (
/^\u043f\u043e\s+\u0441\u043a\u043b\u0430\u0434\u0441\u043a\u043e\u043c\u0443\s+\u043e\u0441\u0442\u0430\u0442\u043a\u0443/u.test(normalized) &&
/\u043d\u0430\u0439\u0434\u0435\u043d/u.test(normalized) &&
/\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a/u.test(normalized) &&
/\u0437\u0430\u043a\u0443\u043f\u043e\u0447\u043d\u043e\u0433\u043e\s+\u0441\u043b\u0435\u0434\u0430/u.test(normalized)
);
}
function preserveInventorySupplierOverlapCounterpartyLeadAfterSanitize(input: {
rawReply: string;
sanitizedReply: string;
addressDebug: unknown;
}): { text: string; applied: boolean } {
const debug = toRecordObject(input.addressDebug);
const detectedIntent = toNullableString(debug?.detected_intent);
if (detectedIntent !== "inventory_supplier_stock_overlap_as_of_date") {
return { text: input.sanitizedReply, applied: false };
}
const rawLead = firstNonEmptyReplyLine(input.rawReply);
if (!hasInventorySupplierOverlapCounterpartyLead(rawLead)) {
return { text: input.sanitizedReply, applied: false };
}
const sanitizedLead = firstNonEmptyReplyLine(input.sanitizedReply);
if (hasInventorySupplierOverlapCounterpartyLead(sanitizedLead)) {
return { text: input.sanitizedReply, applied: false };
}
const sanitizedText = String(input.sanitizedReply ?? "").trim();
return {
text: sanitizedText ? [rawLead, sanitizedText].join("\n\n") : rawLead,
applied: true
};
}
function inspectInventorySupplierOverlapDefensiveLead(firstLine: string): {
stockScopeMatch: boolean;
defensiveMatch: boolean;
} {
const normalized = String(firstLine ?? "").toLocaleLowerCase("ru-RU").replace(/\u0451/gu, "\u0435");
return {
stockScopeMatch: /^\u043f\u043e\s+(?:\u0441\u043a\u043b\u0430\u0434\u0441\u043a\u043e\u043c\u0443\s+\u043e\u0441\u0442\u0430\u0442\u043a\u0443|\u043e\u043a\u043d\u0443)/u.test(
normalized
),
defensiveMatch: /\u043e\u0434\u043d\u043e\u0437\u043d\u0430\u0447\u043d(?:\u044b\u0439\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0430\u044f\s+\u0430\u0442\u0440\u0438\u0431\u0443\u0446\u0438\u044f).*?\u043d\u0435\s+\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d/u.test(
normalized
)
};
}
function buildInventorySupplierOverlapSurfaceRepairAudit(input: {
applied: boolean;
detectedIntent: string | null;
reasonCodes: string[];
counterparties?: string[];
firstLineSample?: string;
stockScopeMatch?: boolean;
defensiveMatch?: boolean;
}): Record<string, unknown> {
return {
schema_version: "inventory_supplier_overlap_surface_repair_v1",
applied: input.applied,
detected_intent: input.detectedIntent,
counterparties: input.counterparties ?? [],
first_line_sample: input.firstLineSample ?? null,
stock_scope_match: input.stockScopeMatch ?? null,
defensive_match: input.defensiveMatch ?? null,
reason_codes: input.reasonCodes
};
}
function repairInventorySupplierOverlapSurfaceReply(
replyText: unknown,
addressDebug: unknown
): { text: string; audit: Record<string, unknown> | null } {
const text = String(replyText ?? "");
const debug = toRecordObject(addressDebug);
const detectedIntent = toNullableString(debug?.detected_intent);
if (detectedIntent !== "inventory_supplier_stock_overlap_as_of_date") {
return {
text,
audit: buildInventorySupplierOverlapSurfaceRepairAudit({
applied: false,
detectedIntent,
reasonCodes: ["inventory_supplier_overlap_surface_repair_intent_not_applicable"]
})
};
}
const lines = text.split(/\r?\n/u);
const firstLine = lines[0]?.trim() ?? "";
const leadInspection = inspectInventorySupplierOverlapDefensiveLead(firstLine);
if (!hasInventorySupplierOverlapDefensiveLead(firstLine)) {
return {
text,
audit: buildInventorySupplierOverlapSurfaceRepairAudit({
applied: false,
detectedIntent,
firstLineSample: firstLine.slice(0, 220),
stockScopeMatch: leadInspection.stockScopeMatch,
defensiveMatch: leadInspection.defensiveMatch,
reasonCodes: ["inventory_supplier_overlap_surface_repair_first_line_not_defensive"]
})
};
}
const counterparties = extractInventorySupplierOverlapCounterpartiesFromReply(text);
if (counterparties.length === 0) {
return {
text,
audit: buildInventorySupplierOverlapSurfaceRepairAudit({
applied: false,
detectedIntent,
counterparties,
firstLineSample: firstLine.slice(0, 220),
stockScopeMatch: leadInspection.stockScopeMatch,
defensiveMatch: leadInspection.defensiveMatch,
reasonCodes: ["inventory_supplier_overlap_surface_repair_no_counterparties"]
})
};
}
const repairedLead =
counterparties.length === 1
? `По складскому остатку найден поставщик закупочного следа: ${counterparties[0]}.`
: `По складскому остатку найдено несколько поставщиков закупочного следа: ${formatInventorySupplierOverlapCounterpartyLead(counterparties)}.`;
return {
text: [repairedLead, firstLine, ...lines.slice(1)].join("\n"),
audit: buildInventorySupplierOverlapSurfaceRepairAudit({
applied: true,
detectedIntent,
counterparties,
firstLineSample: firstLine.slice(0, 220),
stockScopeMatch: leadInspection.stockScopeMatch,
defensiveMatch: leadInspection.defensiveMatch,
reasonCodes: ["inventory_supplier_overlap_surface_repair_applied"]
})
};
}
function extractSingleWarehouseScopeFromReply(replyText: string): string | null {
const values: string[] = [];
const normalized = String(replyText ?? "");
const byFieldPattern = /(?:^|\|)\s*склад\s*:\s*([^|\r\n]+)/giu;
for (const match of normalized.matchAll(byFieldPattern)) {
const value = toNullableString(match[1]?.replace(/[.;,\s]+$/u, ""));
if (value) {
values.push(value);
}
}
const fieldUnique = uniqueNormalizedStrings(values);
if (fieldUnique.length > 0) {
return fieldUnique.length === 1 ? fieldUnique[0] : null;
}
const leadMatch = normalized.match(/(?:по|на)\s+склад[уе]?\s+([^.,;\r\n]+)/iu);
const leadWarehouse = toNullableString(leadMatch?.[1]?.replace(/[.;,\s]+$/u, ""));
if (leadWarehouse) {
values.push(leadWarehouse);
}
const unique = uniqueNormalizedStrings(values);
return unique.length === 1 ? unique[0] : null;
}
function sameBusinessLabel(left: unknown, right: unknown): boolean {
const normalizedLeft = toNullableString(left)?.toLocaleLowerCase("ru-RU").replace(/ё/g, "е");
const normalizedRight = toNullableString(right)?.toLocaleLowerCase("ru-RU").replace(/ё/g, "е");
@@ -115,7 +331,10 @@ function cleanComparisonScopeCompanyLine(line: string, organization: string | nu
.replace(/\bcompany-level\b/giu, "общий по компании")
.replace(/\breusable bundle\b/giu, "сохраненный подтвержденный срез");
if (organization) {
clean = clean.replace(/по компании\s+Альтернатива Плюс/iu, `по компании ${organization}`);
clean = clean.replace(
/по компании\s+.+?(?=\s+(?:подтвержден|подтвержд[её]н|зафиксирован|показан|есть)\b|:|$)/iu,
`по компании ${organization}`
);
}
return clean.trim();
}
@@ -136,9 +355,7 @@ function buildComparisonScopeProofReply(input: {
session: unknown;
userMessage?: unknown;
}): { reply: string; audit: Record<string, unknown> } | null {
if (!hasComparisonScopeProofCue(input.userMessage)) {
return null;
}
const legalOrganizationFromCurrentTurn = legalOrganizationLabelFromClarification(input.userMessage);
const entryPoint = toRecordObject(input.debug.assistant_mcp_discovery_entry_point_v1);
const turnInput = toRecordObject(entryPoint?.turn_input);
const turnMeaning = toRecordObject(turnInput?.turn_meaning_ref);
@@ -146,6 +363,9 @@ function buildComparisonScopeProofReply(input: {
if (!isBusinessOverview) {
return null;
}
if (!hasComparisonScopeProofCue(input.userMessage) && !legalOrganizationFromCurrentTurn) {
return null;
}
const separateCandidates = Array.isArray(turnMeaning?.business_overview_separate_entity_candidates)
? turnMeaning.business_overview_separate_entity_candidates
: [];
@@ -172,7 +392,7 @@ function buildComparisonScopeProofReply(input: {
if (!incomingAmount && !outgoingAmount && !netAmount) {
return null;
}
const organization = legalOrganizationLabelFromClarification(input.userMessage)
const organization = legalOrganizationFromCurrentTurn
?? toNullableString(turnMeaning?.explicit_organization_scope)
?? toNullableString(toRecordObject(comparisonScope?.organization)?.label);
const documentCount = Number(toRecordObject(documentBundle)?.document_count);
@@ -355,8 +575,24 @@ export function runAssistantAddressLaneResponseRuntime<ResponseType = AssistantM
input: RunAssistantAddressLaneResponseRuntimeInput<ResponseType>
): RunAssistantAddressLaneResponseRuntimeOutput<ResponseType> {
const finalizeAddressTurnSafe = input.finalizeAddressTurn ?? finalizeAssistantAddressTurn;
const safeAddressReply = input.sanitizeOutgoingAssistantText(input.addressLane.reply_text);
const debug = input.buildAddressDebugPayload(input.addressLane.debug, input.llmPreDecomposeMeta);
const surfaceRepair = repairInventorySupplierOverlapSurfaceReply(
input.addressLane.reply_text,
debug
);
const safeAddressReplyBase = input.sanitizeOutgoingAssistantText(surfaceRepair.text);
const preservedAddressReply = preserveInventorySupplierOverlapCounterpartyLeadAfterSanitize({
rawReply: surfaceRepair.text,
sanitizedReply: safeAddressReplyBase,
addressDebug: debug
});
const safeAddressReply = preservedAddressReply.text;
if (surfaceRepair.audit) {
debug.inventory_supplier_overlap_surface_repair_v1 = {
...surfaceRepair.audit,
lead_preserved_after_sanitize: preservedAddressReply.applied
};
}
const followupOffer = input.buildAddressFollowupOffer(debug);
if (followupOffer) {
debug.address_followup_offer = followupOffer;
@@ -391,14 +627,38 @@ export function runAssistantAddressLaneResponseRuntime<ResponseType = AssistantM
followupContextSource?.root_filters && typeof followupContextSource.root_filters === "object"
? (followupContextSource.root_filters as Record<string, unknown>)
: null;
if (rootIntent || currentFrameKind) {
const detectedIntent = input.toNonEmptyString(debug.detected_intent);
const rootFrameWarehouse =
input.toNonEmptyString(rootFilters?.warehouse) ??
input.toNonEmptyString(debugFilters?.warehouse) ??
(detectedIntent === "inventory_on_hand_as_of_date" ? extractSingleWarehouseScopeFromReply(safeAddressReply) : null);
const shouldPersistInventoryRootFrame =
rootIntent || currentFrameKind || detectedIntent === "inventory_on_hand_as_of_date";
if (shouldPersistInventoryRootFrame) {
const rootFiltersForDebug: Record<string, unknown> = {
...(rootFilters ?? {}),
organization: input.toNonEmptyString(rootFilters?.organization) ?? input.toNonEmptyString(debugFilters?.organization),
warehouse: rootFrameWarehouse,
as_of_date: input.toNonEmptyString(rootFilters?.as_of_date) ?? input.toNonEmptyString(debugFilters?.as_of_date),
period_from: input.toNonEmptyString(rootFilters?.period_from) ?? input.toNonEmptyString(debugFilters?.period_from),
period_to: input.toNonEmptyString(rootFilters?.period_to) ?? input.toNonEmptyString(debugFilters?.period_to)
};
for (const key of Object.keys(rootFiltersForDebug)) {
if (!input.toNonEmptyString(rootFiltersForDebug[key])) {
delete rootFiltersForDebug[key];
}
}
debug.address_root_frame_context = {
root_intent: rootIntent,
current_frame_kind: currentFrameKind,
organization: input.toNonEmptyString(rootFilters?.organization),
as_of_date: input.toNonEmptyString(rootFilters?.as_of_date),
period_from: input.toNonEmptyString(rootFilters?.period_from),
period_to: input.toNonEmptyString(rootFilters?.period_to)
root_intent: rootIntent ?? (detectedIntent === "inventory_on_hand_as_of_date" ? "inventory_on_hand_as_of_date" : null),
root_filters: rootFiltersForDebug,
root_anchor_type: input.toNonEmptyString(followupContextSource?.root_anchor_type) ?? undefined,
root_anchor_value: input.toNonEmptyString(followupContextSource?.root_anchor_value) ?? undefined,
current_frame_kind: currentFrameKind ?? (detectedIntent === "inventory_on_hand_as_of_date" ? "inventory_root" : null),
organization: input.toNonEmptyString(rootFiltersForDebug.organization),
warehouse: input.toNonEmptyString(rootFiltersForDebug.warehouse),
as_of_date: input.toNonEmptyString(rootFiltersForDebug.as_of_date),
period_from: input.toNonEmptyString(rootFiltersForDebug.period_from),
period_to: input.toNonEmptyString(rootFiltersForDebug.period_to)
};
}
const debugWithRuntimeContracts = attachAssistantRuntimeContractShadow(debug, {
@@ -1479,9 +1479,23 @@ export function buildInventoryRootFrameFromAddressDebug(
}
const rootFiltersCandidate = toRecordObject(rootFrameContext?.root_filters);
const rootFrameFiltersCandidate = rootFrameContext
? {
organization: toNonEmptyString(rootFrameContext.organization),
warehouse: toNonEmptyString(rootFrameContext.warehouse),
as_of_date: toNonEmptyString(rootFrameContext.as_of_date),
period_from: toNonEmptyString(rootFrameContext.period_from),
period_to: toNonEmptyString(rootFrameContext.period_to)
}
: null;
const rootFrameFilters = rootFrameFiltersCandidate
? Object.fromEntries(
Object.entries(rootFrameFiltersCandidate).filter(([, value]) => Boolean(value))
)
: null;
const filters = {
...(rootFiltersCandidate ?? {}),
...(rootFiltersCandidate ? {} : extractedFilters)
...(rootFiltersCandidate ? {} : rootFrameFilters && Object.keys(rootFrameFilters).length > 0 ? rootFrameFilters : extractedFilters)
};
if (!filters.organization) {
const organization = readAddressDebugOrganization(debug, toNonEmptyString);
@@ -572,6 +572,37 @@ function hasInventoryMarginRankingAddressReply(
);
}
function hasExactInventoryPurchaseToSaleChainAddressReply(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
): boolean {
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
if (!hasEffectivelyFactualAddressReply(input)) {
return false;
}
const source = String(input.currentReplySource ?? input.livingChatSource ?? "").trim().toLowerCase();
if (source !== "address_query_runtime_v1" && source !== "address_exact" && source !== "address_lane") {
return false;
}
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
const capabilityId =
toNonEmptyString(input.addressRuntimeMeta?.capability_id) ??
toNonEmptyString(input.addressRuntimeMeta?.capability_contract_id);
if (
detectedIntent !== "inventory_purchase_to_sale_chain" &&
selectedRecipe !== "address_inventory_purchase_to_sale_chain_v1" &&
capabilityId !== "inventory_inventory_purchase_to_sale_chain"
) {
return false;
}
const mcpCallStatus = toNonEmptyString(input.addressRuntimeMeta?.mcp_call_status);
const responseType = toNonEmptyString(input.addressRuntimeMeta?.response_type);
return Boolean(hasConfirmedAddressExecution(input) || mcpCallStatus === "matched_non_empty" || responseType === "FACTUAL_LIST");
}
function hasValueFlowActionConflictWithDiscoveryTurnMeaning(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
@@ -651,6 +682,16 @@ function hasExactMatchedFactualAddressReply(
}
if (hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
const turnMeaning = readDiscoveryTurnMeaning(entryPoint);
const askedDomain = toNonEmptyString(turnMeaning?.asked_domain_family);
const askedAction = toNonEmptyString(turnMeaning?.asked_action_family);
if (
detectedIntent === "inventory_supplier_stock_overlap_as_of_date" &&
hasConfirmedAddressExecution(input) &&
(askedDomain === "entity_resolution" || askedAction === "search_business_entity")
) {
return true;
}
if (!(isMetadataDiscoveryTurn(entryPoint) && isInventoryExactAddressIntent(detectedIntent))) {
return false;
}
@@ -916,6 +957,10 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
const exactBankOperationsAddressReply = hasExactBankOperationsAddressReply(input, entryPoint);
const exactDocumentListAddressReply = hasExactDocumentListAddressReply(input, entryPoint);
const inventoryMarginRankingAddressReply = hasInventoryMarginRankingAddressReply(input, entryPoint);
const exactInventoryPurchaseToSaleChainAddressReply = hasExactInventoryPurchaseToSaleChainAddressReply(
input,
entryPoint
);
const openScopeValueFlowDiscoveryPriority = hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint);
const metadataDiscoveryPriority = hasMetadataDiscoveryPriority(input, entryPoint);
const valueFlowActionConflictWithDiscoveryTurnMeaning = hasValueFlowActionConflictWithDiscoveryTurnMeaning(
@@ -930,6 +975,10 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
exactBankOperationsAddressReply &&
!semanticConflictWithDiscoveryTurnMeaning &&
!valueFlowActionConflictWithDiscoveryTurnMeaning;
const exactInventoryChainProtectsCurrent =
exactInventoryPurchaseToSaleChainAddressReply &&
candidate.candidate_status === "clarification_candidate" &&
valueFlowActionConflictWithDiscoveryTurnMeaning;
if (!entryPoint) {
pushReason(reasonCodes, "mcp_discovery_response_policy_no_entry_point");
@@ -1009,6 +1058,12 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
if (inventoryMarginRankingAddressReply) {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_inventory_margin_ranking_address_reply");
}
if (exactInventoryChainProtectsCurrent) {
pushReason(
reasonCodes,
"mcp_discovery_response_policy_keep_exact_inventory_chain_reply_over_value_flow_clarification"
);
}
if (deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") {
pushReason(
reasonCodes,
@@ -1046,6 +1101,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
!exactBankOperationsProtectsCurrent &&
!exactDocumentListAddressReply &&
!inventoryMarginRankingAddressReply &&
!exactInventoryChainProtectsCurrent &&
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
candidate.eligible_for_future_hot_runtime &&
@@ -70,6 +70,13 @@ function detectSupportedIntent(text, deps) {
reason: "address_intent_resolver_current_turn_signal"
};
}
if (hasInventorySupplierStockOverlapExactSignal(text)) {
return {
intent: "inventory_supplier_stock_overlap_as_of_date",
confidence: "high",
reason: "inventory_supplier_stock_overlap_current_turn_signal"
};
}
if (/(?:\u043a\u0442\u043e\s+\u043d\u0430\u043c(?:\s+\p{L}+){0,4}\s+\u0434\u043e\u043b\u0436|\u043d\u0430\u043c\s+\u043a\u0442\u043e(?:\s+\p{L}+){0,4}\s+\u0434\u043e\u043b\u0436|\u0434\u0435\u0431\u0438\u0442\u043e\u0440|\u0434\u0435\u0431\u0438\u0442\u043e\u0440\u0441\u043a|\breceivables?\b)/iu.test(text)) {
return {
intent: "receivables_confirmed_as_of_date",
@@ -246,6 +253,30 @@ function hasSelectedObjectInventoryExactSignal(text) {
);
}
function hasInventorySupplierStockOverlapExactSignal(text) {
const normalized = String(text ?? "");
if (!normalized) {
return false;
}
const hasStockScope =
/(?:\u0441\u043a\u043b\u0430\u0434|\u043e\u0441\u0442\u0430\u0442|\u043b\u0435\u0436\p{L}*|\u043d\u0430\u0445\u043e\u0434\p{L}*|\u0447\u0438\u0441\u043b\p{L}*|warehouse|stock|inventory|on\s+hand)/iu.test(
normalized
);
const hasSupplierCue =
/(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043f\u043e\u0441\u0442\u0430\u0432\p{L}*|\u0437\u0430\u043a\u0443\u043f|\u043a\u0443\u043f\p{L}*|\u043f\u0440\u0438\u043e\u0431\u0440\p{L}*|supplier|vendor|purchase|bought|supplied|procurement)/iu.test(
normalized
);
const hasOverlapAction =
/(?:\u0443\s+\u043a\u0430\u043a\u043e\u0433\u043e\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043f\u043e\s+\u043a\u0430\u043a\u043e\u043c\u0443\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043e\u0442\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043e\u043f\u0440\u0435\u0434\u0435\u043b\p{L}*[\s\S]{0,80}\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043d\u0430\u0439\u0442\p{L}*[\s\S]{0,80}\u0442\u043e\u0432\u0430\u0440|\u043a\u0430\u043a\u0438\u0435\s+\u0442\u043e\u0432\u0430\u0440|supplier[\s\S]{0,80}(?:stock|inventory)|(?:stock|inventory)[\s\S]{0,80}supplier)/iu.test(
normalized
);
const hasSupplierIdentityAsk =
/(?:\u043a\u0430\u043a\u043e(?:\u0433\u043e|\u043c\u0443)\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043e\u043f\u0440\u0435\u0434\u0435\u043b\p{L}*\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|which\s+supplier|what\s+supplier)/iu.test(
normalized
);
return hasStockScope && hasSupplierCue && (hasOverlapAction || hasSupplierIdentityAsk);
}
function hasOrganizationLevelEarningsOverviewSignal(text) {
const normalized = String(text ?? "");
if (!normalized || hasExplicitCounterpartyValueObject(normalized)) {
@@ -508,13 +539,17 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
const counterpartyBidirectionalValueFlow = compactOrganizationCashflowDisplay ? null : detectCounterpartyBidirectionalValueFlowFamily(joinedText);
const counterpartyTurnover = compactOrganizationCashflowDisplay ? null : detectCounterpartyTurnoverFamily(joinedText);
const selectedObjectInventoryExact = hasSelectedObjectInventoryExactSignal(joinedText);
const llmIntent = toNonEmptyString(input?.llmPreDecomposeMeta?.predecomposeContract?.intent, deps);
const inventorySupplierStockOverlapExact =
supportedIntent?.intent === "inventory_supplier_stock_overlap_as_of_date" ||
llmIntent === "inventory_supplier_stock_overlap_as_of_date" ||
hasInventorySupplierStockOverlapExactSignal(joinedText);
const broadBusinessEvaluation =
compactOrganizationCashflowDisplay
? { family: "broad_business_evaluation" }
: selectedObjectInventoryExact || counterpartyBidirectionalValueFlow?.family
: selectedObjectInventoryExact || inventorySupplierStockOverlapExact || counterpartyBidirectionalValueFlow?.family
? null
: detectBroadBusinessEvaluation(joinedText);
const llmIntent = toNonEmptyString(input?.llmPreDecomposeMeta?.predecomposeContract?.intent, deps);
const explicitIntentCandidate =
broadBusinessEvaluation?.family
? null
@@ -585,8 +620,10 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
? "confirmed_tax_period"
: explicitIntentCandidate === "vat_payable_confirmed_as_of_date"
? "confirmed_snapshot"
: explicitIntentCandidate === "vat_payable_forecast"
? "forecast"
: explicitIntentCandidate === "vat_payable_forecast"
? "forecast"
: explicitIntentCandidate === "inventory_supplier_stock_overlap_as_of_date"
? "supplier_overlap"
: explicitIntentCandidate === "list_documents_by_counterparty"
? "list_documents"
: counterpartyTurnover?.family