import { runAssistantRoutePolicyRuntime } from "./assistantRoutePolicyRuntimeAdapter"; import { runAssistantMcpDiscoveryRuntimeEntryPoint, type RunAssistantMcpDiscoveryRuntimeEntryPointInput } from "./assistantMcpDiscoveryRuntimeEntryPoint"; import { hasInventoryProfitabilityCue } from "./inventoryLifecycleCueHelpers"; export interface BuildAssistantAddressOrchestrationRuntimeInput { userMessage: string; sessionItems: unknown[]; sessionAddressNavigationState?: unknown; sessionOrganizationScope?: { knownOrganizations?: unknown; selectedOrganization?: unknown; activeOrganization?: unknown; } | null; llmProvider: unknown; useMock: boolean; featureAddressLlmPredecomposeV1: boolean; runAddressLlmPreDecompose: () => Promise>; buildAddressLlmPredecomposeContractV1: (input: { sourceMessage: string; canonicalMessage: string; }) => unknown; sanitizeAddressMessageForFallback: (userMessage: string) => string; toNonEmptyString: (value: unknown) => string | null; resolveAddressFollowupCarryoverContext: ( userMessage: string, sessionItems: unknown[], addressInputMessage: string, addressPreDecompose: Record, sessionAddressNavigationState?: unknown ) => AssistantAddressCarryoverLike | null; resolveAssistantOrchestrationDecision: (input: { rawUserMessage: string; effectiveAddressUserMessage: string; followupContext: unknown; llmPreDecomposeMeta: Record; sessionItems?: unknown[]; sessionOrganizationScope?: unknown; useMock: boolean; }) => Record; buildAddressDialogContinuationContractV2: ( userMessage: string, addressInputMessage: string, carryover: AssistantAddressCarryoverLike | null, addressPreDecompose: Record ) => unknown; runMcpDiscoveryRuntimeEntryPoint?: ( input: RunAssistantMcpDiscoveryRuntimeEntryPointInput ) => Promise>; } export interface AssistantAddressCarryoverLike { followupContext?: unknown; [key: string]: unknown; } export interface BuildAssistantAddressOrchestrationRuntimeOutput { addressPreDecompose: Record; addressInputMessage: string; carryover: AssistantAddressCarryoverLike | null; orchestrationDecision: Record; addressRuntimeMeta: Record; livingModeDecision: { mode: unknown; reason: unknown; }; } function toRecordObject(value: unknown): Record | null { if (!value || typeof value !== "object" || Array.isArray(value)) { return null; } return value as Record; } function hasSelectedObjectInventorySignal(text: string | null): boolean { return /(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ним|selected\s+object)/iu.test( String(text ?? "") ); } function hasSelectedObjectInventoryActionCue(text: string | null): boolean { const value = String(text ?? ""); return ( /(?:кому[\s\S]{0,80}(?:продал[аи]?|реализова[нлт][а-я]*|поставил[аи]?|поставлен[а-я]*|отгрузил[аи]?|отгружен[а-я]*)|кому\s+был\s+продан|куда[\s\S]{0,80}(?:продал[аи]?|реализова[нлт][а-я]*|поставил[аи]?|поставлен[а-я]*|отгрузил[аи]?|отгружен[а-я]*)|кто[\s\S]{0,40}купил|кто\s+это\s+поставил|кто\s+поставил|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+мы\s+купили|где\s+куплено|по\s+каким\s+документам|какими\s+документами|покажи\s+документы|документы[\s\S]{0,80}(?:по\s+(?:ним|ней|нему|этой\s+позиции|этому\s+товару)|операци)|документы\s+закупки|buyer|sale\s+trace|supplier|vendor|purchase\s+documents|purchase[\s-]?to[\s-]?sale|old\s+purchase|aged\s+stock)/iu.test( value ) || hasInventoryProfitabilityCue(value) ); } function hasShortInventoryPurchaseFollowupCue(text: string | null): boolean { return /(?:^|[\s,.;:!?])(а\s+)?(?:купили\s+у\s+кого|у\s+кого\s+купили|поставщик|продавец|seller)(?:[\s,.;:!?]|$)/iu.test( String(text ?? "") ); } function isInventorySelectedObjectOrRootIntent(intent: string | null): boolean { return ( intent === "inventory_on_hand_as_of_date" || intent === "inventory_purchase_provenance_for_item" || intent === "inventory_purchase_documents_for_item" || intent === "inventory_sale_trace_for_item" || intent === "inventory_profitability_for_item" || intent === "inventory_purchase_to_sale_chain" || intent === "inventory_aging_by_purchase_date" ); } function isGenericCanonicalDriftIntent(intent: string | null): boolean { return ( intent === "open_items_by_counterparty_or_contract" || intent === "customer_revenue_and_payments" || intent === "list_documents_by_counterparty" || intent === "list_documents_by_contract" || intent === "bank_operations_by_counterparty" || intent === "bank_operations_by_contract" || intent === "documents_forming_balance" ); } function hasSameDateFollowupSignal(text: string | null): boolean { return /(?:эту\s+же\s+дат(?:у|е|ой)|ту\s+же\s+дат(?:у|е|ой)|same\s+date)/iu.test(String(text ?? "")); } function hasExplicitCurrentDateSignal(text: string | null): boolean { return /(?:текущ(?:ую|ая|ий|ее|ей)\s+дат(?:у|а|е|ой)|сегодняшн(?:юю|ий|ей)\s+дат(?:у|а|е|ой)|today|current\s+date)/iu.test( String(text ?? "") ); } function hasInventoryTemporalRootFollowupCue(text: string | null): boolean { const value = String(text ?? "").trim().toLowerCase(); if (!value) { return false; } const tokenCount = value.split(/\s+/).filter(Boolean).length; const hasMonthYearCue = /(?:январ(?:ь|е)|феврал(?:ь|е)|март(?:е)?|апрел(?:ь|е)|ма(?:й|е)|июн(?:ь|е)|июл(?:ь|е)|август(?:е)?|сентябр(?:ь|е)|октябр(?:ь|е)|ноябр(?:ь|е)|декабр(?:ь|е))(?:\s+\d{4})?/iu.test( value ) || /\b(?:19|20)\d{2}\b/u.test(value); if (tokenCount <= 3 && hasMonthYearCue) { return true; } const hasInventoryLexeme = /(?:остат|склад|товар|позици|номенклатур)/iu.test(value); return hasInventoryLexeme && (hasMonthYearCue || hasSameDateFollowupSignal(value)); } function shouldPreferRawFollowupMessage( userMessage: string, addressInputMessage: string, carryover: AssistantAddressCarryoverLike | null, addressPreDecompose: Record, toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"] ): boolean { if (!carryover?.followupContext || typeof carryover.followupContext !== "object") { return false; } const rawMessage = toNonEmptyString(userMessage); const canonicalMessage = toNonEmptyString(addressInputMessage); if (!rawMessage || !canonicalMessage || rawMessage === canonicalMessage) { return false; } const predecomposeContract = addressPreDecompose?.predecomposeContract && typeof addressPreDecompose.predecomposeContract === "object" ? (addressPreDecompose.predecomposeContract as Record) : null; const mode = toNonEmptyString(predecomposeContract?.mode) ?? "unknown"; const intent = toNonEmptyString(predecomposeContract?.intent) ?? "unknown"; const followupContext = carryover.followupContext && typeof carryover.followupContext === "object" ? (carryover.followupContext as Record) : null; const previousIntent = toNonEmptyString(followupContext?.previous_intent); const rootIntent = toNonEmptyString(followupContext?.root_intent); const previousAnchorType = toNonEmptyString(followupContext?.previous_anchor_type); const hasReferentialDocumentExclusionFollowupCue = /(?:\u043a\u0440\u043e\u043c\u0435|\u043f\u043e\u043c\u0438\u043c\u043e)\s+(?:\u044d\u0442\u043e\u0433\u043e|\u044d\u0442\u043e\u0439|\u044d\u0442\u043e\u0442|\u044d\u0442\u0443|\u044d\u0442\u0438\u0445)(?:\s+(?:\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430|\u0434\u043e\u0433\u043e\u0432\u043e\u0440\u0430|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430))?/iu.test( rawMessage ); const hasInventoryItemCarryover = previousAnchorType === "item" && isInventorySelectedObjectOrRootIntent(previousIntent); const hasInventoryFrameCarryover = isInventorySelectedObjectOrRootIntent(previousIntent) || isInventorySelectedObjectOrRootIntent(rootIntent); const hasDocumentCarryover = previousIntent === "list_documents_by_counterparty" || previousIntent === "list_documents_by_contract"; if (mode === "unsupported" && intent === "unknown") { return true; } if (hasDocumentCarryover && hasReferentialDocumentExclusionFollowupCue) { return true; } if (hasSameDateFollowupSignal(rawMessage) && hasExplicitCurrentDateSignal(canonicalMessage)) { return true; } if ( hasInventoryFrameCarryover && hasInventoryTemporalRootFollowupCue(rawMessage) && (intent === "account_balance_snapshot" || intent === "documents_forming_balance" || intent === "unknown") ) { return true; } return ( (hasSelectedObjectInventorySignal(rawMessage) || hasInventoryItemCarryover) && (hasSelectedObjectInventoryActionCue(rawMessage) || hasShortInventoryPurchaseFollowupCue(rawMessage)) && (isGenericCanonicalDriftIntent(intent) || intent === "unknown") ); } function fallbackAddressPreDecompose( userMessage: string, llmProvider: unknown, buildAddressLlmPredecomposeContractV1: BuildAssistantAddressOrchestrationRuntimeInput["buildAddressLlmPredecomposeContractV1"], sanitizeAddressMessageForFallback: BuildAssistantAddressOrchestrationRuntimeInput["sanitizeAddressMessageForFallback"] ): Record { const provider = llmProvider === "local" ? "local" : llmProvider === "openai" ? "openai" : null; return { attempted: false, applied: false, provider, traceId: null, effectiveMessage: userMessage, reason: "disabled_by_feature_flag", llmCanonicalCandidateDetected: false, predecomposeContract: buildAddressLlmPredecomposeContractV1({ sourceMessage: userMessage, canonicalMessage: userMessage }), fallbackRuleHit: null, sanitizedUserMessage: sanitizeAddressMessageForFallback(userMessage), toolGateDecision: null, toolGateReason: null }; } export async function buildAssistantAddressOrchestrationRuntime( input: BuildAssistantAddressOrchestrationRuntimeInput ): Promise { const initialAddressPreDecompose = input.featureAddressLlmPredecomposeV1 ? await input.runAddressLlmPreDecompose() : fallbackAddressPreDecompose( input.userMessage, input.llmProvider, input.buildAddressLlmPredecomposeContractV1, input.sanitizeAddressMessageForFallback ); let addressPreDecompose = initialAddressPreDecompose; let addressInputMessage = input.toNonEmptyString(addressPreDecompose?.effectiveMessage) ?? input.userMessage; let carryover = input.resolveAddressFollowupCarryoverContext( input.userMessage, input.sessionItems, addressInputMessage, addressPreDecompose, input.sessionAddressNavigationState ); if ( shouldPreferRawFollowupMessage( input.userMessage, addressInputMessage, carryover, addressPreDecompose, input.toNonEmptyString ) ) { addressInputMessage = input.userMessage; addressPreDecompose = { ...addressPreDecompose, applied: false, effectiveMessage: input.userMessage, reason: "followup_raw_message_preferred_over_llm_rewrite", predecomposeContract: input.buildAddressLlmPredecomposeContractV1({ sourceMessage: input.userMessage, canonicalMessage: input.userMessage }) }; carryover = input.resolveAddressFollowupCarryoverContext( input.userMessage, input.sessionItems, addressInputMessage, addressPreDecompose, input.sessionAddressNavigationState ); } const followupContext = toRecordObject(carryover?.followupContext); const routePolicyRuntime = runAssistantRoutePolicyRuntime({ rawUserMessage: input.userMessage, effectiveAddressUserMessage: addressInputMessage, followupContext, llmPreDecomposeMeta: addressPreDecompose, sessionItems: input.sessionItems, sessionOrganizationScope: input.sessionOrganizationScope ?? null, useMock: input.useMock, resolveAssistantOrchestrationDecision: input.resolveAssistantOrchestrationDecision }); const orchestrationDecision = routePolicyRuntime.orchestrationDecision; const orchestrationContract = toRecordObject(orchestrationDecision.orchestrationContract); const predecomposeContract = toRecordObject(addressPreDecompose.predecomposeContract); const dialogContinuationContract = input.buildAddressDialogContinuationContractV2( input.userMessage, addressInputMessage, carryover, addressPreDecompose ); const runDiscoveryEntryPoint = input.runMcpDiscoveryRuntimeEntryPoint ?? runAssistantMcpDiscoveryRuntimeEntryPoint; let mcpDiscoveryRuntimeEntryPoint: Record | null = null; let mcpDiscoveryRuntimeEntryPointError: string | null = null; try { mcpDiscoveryRuntimeEntryPoint = (await runDiscoveryEntryPoint({ userMessage: input.userMessage, effectiveMessage: addressInputMessage, assistantTurnMeaning: toRecordObject(orchestrationContract?.assistant_turn_meaning), predecomposeContract, followupContext })) as Record; } catch (error) { mcpDiscoveryRuntimeEntryPointError = String(error instanceof Error ? error.message : error ?? "unknown_error").slice(0, 280); } const addressRuntimeMeta = { ...addressPreDecompose, toolGateDecision: orchestrationDecision.toolGateDecision ?? null, toolGateReason: orchestrationDecision.toolGateReason ?? null, dialogContinuationContract, orchestrationContract: orchestrationContract ?? null, routePolicyContract: routePolicyRuntime.routePolicyContract, mcpDiscoveryRuntimeEntryPoint, mcpDiscoveryRuntimeEntryPointError }; return { addressPreDecompose, addressInputMessage, carryover, orchestrationDecision, addressRuntimeMeta, livingModeDecision: routePolicyRuntime.livingModeDecision }; }