304 lines
12 KiB
TypeScript
304 lines
12 KiB
TypeScript
// @ts-nocheck
|
|
|
|
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;
|
|
}
|
|
|
|
export interface AssistantRouteMemorySignals {
|
|
contextualHistoricalCapabilityFollowupDetected: boolean;
|
|
contextualMemoryRecapFollowupDetected: boolean;
|
|
}
|
|
|
|
export interface ResolveAssistantLivingChatMemoryContextInput {
|
|
modeDecisionReason?: unknown;
|
|
sessionItems?: unknown[];
|
|
}
|
|
|
|
export interface AssistantLivingChatMemoryContext {
|
|
contextualInventoryHistoryCapabilityFollowup: boolean;
|
|
contextualMemoryRecapFollowup: boolean;
|
|
lastGroundedInventoryAddressDebug: Record<string, unknown> | null;
|
|
lastMemoryAddressDebug: Record<string, unknown> | null;
|
|
}
|
|
|
|
export interface AssistantMemoryRecapPolicyDeps {
|
|
hasHistoricalCapabilityFollowupSignal: (text: unknown) => boolean;
|
|
hasConversationMemoryRecallFollowupSignal: (text: unknown) => boolean;
|
|
isGroundedInventoryContextDebug: (debug: unknown) => boolean;
|
|
}
|
|
|
|
function formatIsoDateForReply(value: unknown): string | null {
|
|
const source = String(value ?? "").trim();
|
|
const match = source.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
if (!match) {
|
|
return null;
|
|
}
|
|
return `${match[3]}.${match[2]}.${match[1]}`;
|
|
}
|
|
|
|
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
|
|
)
|
|
);
|
|
}
|
|
|
|
function findLastGroundedInventoryAddressDebug(items: unknown[]): Record<string, unknown> | null {
|
|
if (!Array.isArray(items)) {
|
|
return null;
|
|
}
|
|
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
const item = items[index] as { role?: string; debug?: Record<string, unknown> } | null;
|
|
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
|
|
continue;
|
|
}
|
|
const debug = item.debug;
|
|
const answerGroundingCheck =
|
|
debug.answer_grounding_check && typeof debug.answer_grounding_check === "object"
|
|
? (debug.answer_grounding_check as Record<string, unknown>)
|
|
: null;
|
|
const groundingStatus = String(answerGroundingCheck?.status ?? "");
|
|
const detectedIntent = String(debug.detected_intent ?? "");
|
|
const capabilityId = String(debug.capability_id ?? "");
|
|
const rootFrameContext =
|
|
debug.address_root_frame_context && typeof debug.address_root_frame_context === "object"
|
|
? (debug.address_root_frame_context as Record<string, unknown>)
|
|
: null;
|
|
const rootIntent = String(rootFrameContext?.root_intent ?? "");
|
|
const isInventoryContext =
|
|
detectedIntent === "inventory_on_hand_as_of_date" ||
|
|
capabilityId === "confirmed_inventory_on_hand_as_of_date" ||
|
|
rootIntent === "inventory_on_hand_as_of_date";
|
|
if (groundingStatus === "grounded" && isInventoryContext) {
|
|
return debug;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function findLastAddressDebugWithItem(items: unknown[]): Record<string, unknown> | null {
|
|
if (!Array.isArray(items)) {
|
|
return null;
|
|
}
|
|
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
const item = items[index] as { role?: string; debug?: Record<string, unknown> } | null;
|
|
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
|
|
continue;
|
|
}
|
|
const debug = item.debug;
|
|
if (String(debug.execution_lane ?? "") !== "address_query") {
|
|
continue;
|
|
}
|
|
const extractedFilters =
|
|
debug.extracted_filters && typeof debug.extracted_filters === "object"
|
|
? (debug.extracted_filters as Record<string, unknown>)
|
|
: null;
|
|
const itemLabel =
|
|
String(extractedFilters?.item ?? "").trim() ||
|
|
(String(debug.anchor_type ?? "") === "item"
|
|
? String(debug.anchor_value_resolved ?? debug.anchor_value_raw ?? "").trim()
|
|
: "");
|
|
if (itemLabel) {
|
|
return debug;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function findLastAddressDebug(items: unknown[]): Record<string, unknown> | null {
|
|
if (!Array.isArray(items)) {
|
|
return null;
|
|
}
|
|
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
const item = items[index] as { role?: string; debug?: Record<string, unknown> } | null;
|
|
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
|
|
continue;
|
|
}
|
|
if (String(item.debug.execution_lane ?? "") === "address_query") {
|
|
return item.debug;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function buildInventoryHistoryCapabilityFollowupReply(input: {
|
|
organization: string | null;
|
|
addressDebug: Record<string, unknown> | 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<string, unknown>)
|
|
: null;
|
|
const extractedFilters =
|
|
input.addressDebug?.extracted_filters && typeof input.addressDebug.extracted_filters === "object"
|
|
? (input.addressDebug.extracted_filters as Record<string, unknown>)
|
|
: 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");
|
|
}
|
|
|
|
export function buildAddressMemoryRecapReply(input: {
|
|
organization: string | null;
|
|
addressDebug: Record<string, unknown> | 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<string, unknown>)
|
|
: 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<string, unknown>)
|
|
: 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 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 sessionItems = Array.isArray(input.sessionItems) ? input.sessionItems : [];
|
|
return {
|
|
contextualInventoryHistoryCapabilityFollowup,
|
|
contextualMemoryRecapFollowup,
|
|
lastGroundedInventoryAddressDebug: contextualInventoryHistoryCapabilityFollowup
|
|
? findLastGroundedInventoryAddressDebug(sessionItems)
|
|
: null,
|
|
lastMemoryAddressDebug: contextualMemoryRecapFollowup
|
|
? findLastAddressDebugWithItem(sessionItems) ?? findLastAddressDebug(sessionItems)
|
|
: null
|
|
};
|
|
}
|
|
|
|
export function createAssistantMemoryRecapPolicy(
|
|
deps: AssistantMemoryRecapPolicyDeps
|
|
) {
|
|
function resolveRouteMemorySignals(
|
|
input: ResolveAssistantRouteMemorySignalsInput
|
|
): AssistantRouteMemorySignals {
|
|
const samples = collectMessageSamples(input);
|
|
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(input.lastGroundedAddressDebug)
|
|
),
|
|
contextualMemoryRecapFollowupDetected: Boolean(
|
|
!input.dataScopeMetaQuery &&
|
|
!input.capabilityMetaQuery &&
|
|
!input.aggregateBusinessAnalyticsSignal &&
|
|
memoryRecapSignal &&
|
|
(explicitRecapPromptSignal || (!input.dataRetrievalSignal && !input.strongDataSignal)) &&
|
|
input.hasPriorAddressDebug
|
|
)
|
|
};
|
|
}
|
|
|
|
return {
|
|
resolveRouteMemorySignals
|
|
};
|
|
}
|