import { buildAddressMemoryRecapReply as buildAddressMemoryRecapReplyFromPolicy, buildInventoryHistoryCapabilityFollowupReply as buildInventoryHistoryCapabilityFollowupReplyFromPolicy, resolveAssistantLivingChatMemoryContext } from "./assistantMemoryRecapPolicy"; import { formatIsoDateForReply, resolveAssistantOrganizationAuthority } from "./assistantContinuityPolicy"; export interface AssistantLivingChatSessionScopeInput { knownOrganizations?: unknown[]; selectedOrganization?: unknown; activeOrganization?: unknown; } export interface AssistantLivingChatModeDecisionInput { mode?: string | null; reason?: string | null; } export interface AssistantLivingChatRuntimeInput { userMessage: string; sessionItems: unknown[]; modeDecision?: AssistantLivingChatModeDecisionInput | null; sessionScope: AssistantLivingChatSessionScopeInput; addressRuntimeMeta?: Record | null; traceIdFactory: () => string; toNonEmptyString: (value: unknown) => string | null; mergeKnownOrganizations: (values: unknown[]) => string[]; hasAssistantDataScopeMetaQuestionSignal: (message: string) => boolean; shouldHandleAsAssistantCapabilityMetaQuery: (message: string) => boolean; hasDestructiveDataActionSignal: (message: string) => boolean; hasDangerOrCoercionSignal: (message: string) => boolean; hasOperationalAdminActionRequestSignal: (message: string) => boolean; hasOrganizationFactLookupSignal: (message: string) => boolean; hasOrganizationFactFollowupSignal: (message: string, items: unknown[]) => boolean; hasLivingChatSignal: (message: string) => boolean; shouldEmitOrganizationSelectionReply: (message: string, activeOrganization: string | null) => boolean; hasAssistantCapabilityQuestionSignal: (message: string) => boolean; resolveDataScopeProbe: () => Promise | null>; executeLlmChat: () => Promise; applyScriptGuard: (chatText: string, userMessage: string) => { text: string; applied: boolean; reason: string | null; }; applyGroundingGuard: (input: { userMessage: string; chatText: string; organization: string | null; }) => { text: string; applied: boolean; reason: string | null; }; buildAssistantSafetyRefusalReply: () => string; buildAssistantDataScopeContractReply: (scopeProbe: Record | null) => string; buildAssistantProactiveOrganizationOfferReply: (scopeProbe: Record | null) => string; buildAssistantOrganizationFactBoundaryReply: (organization: string | null) => string; buildAssistantDataScopeSelectionReply: (organization: string | null) => string; buildAssistantOperationalBoundaryReply: () => string; buildAssistantCapabilityContractReply: (userMessage?: string) => string; } export interface AssistantLivingChatRuntimeOutput { handled: boolean; chatText: string; debug: Record | null; } function hasPriorAssistantTurn(items: unknown[]): boolean { if (!Array.isArray(items)) { return false; } return items.some((item) => item && typeof item === "object" && (item as { role?: string }).role === "assistant"); } function buildDeterministicSmalltalkLeadReply(): string { return "\u041f\u0440\u0438\u0432\u0435\u0442! \u0412\u0441\u0451 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e."; } function buildInventoryHistoryCapabilityFollowupReply(input: { organization: string | null; addressDebug: Record | null; toNonEmptyString: (value: unknown) => string | null; }): string { const rootFrameContext = input.addressDebug?.address_root_frame_context && typeof input.addressDebug.address_root_frame_context === "object" ? (input.addressDebug.address_root_frame_context as Record) : null; const extractedFilters = input.addressDebug?.extracted_filters && typeof input.addressDebug.extracted_filters === "object" ? (input.addressDebug.extracted_filters as Record) : null; const organization = input.organization ?? input.toNonEmptyString(rootFrameContext?.organization) ?? input.toNonEmptyString(extractedFilters?.organization); const lastAsOfDate = formatIsoDateForReply(rootFrameContext?.as_of_date) ?? formatIsoDateForReply(extractedFilters?.as_of_date); const organizationPart = organization ? ` по компании «${organization}»` : ""; const referenceLine = lastAsOfDate ? `Да, могу. Сейчас мы уже смотрели складской срез${organizationPart} на ${lastAsOfDate}.` : `Да, могу показать исторические данные${organizationPart} в этом же складском контуре.`; return [ referenceLine, `Могу показать исторические остатки${organizationPart} за нужный месяц, дату или год.`, "Например:", "- `на март 2020`", "- `на июнь 2016`", "- `за 2017 год`", "- `сравни июнь 2016 с текущим срезом`", "Если хочешь, сразу покажу нужный исторический период." ].join("\n"); } function buildAddressMemoryRecapReply(input: { organization: string | null; addressDebug: Record | null; toNonEmptyString: (value: unknown) => string | null; }): string { const extractedFilters = input.addressDebug?.extracted_filters && typeof input.addressDebug.extracted_filters === "object" ? (input.addressDebug.extracted_filters as Record) : null; const rootFrameContext = input.addressDebug?.address_root_frame_context && typeof input.addressDebug.address_root_frame_context === "object" ? (input.addressDebug.address_root_frame_context as Record) : null; const item = input.toNonEmptyString(extractedFilters?.item) ?? (String(input.addressDebug?.anchor_type ?? "") === "item" ? input.toNonEmptyString(input.addressDebug?.anchor_value_resolved) ?? input.toNonEmptyString(input.addressDebug?.anchor_value_raw) : null); const organization = input.organization ?? input.toNonEmptyString(extractedFilters?.organization) ?? input.toNonEmptyString(rootFrameContext?.organization); const scopedDate = formatIsoDateForReply(extractedFilters?.as_of_date) ?? formatIsoDateForReply(rootFrameContext?.as_of_date) ?? formatIsoDateForReply(extractedFilters?.period_to); if (item) { const datePart = scopedDate ? ` в срезе на ${scopedDate}` : ""; const organizationPart = organization ? ` по компании «${organization}»` : ""; return [ `Да, помню. Мы обсуждали позицию «${item}»${organizationPart}${datePart}.`, "Могу продолжить по ней без переписывания сущности: кто поставил, когда купили, по каким документам или кому продали." ].join(" "); } if (organization || scopedDate) { const organizationPart = organization ? ` по компании «${organization}»` : ""; const datePart = scopedDate ? ` на ${scopedDate}` : ""; return [ `Да, помню. Мы уже смотрели адресный контур${organizationPart}${datePart}.`, "Могу кратко напомнить контекст или сразу продолжить следующий шаг по этому же сценарию." ].join(" "); } return "Да, помню предыдущий адресный контур. Могу кратко напомнить, что мы уже подтвердили, или сразу продолжить следующий шаг."; } export async function runAssistantLivingChatRuntime( input: AssistantLivingChatRuntimeInput ): Promise { const userMessage = String(input.userMessage ?? ""); const organizationAuthority = resolveAssistantOrganizationAuthority({ sessionItems: input.sessionItems, sessionKnownOrganizations: Array.isArray(input.sessionScope.knownOrganizations) ? input.sessionScope.knownOrganizations : [], sessionSelectedOrganization: input.sessionScope.selectedOrganization, sessionActiveOrganization: input.sessionScope.activeOrganization, toNonEmptyString: input.toNonEmptyString, normalizeOrganizationScopeValue: input.toNonEmptyString, mergeKnownOrganizations: input.mergeKnownOrganizations }); const continuitySnapshot = organizationAuthority.continuitySnapshot; const dataScopeMetaQuery = input.hasAssistantDataScopeMetaQuestionSignal(userMessage); const capabilityMetaQuery = input.shouldHandleAsAssistantCapabilityMetaQuery(userMessage); const destructiveSignal = input.hasDestructiveDataActionSignal(userMessage); const dangerSignal = input.hasDangerOrCoercionSignal(userMessage); const operationalSignal = input.hasOperationalAdminActionRequestSignal(userMessage); let dataScopeProbe: Record | null = null; let chatText = ""; let livingChatSource = "llm_chat"; let livingChatScriptGuardApplied = false; let livingChatScriptGuardReason: string | null = null; let livingChatGroundingGuardApplied = false; let livingChatGroundingGuardReason: string | null = null; let livingChatProactiveScopeOfferApplied = false; const continuityActiveOrganization = organizationAuthority.continuityActiveOrganization; let knownOrganizations = [...organizationAuthority.knownOrganizations]; let selectedOrganization = organizationAuthority.selectedOrganization; let activeOrganization = organizationAuthority.activeOrganization; const memoryRecapContext = resolveAssistantLivingChatMemoryContext({ modeDecisionReason: input.modeDecision?.reason ?? null, sessionItems: input.sessionItems }); const contextualInventoryHistoryCapabilityFollowup = memoryRecapContext.contextualInventoryHistoryCapabilityFollowup; const contextualMemoryRecapFollowup = memoryRecapContext.contextualMemoryRecapFollowup; const lastGroundedInventoryAddressDebug = memoryRecapContext.lastGroundedInventoryAddressDebug; const lastMemoryAddressDebug = memoryRecapContext.lastMemoryAddressDebug; if (capabilityMetaQuery && (destructiveSignal || dangerSignal)) { chatText = input.buildAssistantSafetyRefusalReply(); livingChatSource = "deterministic_safety_refusal"; } else if (dataScopeMetaQuery) { dataScopeProbe = await input.resolveDataScopeProbe(); chatText = input.buildAssistantDataScopeContractReply(dataScopeProbe); knownOrganizations = input.mergeKnownOrganizations([ ...knownOrganizations, ...(Array.isArray(dataScopeProbe?.organizations) ? (dataScopeProbe.organizations as unknown[]) : []) ]); if (!activeOrganization && knownOrganizations.length === 1) { activeOrganization = knownOrganizations[0]; } livingChatSource = dataScopeProbe?.status === "resolved" ? "deterministic_data_scope_contract_live" : "deterministic_data_scope_contract"; } else if ((selectedOrganization || activeOrganization) && input.hasOrganizationFactLookupSignal(userMessage)) { const scopedOrganization = selectedOrganization ?? activeOrganization ?? null; chatText = input.buildAssistantOrganizationFactBoundaryReply(scopedOrganization); activeOrganization = scopedOrganization ?? activeOrganization; livingChatSource = "deterministic_organization_fact_boundary"; } else if ( (selectedOrganization || activeOrganization) && input.hasOrganizationFactFollowupSignal(userMessage, input.sessionItems) ) { const scopedOrganization = selectedOrganization ?? activeOrganization ?? null; chatText = input.buildAssistantOrganizationFactBoundaryReply(scopedOrganization); activeOrganization = scopedOrganization ?? activeOrganization; livingChatSource = "deterministic_organization_fact_boundary_followup"; } else if ( !capabilityMetaQuery && input.shouldEmitOrganizationSelectionReply(userMessage, selectedOrganization ?? activeOrganization) ) { const scopedOrganization = selectedOrganization ?? activeOrganization ?? null; chatText = input.buildAssistantDataScopeSelectionReply(scopedOrganization); activeOrganization = scopedOrganization ?? activeOrganization; livingChatSource = "deterministic_data_scope_selection_contract"; } else if (capabilityMetaQuery && operationalSignal && !input.hasAssistantCapabilityQuestionSignal(userMessage)) { chatText = input.buildAssistantOperationalBoundaryReply(); livingChatSource = "deterministic_operational_boundary"; } else if (contextualInventoryHistoryCapabilityFollowup) { const scopedOrganization = selectedOrganization ?? activeOrganization ?? null; chatText = buildInventoryHistoryCapabilityFollowupReplyFromPolicy({ organization: scopedOrganization, addressDebug: lastGroundedInventoryAddressDebug, toNonEmptyString: input.toNonEmptyString }); activeOrganization = scopedOrganization ?? activeOrganization; livingChatSource = "deterministic_inventory_history_capability_contract"; } else if (contextualMemoryRecapFollowup) { const scopedOrganization = selectedOrganization ?? activeOrganization ?? null; chatText = buildAddressMemoryRecapReplyFromPolicy({ organization: scopedOrganization, addressDebug: lastMemoryAddressDebug, sessionItems: input.sessionItems, toNonEmptyString: input.toNonEmptyString }); activeOrganization = scopedOrganization ?? activeOrganization; livingChatSource = "deterministic_memory_recap_contract"; } else if (capabilityMetaQuery) { chatText = input.buildAssistantCapabilityContractReply(userMessage); livingChatSource = "deterministic_capability_contract"; } else { chatText = await input.executeLlmChat(); const scriptGuard = input.applyScriptGuard(chatText, userMessage); chatText = scriptGuard.text; if (scriptGuard.applied) { livingChatScriptGuardApplied = true; livingChatScriptGuardReason = scriptGuard.reason; livingChatSource = "llm_chat_script_guard"; } const groundingGuard = input.applyGroundingGuard({ userMessage, chatText, organization: activeOrganization ?? selectedOrganization ?? null }); chatText = groundingGuard.text; if (groundingGuard.applied) { livingChatGroundingGuardApplied = true; livingChatGroundingGuardReason = groundingGuard.reason; livingChatSource = "llm_chat_grounding_guard"; } const shouldOfferProactiveOrganizationScope = !selectedOrganization && !activeOrganization && !continuitySnapshot.hasGroundedAddressContext && !hasPriorAssistantTurn(input.sessionItems) && input.modeDecision?.mode === "chat" && input.hasLivingChatSignal(userMessage); if (shouldOfferProactiveOrganizationScope) { const proactiveScopeProbe = await input.resolveDataScopeProbe(); const mergedKnownOrganizations = input.mergeKnownOrganizations([ ...knownOrganizations, ...(Array.isArray(proactiveScopeProbe?.organizations) ? (proactiveScopeProbe.organizations as unknown[]) : []) ]); knownOrganizations = mergedKnownOrganizations; if (!activeOrganization && mergedKnownOrganizations.length === 1) { activeOrganization = mergedKnownOrganizations[0]; } const proactiveOffer = input.buildAssistantProactiveOrganizationOfferReply(proactiveScopeProbe); if (proactiveOffer) { chatText = [buildDeterministicSmalltalkLeadReply(), proactiveOffer] .filter((part) => String(part ?? "").trim().length > 0) .join(" "); livingChatProactiveScopeOfferApplied = true; livingChatSource = "deterministic_smalltalk_with_proactive_scope_offer"; if (!dataScopeProbe) { dataScopeProbe = proactiveScopeProbe; } } } } if (!chatText) { return { handled: false, chatText: "", debug: null }; } const addressRuntimeMeta = (input.addressRuntimeMeta && typeof input.addressRuntimeMeta === "object" ? input.addressRuntimeMeta : {}) as Record; const predecomposeContract = addressRuntimeMeta.predecomposeContract && typeof addressRuntimeMeta.predecomposeContract === "object" ? (addressRuntimeMeta.predecomposeContract as Record) : null; const semanticExtractionContract = addressRuntimeMeta.semanticExtractionContract && typeof addressRuntimeMeta.semanticExtractionContract === "object" ? (addressRuntimeMeta.semanticExtractionContract as Record) : null; const debug: Record = { trace_id: input.traceIdFactory(), prompt_version: "living_chat_router_v1", schema_version: "living_chat_router_v1", fallback_type: "none", detected_mode: "chat", detected_mode_confidence: "high", execution_lane: "living_chat", living_router_mode: input.modeDecision?.mode ?? "chat", living_router_reason: input.modeDecision?.reason ?? "living_chat_signal_detected", living_chat_response_source: livingChatSource, living_chat_script_guard_applied: livingChatScriptGuardApplied, living_chat_script_guard_reason: livingChatScriptGuardReason, living_chat_grounding_guard_applied: livingChatGroundingGuardApplied, living_chat_grounding_guard_reason: livingChatGroundingGuardReason, living_chat_proactive_scope_offer_applied: livingChatProactiveScopeOfferApplied, living_chat_data_scope_probe_status: dataScopeProbe?.status ?? null, living_chat_data_scope_probe_channel: dataScopeProbe?.channel ?? null, living_chat_data_scope_probe_org_count: Array.isArray(dataScopeProbe?.organizations) ? dataScopeProbe.organizations.length : 0, living_chat_data_scope_probe_organizations: Array.isArray(dataScopeProbe?.organizations) ? input.mergeKnownOrganizations(dataScopeProbe.organizations as unknown[]) : [], living_chat_data_scope_probe_error: dataScopeProbe?.error ?? null, living_chat_continuity_grounded_context_detected: continuitySnapshot.hasGroundedAddressContext, living_chat_continuity_active_organization: continuityActiveOrganization, living_chat_selected_organization: selectedOrganization ?? null, assistant_known_organizations: knownOrganizations, assistant_active_organization: activeOrganization ?? null, address_llm_predecompose_attempted: Boolean(addressRuntimeMeta.attempted), address_llm_predecompose_applied: Boolean(addressRuntimeMeta.applied), address_llm_predecompose_reason: addressRuntimeMeta.reason ?? null, address_llm_predecompose_contract: predecomposeContract, address_semantic_extraction_contract: semanticExtractionContract, orchestration_contract_v1: addressRuntimeMeta.orchestrationContract ?? null, tool_gate_decision: addressRuntimeMeta.toolGateDecision ?? null, tool_gate_reason: addressRuntimeMeta.toolGateReason ?? null, normalized: null, normalizer_output: null }; return { handled: true, chatText, debug }; }