Архитектура: протянуть shared root-frame authority в assistantService follow-up glue
This commit is contained in:
parent
62f9bad750
commit
a71e1352be
|
|
@ -406,6 +406,15 @@ Still open after the accepted phase12 replay:
|
||||||
- the adapter now imports and uses only the shared builders from `assistantMemoryRecapPolicy`, which makes the live chat branch structurally closer to a single owner for grounded contextual replies;
|
- the adapter now imports and uses only the shared builders from `assistantMemoryRecapPolicy`, which makes the live chat branch structurally closer to a single owner for grounded contextual replies;
|
||||||
- targeted `assistantLivingChatRuntimeAdapter` and `assistantMemoryRecapPolicy` tests stay green after the cleanup, and backend build remains green;
|
- targeted `assistantLivingChatRuntimeAdapter` and `assistantMemoryRecapPolicy` tests stay green after the cleanup, and backend build remains green;
|
||||||
- live reruns on `phase14` and `phase15` on `2026-04-19` surfaced partial top-level status only because the packs still pin `inventory today` expectations to `2026-04-18`; the repaired contextual reply contours themselves stayed semantically clean, which confirms this pass as owner reduction rather than a new runtime regression.
|
- live reruns on `phase14` and `phase15` on `2026-04-19` surfaced partial top-level status only because the packs still pin `inventory today` expectations to `2026-04-18`; the repaired contextual reply contours themselves stayed semantically clean, which confirms this pass as owner reduction rather than a new runtime regression.
|
||||||
|
- the next continuity-authority pass now removes one more duplicate root-frame owner from `assistantService` follow-up glue:
|
||||||
|
- `assistantService.extractAddressCarryoverAnchor(...)` no longer reconstructs anchor resolution from raw `anchor_value_* / extracted_filters` using its own local precedence order;
|
||||||
|
- `assistantService.findRecentInventoryRootFrame(...)` no longer rebuilds inventory root carryover from `detected_intent + extracted_filters` as a separate local parser;
|
||||||
|
- both seams now consume the shared continuity helpers:
|
||||||
|
- `resolveAddressDebugAnchorContext(...)`
|
||||||
|
- `buildInventoryRootFrameFromAddressDebug(...)`
|
||||||
|
- this matters because follow-up carryover in the top-level service now reads the same root-frame authority that already owns `root_filters / root_anchor / current_frame_kind`, instead of keeping a service-local fallback that could silently prefer drilldown `extracted_filters` over the real `address_root_frame_context`;
|
||||||
|
- targeted `assistantAddressFollowupContext` and `addressInventoryRootFrameRegression` suites are green after the move, including a new regression that explicitly proves `root_filters` come from `address_root_frame_context.root_filters` rather than from stale drilldown `extracted_filters`;
|
||||||
|
- this pass strengthens continuity convergence in the top-level orchestration glue without introducing a new case-specific branch.
|
||||||
|
|
||||||
## Next Execution Slice (2026-04-18)
|
## Next Execution Slice (2026-04-18)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2454,15 +2454,7 @@ function extractAddressCarryoverAnchor(addressDebug) {
|
||||||
anchorValue: null
|
anchorValue: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return (0, assistantContinuityPolicy_1.resolveAddressDebugAnchorContext)(addressDebug, toNonEmptyString);
|
||||||
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")
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
function findRecentInventoryRootFrame(items) {
|
function findRecentInventoryRootFrame(items) {
|
||||||
for (let index = items.length - 1; index >= 0; index -= 1) {
|
for (let index = items.length - 1; index >= 0; index -= 1) {
|
||||||
|
|
@ -2474,20 +2466,15 @@ function findRecentInventoryRootFrame(items) {
|
||||||
if (!isAddressLaneDebugPayload(debug)) {
|
if (!isAddressLaneDebugPayload(debug)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const detectedIntent = toNonEmptyString(debug.detected_intent);
|
const rootFrame = (0, assistantContinuityPolicy_1.buildInventoryRootFrameFromAddressDebug)(debug, toNonEmptyString);
|
||||||
if (!isInventoryRootFrameIntent(detectedIntent)) {
|
if (!rootFrame) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const anchor = extractAddressCarryoverAnchor(debug);
|
|
||||||
const filtersRaw = debug.extracted_filters;
|
|
||||||
const filters = filtersRaw && typeof filtersRaw === "object"
|
|
||||||
? { ...filtersRaw }
|
|
||||||
: {};
|
|
||||||
return {
|
return {
|
||||||
intent: detectedIntent,
|
intent: rootFrame.intent,
|
||||||
filters,
|
filters: { ...(rootFrame.filters ?? {}) },
|
||||||
anchorType: anchor.anchorType,
|
anchorType: rootFrame.anchorType,
|
||||||
anchorValue: anchor.anchorValue,
|
anchorValue: rootFrame.anchorValue,
|
||||||
messageId: toNonEmptyString(item.message_id)
|
messageId: toNonEmptyString(item.message_id)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2409,15 +2409,7 @@ function extractAddressCarryoverAnchor(addressDebug) {
|
||||||
anchorValue: null
|
anchorValue: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return (0, assistantContinuityPolicy_1.resolveAddressDebugAnchorContext)(addressDebug, toNonEmptyString);
|
||||||
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")
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
function findRecentInventoryRootFrame(items) {
|
function findRecentInventoryRootFrame(items) {
|
||||||
for (let index = items.length - 1; index >= 0; index -= 1) {
|
for (let index = items.length - 1; index >= 0; index -= 1) {
|
||||||
|
|
@ -2429,20 +2421,15 @@ function findRecentInventoryRootFrame(items) {
|
||||||
if (!isAddressLaneDebugPayload(debug)) {
|
if (!isAddressLaneDebugPayload(debug)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const detectedIntent = toNonEmptyString(debug.detected_intent);
|
const rootFrame = (0, assistantContinuityPolicy_1.buildInventoryRootFrameFromAddressDebug)(debug, toNonEmptyString);
|
||||||
if (!isInventoryRootFrameIntent(detectedIntent)) {
|
if (!rootFrame) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const anchor = extractAddressCarryoverAnchor(debug);
|
|
||||||
const filtersRaw = debug.extracted_filters;
|
|
||||||
const filters = filtersRaw && typeof filtersRaw === "object"
|
|
||||||
? { ...filtersRaw }
|
|
||||||
: {};
|
|
||||||
return {
|
return {
|
||||||
intent: detectedIntent,
|
intent: rootFrame.intent,
|
||||||
filters,
|
filters: { ...(rootFrame.filters ?? {}) },
|
||||||
anchorType: anchor.anchorType,
|
anchorType: rootFrame.anchorType,
|
||||||
anchorValue: anchor.anchorValue,
|
anchorValue: rootFrame.anchorValue,
|
||||||
messageId: toNonEmptyString(item.message_id)
|
messageId: toNonEmptyString(item.message_id)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2492,6 +2492,109 @@ describe("assistant address follow-up carryover", () => {
|
||||||
expect(normalizerService.normalize).not.toHaveBeenCalled();
|
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 () => {
|
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 calls: Array<{ message: string; options?: any }> = [];
|
||||||
const followupMessage = "у кого мы модуль прямоугольный купили кстати";
|
const followupMessage = "у кого мы модуль прямоугольный купили кстати";
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue