// @ts-nocheck import { isGroundedAddressDebug, resolveAddressDebugContextFacts, resolveAssistantContinuitySnapshot } from "./assistantContinuityPolicy"; export interface ResolveAssistantRouteMemorySignalsInput { rawUserMessage?: unknown; repairedRawUserMessage?: unknown; effectiveAddressUserMessage?: unknown; repairedEffectiveAddressUserMessage?: unknown; dataScopeMetaQuery?: boolean; capabilityMetaQuery?: boolean; dataRetrievalSignal?: boolean; strongDataSignal?: boolean; aggregateBusinessAnalyticsSignal?: boolean; lastGroundedAddressDebug?: unknown; hasPriorAddressDebug?: boolean; sessionItems?: unknown[]; } export interface AssistantRouteMemorySignals { contextualHistoricalCapabilityFollowupDetected: boolean; contextualMemoryRecapFollowupDetected: boolean; } export interface ResolveAssistantLivingChatMemoryContextInput { modeDecisionReason?: unknown; sessionItems?: unknown[]; } export interface AssistantLivingChatMemoryContext { contextualInventoryHistoryCapabilityFollowup: boolean; contextualMemoryRecapFollowup: boolean; contextualAnswerInspectionFollowup: boolean; lastGroundedInventoryAddressDebug: Record | null; lastMemoryAddressDebug: Record | null; lastAnswerInspectionAddressDebug: Record | null; } export interface AssistantMemoryRecapPolicyDeps { hasHistoricalCapabilityFollowupSignal: (text: unknown) => boolean; hasConversationMemoryRecallFollowupSignal: (text: unknown) => boolean; isGroundedInventoryContextDebug: (debug: unknown) => boolean; } function toNonEmptyString(value: unknown): string | null { if (value === null || value === undefined) { return null; } const text = String(value).trim(); return text.length > 0 ? text : null; } function collectMessageSamples(input: ResolveAssistantRouteMemorySignalsInput): string[] { const values = [ input.rawUserMessage, input.repairedRawUserMessage, input.effectiveAddressUserMessage, input.repairedEffectiveAddressUserMessage ]; return Array.from( new Set( values .map((item) => String(item ?? "").trim()) .filter((item) => item.length > 0) ) ); } function hasSignalAcrossSamples( samples: string[], detector: (text: unknown) => boolean ): boolean { return samples.some((sample) => detector(sample)); } function hasExplicitRecapPromptSignal(samples: string[]): boolean { return samples.some((sample) => /(?:что\s+мы\s+.*(?:обсуждали|выяснили)|что\s+уже\s+выяснили|что\s+уже\s+поняли|напомни\s+что\s+мы)/iu.test( sample ) ); } export function buildInventoryHistoryCapabilityFollowupReply(input: { organization: string | null; addressDebug: Record | null; toNonEmptyString: (value: unknown) => string | null; }): string { const contextFacts = resolveAddressDebugContextFacts(input.addressDebug, input.toNonEmptyString); const organization = input.organization ?? contextFacts.organization; const lastAsOfDate = contextFacts.scopedDate; const organizationPart = organization ? ` по компании «${organization}»` : ""; const referenceLine = lastAsOfDate ? `Да, могу. Сейчас мы уже смотрели складской срез${organizationPart} на ${lastAsOfDate}.` : `Да, могу показать исторические данные${organizationPart} в этом же складском контуре.`; return [ referenceLine, `Могу показать исторические остатки${organizationPart} за нужный месяц, дату или год.`, "Например:", "- `на март 2020`", "- `на июнь 2016`", "- `за 2017 год`", "- `сравни июнь 2016 с текущим срезом`", "Если хочешь, сразу покажу нужный исторический период." ].join("\n"); } function normalizeRecapIdentity(value: unknown): string { return String(value ?? "") .trim() .toLowerCase() .replace(/[«»"'`]/g, "") .replace(/\s+/g, " "); } function buildRecapFactLine(input: { debug: Record | null; item: string | null; organization: string | null; }): string | null { const detectedIntent = String(input.debug?.detected_intent ?? ""); const scopedDate = resolveAddressDebugContextFacts(input.debug).scopedDate; const itemPart = input.item ? `по позиции «${input.item}»` : null; const organizationPart = input.organization ? `по компании «${input.organization}»` : null; const datePart = scopedDate ? ` на ${scopedDate}` : ""; if (detectedIntent === "inventory_on_hand_as_of_date") { return `смотрели остатки${organizationPart ? ` ${organizationPart}` : ""}${datePart}`.trim(); } if (detectedIntent === "inventory_purchase_provenance_for_item" && itemPart) { return `разобрали, кто поставлял ${itemPart}${datePart}`.trim(); } if (detectedIntent === "inventory_purchase_documents_for_item" && itemPart) { return `подняли документы закупки ${itemPart}${datePart}`.trim(); } if (detectedIntent === "inventory_sale_trace_for_item" && itemPart) { return `разобрали, кому продавали ${itemPart}${datePart}`.trim(); } if (detectedIntent === "inventory_purchase_to_sale_chain" && itemPart) { return `проследили цепочку от закупки до продажи ${itemPart}${datePart}`.trim(); } if (detectedIntent === "inventory_profitability_for_item" && itemPart) { return `смотрели рентабельность ${itemPart}${datePart}`.trim(); } if (detectedIntent === "inventory_aging_by_purchase_date" && itemPart) { return `смотрели возраст остатков ${itemPart}${datePart}`.trim(); } if (detectedIntent === "counterparty_activity_lifecycle" && organizationPart) { return `смотрели активность в базе 1С ${organizationPart}`.trim(); } if (detectedIntent === "list_documents_by_counterparty" && organizationPart) { return `поднимали документы ${organizationPart}${datePart}`.trim(); } return null; } function collectRecentRecapFacts(input: { sessionItems?: unknown[]; item: string | null; organization: string | null; toNonEmptyString: (value: unknown) => string | null; }): string[] { const sessionItems = Array.isArray(input.sessionItems) ? input.sessionItems : []; if (sessionItems.length === 0) { return []; } const currentItemKey = normalizeRecapIdentity(input.item); const currentOrganizationKey = normalizeRecapIdentity(input.organization); const facts: string[] = []; const seen = new Set(); for (let index = sessionItems.length - 1; index >= 0; index -= 1) { const item = sessionItems[index] as { role?: string; debug?: Record } | null; if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") { continue; } if (!isGroundedAddressDebug(item.debug, input.toNonEmptyString)) { continue; } const debugContext = resolveAddressDebugContextFacts(item.debug, input.toNonEmptyString); const debugItem = debugContext.item; const debugOrganization = debugContext.organization; const itemMatches = currentItemKey ? normalizeRecapIdentity(debugItem) === currentItemKey : false; const organizationMatches = currentOrganizationKey ? normalizeRecapIdentity(debugOrganization) === currentOrganizationKey : false; if (currentItemKey && !itemMatches) { continue; } if (!currentItemKey && currentOrganizationKey && !organizationMatches) { continue; } const fact = buildRecapFactLine({ debug: item.debug, item: debugItem, organization: debugOrganization }); if (!fact || seen.has(fact)) { continue; } seen.add(fact); facts.push(fact); if (facts.length >= 3) { break; } } return facts.reverse(); } export function buildAddressMemoryRecapReply(input: { organization: string | null; addressDebug: Record | null; sessionItems?: unknown[]; toNonEmptyString: (value: unknown) => string | null; }): string { const contextFacts = resolveAddressDebugContextFacts(input.addressDebug, input.toNonEmptyString); const item = contextFacts.item; const organization = input.organization ?? contextFacts.organization; const scopedDate = contextFacts.scopedDate; const recapFacts = collectRecentRecapFacts({ sessionItems: input.sessionItems, item, organization, toNonEmptyString: input.toNonEmptyString }); if (item) { if (recapFacts.length > 0) { const datePart = scopedDate ? ` в срезе на ${scopedDate}` : ""; const organizationPart = organization ? ` по компании «${organization}»` : ""; return [ `Да, помню. По позиции «${item}»${organizationPart}${datePart} мы уже выяснили:`, ...recapFacts.map((fact) => `- ${fact}.`), "Могу сразу продолжить по ней: поставщик, закупка, документы или продажа." ].join("\n"); } 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 function buildSelectedObjectAnswerInspectionReply(input: { addressDebug: Record | null; toNonEmptyString: (value: unknown) => string | null; }): string { const contextFacts = resolveAddressDebugContextFacts(input.addressDebug, input.toNonEmptyString); const itemLabel = contextFacts.item ?? "эта позиция"; const detectedIntent = String(input.addressDebug?.detected_intent ?? ""); if (detectedIntent === "inventory_sale_trace_for_item") { return [ `Да, если так прозвучало, это ошибка чтения ответа. «${itemLabel}» здесь не контрагент, а сама позиция, по которой мы смотрели продажу.`, "В предыдущем ответе я показывал документы выбытия по этой позиции. Покупатель в доступных данных отдельно не выделен, поэтому назвать контрагента-покупателя я там не мог.", "Если хочешь, следующим шагом могу отдельно проверить, можно ли вытащить покупателя по связанным документам реализации." ].join(" "); } if ( detectedIntent === "inventory_purchase_provenance_for_item" || detectedIntent === "inventory_purchase_documents_for_item" ) { return [ `Да, если так прозвучало, это ошибка чтения ответа. «${itemLabel}» здесь не контрагент, а сама позиция или номенклатура.`, "В предыдущем ответе речь шла о закупке этой позиции: я перечислял поставщиков или закупочные документы по ней, а не называл саму позицию контрагентом." ].join(" "); } return [ `Да, если так прозвучало, это ошибка чтения ответа. «${itemLabel}» здесь не контрагент, а выбранный объект разбора.`, "Я сейчас уточняю именно смысл предыдущего grounded-ответа по этой позиции, а не запускаю новый адресный поиск." ].join(" "); } export function resolveAssistantLivingChatMemoryContext( input: ResolveAssistantLivingChatMemoryContextInput ): AssistantLivingChatMemoryContext { const contextualInventoryHistoryCapabilityFollowup = String(input.modeDecisionReason ?? "") === "inventory_history_capability_followup_detected"; const contextualMemoryRecapFollowup = String(input.modeDecisionReason ?? "") === "memory_recap_followup_detected"; const contextualAnswerInspectionFollowup = String(input.modeDecisionReason ?? "") === "answer_inspection_followup_detected"; const continuity = resolveAssistantContinuitySnapshot({ sessionItems: input.sessionItems, toNonEmptyString }); return { contextualInventoryHistoryCapabilityFollowup, contextualMemoryRecapFollowup, contextualAnswerInspectionFollowup, lastGroundedInventoryAddressDebug: contextualInventoryHistoryCapabilityFollowup ? continuity.lastGroundedInventoryAddressDebug : null, lastMemoryAddressDebug: contextualMemoryRecapFollowup ? continuity.lastGroundedItemAddressDebug ?? continuity.lastGroundedAddressDebug : null, lastAnswerInspectionAddressDebug: contextualAnswerInspectionFollowup ? continuity.lastGroundedItemAddressDebug ?? continuity.lastGroundedAddressDebug : null }; } export function createAssistantMemoryRecapPolicy( deps: AssistantMemoryRecapPolicyDeps ) { function resolveRouteMemorySignals( input: ResolveAssistantRouteMemorySignalsInput ): AssistantRouteMemorySignals { const samples = collectMessageSamples(input); const continuity = resolveAssistantContinuitySnapshot({ sessionItems: input.sessionItems, toNonEmptyString }); const groundedInventoryContext = continuity.lastGroundedInventoryAddressDebug ?? input.lastGroundedAddressDebug; const historicalCapabilitySignal = hasSignalAcrossSamples( samples, deps.hasHistoricalCapabilityFollowupSignal ); const memoryRecapSignal = hasSignalAcrossSamples( samples, deps.hasConversationMemoryRecallFollowupSignal ); const explicitRecapPromptSignal = hasExplicitRecapPromptSignal(samples); return { contextualHistoricalCapabilityFollowupDetected: Boolean( input.capabilityMetaQuery && !input.dataScopeMetaQuery && !input.dataRetrievalSignal && historicalCapabilitySignal && deps.isGroundedInventoryContextDebug(groundedInventoryContext) ), contextualMemoryRecapFollowupDetected: Boolean( !input.dataScopeMetaQuery && !input.capabilityMetaQuery && !input.aggregateBusinessAnalyticsSignal && memoryRecapSignal && (explicitRecapPromptSignal || (!input.dataRetrievalSignal && !input.strongDataSignal)) && continuity.hasGroundedAddressContext ) }; } return { resolveRouteMemorySignals }; }