АРЧ - Усилить root-frame возврат, memory recap и colloquial provenance follow-up

This commit is contained in:
2026-04-15 20:34:21 +03:00
parent f911f9893b
commit 8056bdfaf2
16 changed files with 2032 additions and 21 deletions
@@ -99,6 +99,51 @@ function findLastGroundedInventoryAddressDebug(items: unknown[]): Record<string,
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;
}
function buildInventoryHistoryCapabilityFollowupReply(input: {
organization: string | null;
addressDebug: Record<string, unknown> | null;
@@ -135,6 +180,55 @@ function buildInventoryHistoryCapabilityFollowupReply(input: {
].join("\n");
}
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 async function runAssistantLivingChatRuntime(
input: AssistantLivingChatRuntimeInput
): Promise<AssistantLivingChatRuntimeOutput> {
@@ -157,9 +251,14 @@ export async function runAssistantLivingChatRuntime(
let activeOrganization = input.toNonEmptyString(input.sessionScope.activeOrganization);
const contextualInventoryHistoryCapabilityFollowup =
input.modeDecision?.reason === "inventory_history_capability_followup_detected";
const contextualMemoryRecapFollowup =
input.modeDecision?.reason === "memory_recap_followup_detected";
const lastGroundedInventoryAddressDebug = contextualInventoryHistoryCapabilityFollowup
? findLastGroundedInventoryAddressDebug(input.sessionItems)
: null;
const lastMemoryAddressDebug = contextualMemoryRecapFollowup
? findLastAddressDebugWithItem(input.sessionItems) ?? findLastAddressDebug(input.sessionItems)
: null;
if (capabilityMetaQuery && (destructiveSignal || dangerSignal)) {
chatText = input.buildAssistantSafetyRefusalReply();
@@ -211,6 +310,15 @@ export async function runAssistantLivingChatRuntime(
});
activeOrganization = scopedOrganization ?? activeOrganization;
livingChatSource = "deterministic_inventory_history_capability_contract";
} else if (contextualMemoryRecapFollowup) {
const scopedOrganization = selectedOrganization ?? activeOrganization ?? null;
chatText = buildAddressMemoryRecapReply({
organization: scopedOrganization,
addressDebug: lastMemoryAddressDebug,
toNonEmptyString: input.toNonEmptyString
});
activeOrganization = scopedOrganization ?? activeOrganization;
livingChatSource = "deterministic_memory_recap_contract";
} else if (capabilityMetaQuery) {
chatText = input.buildAssistantCapabilityContractReply();
livingChatSource = "deterministic_capability_contract";