АРЧ АП11 - Архитектура после регресса: Архитектура: централизовать address debug context для memory recap и living-chat follow-up

This commit is contained in:
2026-04-18 20:30:58 +03:00
parent 4e1830282b
commit d29fbba214
8 changed files with 204 additions and 62 deletions
@@ -5,6 +5,7 @@ exports.readAddressDebugFilters = readAddressDebugFilters;
exports.readAddressDebugItem = readAddressDebugItem;
exports.readAddressDebugOrganization = readAddressDebugOrganization;
exports.readAddressDebugScopedDate = readAddressDebugScopedDate;
exports.resolveAddressDebugContextFacts = resolveAddressDebugContextFacts;
exports.buildInventoryRootFrameFromAddressDebug = buildInventoryRootFrameFromAddressDebug;
exports.isGroundedAddressDebug = isGroundedAddressDebug;
exports.resolveAssistantContinuitySnapshot = resolveAssistantContinuitySnapshot;
@@ -59,6 +60,13 @@ function readAddressDebugScopedDate(debug) {
formatIsoDateForReply(rootFrameContext?.as_of_date) ??
formatIsoDateForReply(extractedFilters?.period_to));
}
function resolveAddressDebugContextFacts(debug, toNonEmptyString = fallbackToNonEmptyString) {
return {
item: readAddressDebugItem(debug, toNonEmptyString),
organization: readAddressDebugOrganization(debug, toNonEmptyString),
scopedDate: readAddressDebugScopedDate(debug)
};
}
function buildInventoryRootFrameFromAddressDebug(debug, toNonEmptyString = fallbackToNonEmptyString) {
if (!debug || typeof debug !== "object") {
return null;
@@ -31,16 +31,9 @@ function hasExplicitRecapPromptSignal(samples) {
return samples.some((sample) => /(?:что\s+мы\s+.*(?:обсуждали|выяснили)|что\s+уже\s+выяснили|что\s+уже\s+поняли|напомни\s+что\s+мы)/iu.test(sample));
}
function buildInventoryHistoryCapabilityFollowupReply(input) {
const rootFrameContext = input.addressDebug?.address_root_frame_context &&
typeof input.addressDebug.address_root_frame_context === "object"
? input.addressDebug.address_root_frame_context
: null;
const extractedFilters = (0, assistantContinuityPolicy_1.readAddressDebugFilters)(input.addressDebug);
const organization = input.organization ??
input.toNonEmptyString(rootFrameContext?.organization) ??
input.toNonEmptyString(extractedFilters?.organization);
const lastAsOfDate = (0, assistantContinuityPolicy_1.formatIsoDateForReply)(rootFrameContext?.as_of_date) ??
(0, assistantContinuityPolicy_1.formatIsoDateForReply)(extractedFilters?.as_of_date);
const contextFacts = (0, assistantContinuityPolicy_1.resolveAddressDebugContextFacts)(input.addressDebug, input.toNonEmptyString);
const organization = input.organization ?? contextFacts.organization;
const lastAsOfDate = contextFacts.scopedDate;
const organizationPart = organization ? ` по компании «${organization}»` : "";
const referenceLine = lastAsOfDate
? `Да, могу. Сейчас мы уже смотрели складской срез${organizationPart} на ${lastAsOfDate}.`
@@ -65,7 +58,7 @@ function normalizeRecapIdentity(value) {
}
function buildRecapFactLine(input) {
const detectedIntent = String(input.debug?.detected_intent ?? "");
const scopedDate = (0, assistantContinuityPolicy_1.readAddressDebugScopedDate)(input.debug);
const scopedDate = (0, assistantContinuityPolicy_1.resolveAddressDebugContextFacts)(input.debug).scopedDate;
const itemPart = input.item ? `по позиции «${input.item}»` : null;
const organizationPart = input.organization ? `по компании «${input.organization}»` : null;
const datePart = scopedDate ? ` на ${scopedDate}` : "";
@@ -115,8 +108,9 @@ function collectRecentRecapFacts(input) {
if (!(0, assistantContinuityPolicy_1.isGroundedAddressDebug)(item.debug, input.toNonEmptyString)) {
continue;
}
const debugItem = (0, assistantContinuityPolicy_1.readAddressDebugItem)(item.debug, input.toNonEmptyString);
const debugOrganization = (0, assistantContinuityPolicy_1.readAddressDebugOrganization)(item.debug, input.toNonEmptyString);
const debugContext = (0, assistantContinuityPolicy_1.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
@@ -144,18 +138,10 @@ function collectRecentRecapFacts(input) {
return facts.reverse();
}
function buildAddressMemoryRecapReply(input) {
const extractedFilters = input.addressDebug?.extracted_filters && typeof input.addressDebug.extracted_filters === "object"
? input.addressDebug.extracted_filters
: null;
const rootFrameContext = input.addressDebug?.address_root_frame_context &&
typeof input.addressDebug.address_root_frame_context === "object"
? input.addressDebug.address_root_frame_context
: null;
const item = (0, assistantContinuityPolicy_1.readAddressDebugItem)(input.addressDebug, input.toNonEmptyString);
const organization = input.organization ??
input.toNonEmptyString(extractedFilters?.organization) ??
input.toNonEmptyString(rootFrameContext?.organization);
const scopedDate = (0, assistantContinuityPolicy_1.readAddressDebugScopedDate)(input.addressDebug);
const contextFacts = (0, assistantContinuityPolicy_1.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,
@@ -27,6 +27,12 @@ export interface AssistantContinuitySnapshot {
hasGroundedInventoryContext: boolean;
}
export interface AssistantAddressDebugContextFacts {
item: string | null;
organization: string | null;
scopedDate: string | null;
}
export interface AssistantOrganizationAuthorityInput {
sessionItems?: unknown[];
sessionKnownOrganizations?: unknown[];
@@ -118,6 +124,17 @@ export function readAddressDebugScopedDate(debug: Record<string, unknown> | null
);
}
export function resolveAddressDebugContextFacts(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): AssistantAddressDebugContextFacts {
return {
item: readAddressDebugItem(debug, toNonEmptyString),
organization: readAddressDebugOrganization(debug, toNonEmptyString),
scopedDate: readAddressDebugScopedDate(debug)
};
}
export function buildInventoryRootFrameFromAddressDebug(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
@@ -1,12 +1,8 @@
// @ts-nocheck
import {
formatIsoDateForReply,
isGroundedAddressDebug,
readAddressDebugFilters,
readAddressDebugItem,
readAddressDebugOrganization,
readAddressDebugScopedDate,
resolveAddressDebugContextFacts,
resolveAssistantContinuitySnapshot
} from "./assistantContinuityPolicy";
@@ -92,19 +88,9 @@ export function buildInventoryHistoryCapabilityFollowupReply(input: {
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 = readAddressDebugFilters(input.addressDebug);
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 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}.`
@@ -135,7 +121,7 @@ function buildRecapFactLine(input: {
organization: string | null;
}): string | null {
const detectedIntent = String(input.debug?.detected_intent ?? "");
const scopedDate = readAddressDebugScopedDate(input.debug);
const scopedDate = resolveAddressDebugContextFacts(input.debug).scopedDate;
const itemPart = input.item ? `по позиции «${input.item}»` : null;
const organizationPart = input.organization ? `по компании «${input.organization}»` : null;
const datePart = scopedDate ? ` на ${scopedDate}` : "";
@@ -192,8 +178,9 @@ function collectRecentRecapFacts(input: {
if (!isGroundedAddressDebug(item.debug, input.toNonEmptyString)) {
continue;
}
const debugItem = readAddressDebugItem(item.debug, input.toNonEmptyString);
const debugOrganization = readAddressDebugOrganization(item.debug, input.toNonEmptyString);
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
@@ -228,21 +215,10 @@ export function buildAddressMemoryRecapReply(input: {
sessionItems?: unknown[];
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 = readAddressDebugItem(input.addressDebug, input.toNonEmptyString);
const organization =
input.organization ??
input.toNonEmptyString(extractedFilters?.organization) ??
input.toNonEmptyString(rootFrameContext?.organization);
const scopedDate = readAddressDebugScopedDate(input.addressDebug);
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,
@@ -1,5 +1,8 @@
import { describe, expect, it } from "vitest";
import { resolveAssistantOrganizationAuthority } from "../src/services/assistantContinuityPolicy";
import {
resolveAddressDebugContextFacts,
resolveAssistantOrganizationAuthority
} from "../src/services/assistantContinuityPolicy";
describe("assistantContinuityPolicy organization authority", () => {
it("prefers explicit assistant organization authority over older grounded continuity and merges known organizations once", () => {
@@ -55,4 +58,21 @@ describe("assistantContinuityPolicy organization authority", () => {
]);
expect(authority.organizationClarificationSelectionFromScope).toBe("Org Selected");
});
it("reads item, organization and scoped date from root-frame fallback when direct filters are missing", () => {
const facts = resolveAddressDebugContextFacts({
anchor_type: "item",
anchor_value_resolved: "Рабочая станция",
address_root_frame_context: {
organization: 'ООО "Альтернатива Плюс"',
as_of_date: "2020-03-31"
}
});
expect(facts).toEqual({
item: "Рабочая станция",
organization: 'ООО "Альтернатива Плюс"',
scopedDate: "31.03.2020"
});
});
});