Архитектура: протянуть shared root-frame authority в assistantService follow-up glue

This commit is contained in:
2026-04-19 09:01:28 +03:00
parent 62f9bad750
commit a71e1352be
4 changed files with 126 additions and 40 deletions
+7 -20
View File
@@ -2454,15 +2454,7 @@ function extractAddressCarryoverAnchor(addressDebug) {
anchorValue: null
};
}
return {
anchorType: toNonEmptyString(addressDebug.anchor_type),
anchorValue: toNonEmptyString(addressDebug.anchor_value_resolved) ??
toNonEmptyString(addressDebug.anchor_value_raw) ??
readAddressInventoryItemFilter(addressDebug) ??
readAddressFilterString(addressDebug, "counterparty") ??
readAddressFilterString(addressDebug, "contract") ??
readAddressFilterString(addressDebug, "account")
};
return (0, assistantContinuityPolicy_1.resolveAddressDebugAnchorContext)(addressDebug, toNonEmptyString);
}
function findRecentInventoryRootFrame(items) {
for (let index = items.length - 1; index >= 0; index -= 1) {
@@ -2474,20 +2466,15 @@ function findRecentInventoryRootFrame(items) {
if (!isAddressLaneDebugPayload(debug)) {
continue;
}
const detectedIntent = toNonEmptyString(debug.detected_intent);
if (!isInventoryRootFrameIntent(detectedIntent)) {
const rootFrame = (0, assistantContinuityPolicy_1.buildInventoryRootFrameFromAddressDebug)(debug, toNonEmptyString);
if (!rootFrame) {
continue;
}
const anchor = extractAddressCarryoverAnchor(debug);
const filtersRaw = debug.extracted_filters;
const filters = filtersRaw && typeof filtersRaw === "object"
? { ...filtersRaw }
: {};
return {
intent: detectedIntent,
filters,
anchorType: anchor.anchorType,
anchorValue: anchor.anchorValue,
intent: rootFrame.intent,
filters: { ...(rootFrame.filters ?? {}) },
anchorType: rootFrame.anchorType,
anchorValue: rootFrame.anchorValue,
messageId: toNonEmptyString(item.message_id)
};
}
@@ -2409,15 +2409,7 @@ function extractAddressCarryoverAnchor(addressDebug) {
anchorValue: null
};
}
return {
anchorType: toNonEmptyString(addressDebug.anchor_type),
anchorValue: toNonEmptyString(addressDebug.anchor_value_resolved) ??
toNonEmptyString(addressDebug.anchor_value_raw) ??
readAddressInventoryItemFilter(addressDebug) ??
readAddressFilterString(addressDebug, "counterparty") ??
readAddressFilterString(addressDebug, "contract") ??
readAddressFilterString(addressDebug, "account")
};
return (0, assistantContinuityPolicy_1.resolveAddressDebugAnchorContext)(addressDebug, toNonEmptyString);
}
function findRecentInventoryRootFrame(items) {
for (let index = items.length - 1; index >= 0; index -= 1) {
@@ -2429,20 +2421,15 @@ function findRecentInventoryRootFrame(items) {
if (!isAddressLaneDebugPayload(debug)) {
continue;
}
const detectedIntent = toNonEmptyString(debug.detected_intent);
if (!isInventoryRootFrameIntent(detectedIntent)) {
const rootFrame = (0, assistantContinuityPolicy_1.buildInventoryRootFrameFromAddressDebug)(debug, toNonEmptyString);
if (!rootFrame) {
continue;
}
const anchor = extractAddressCarryoverAnchor(debug);
const filtersRaw = debug.extracted_filters;
const filters = filtersRaw && typeof filtersRaw === "object"
? { ...filtersRaw }
: {};
return {
intent: detectedIntent,
filters,
anchorType: anchor.anchorType,
anchorValue: anchor.anchorValue,
intent: rootFrame.intent,
filters: { ...(rootFrame.filters ?? {}) },
anchorType: rootFrame.anchorType,
anchorValue: rootFrame.anchorValue,
messageId: toNonEmptyString(item.message_id)
};
}
@@ -2492,6 +2492,109 @@ describe("assistant address follow-up carryover", () => {
expect(normalizerService.normalize).not.toHaveBeenCalled();
});
it("restores inventory root filters from address_root_frame_context instead of drilldown extracted filters", async () => {
const calls: Array<{ message: string; options?: any }> = [];
const followupMessage = "остатки на эту же дату";
const itemLabel = "Рабочая станция универсального специалиста";
const organization = 'ООО "Альтернатива Плюс"';
const addressQueryService = {
tryHandle: vi.fn(async (message: string, options?: any) => {
calls.push({ message, options });
if (message === followupMessage && options?.followupContext) {
return buildAddressLaneResult({
reply_text: "Собран складской срез на дату из root frame.",
debug: {
...buildAddressLaneResult().debug,
detected_intent: "inventory_on_hand_as_of_date",
extracted_filters: {
organization,
as_of_date: "2021-03-31",
period_from: "2021-03-01",
period_to: "2021-03-31"
},
reasons: ["address_action_detected", "address_entity_detected", "address_followup_context_applied"]
}
});
}
return null;
})
} as any;
const normalizerService = {
normalize: vi.fn(async () => ({
assistant_reply: "normalizer_fallback_should_not_be_used",
reply_type: "partial_coverage",
debug: {}
}))
} as any;
const sessions = new AssistantSessionStore();
const service = new AssistantService(
normalizerService,
sessions as any,
{} as any,
{ persistSession: vi.fn() } as any,
addressQueryService
);
const sessionId = `asst-address-root-frame-authority-${Date.now()}`;
sessions.appendItem(sessionId, {
message_id: "msg-root-frame-authority-seed",
session_id: sessionId,
role: "assistant",
text: "inventory purchase documents seed",
reply_type: "factual",
created_at: "2026-04-19T08:55:00.000Z",
trace_id: "address-root-frame-authority-seed",
debug: {
detected_mode: "address_query",
detected_intent: "inventory_purchase_documents_for_item",
extracted_filters: {
item: itemLabel,
organization,
as_of_date: "2021-04-15"
},
selected_recipe: "address_inventory_purchase_documents_for_item_v1",
anchor_type: "item",
anchor_value_raw: itemLabel,
anchor_value_resolved: itemLabel,
address_root_frame_context: {
root_intent: "inventory_on_hand_as_of_date",
root_filters: {
organization,
warehouse: "Основной склад",
as_of_date: "2021-03-31",
period_from: "2021-03-01",
period_to: "2021-03-31"
},
root_anchor_type: "organization",
root_anchor_value: organization,
current_frame_kind: "inventory_drilldown"
}
}
} as any);
const second = await service.handleMessage({
session_id: sessionId,
user_message: followupMessage,
useMock: true
} as any);
expect(second.ok).toBe(true);
expect(second.reply_type).toBe("factual");
expect(calls).toHaveLength(1);
expect(calls[0].options?.followupContext?.root_context_only).toBe(true);
expect(calls[0].options?.followupContext?.root_intent).toBe("inventory_on_hand_as_of_date");
expect(calls[0].options?.followupContext?.root_filters?.organization).toBe(organization);
expect(calls[0].options?.followupContext?.root_filters?.warehouse).toBe("Основной склад");
expect(calls[0].options?.followupContext?.root_filters?.as_of_date).toBe("2021-03-31");
expect(calls[0].options?.followupContext?.root_filters?.period_from).toBe("2021-03-01");
expect(calls[0].options?.followupContext?.root_filters?.period_to).toBe("2021-03-31");
expect(calls[0].options?.followupContext?.root_filters?.as_of_date).not.toBe("2021-04-15");
expect(normalizerService.normalize).not.toHaveBeenCalled();
});
it("treats colloquial supplier follow-up from an inventory root slice as continuation of the focused item", async () => {
const calls: Array<{ message: string; options?: any }> = [];
const followupMessage = "у кого мы модуль прямоугольный купили кстати";