ARCH: защитить document→payments pivot от discovery override
This commit is contained in:
@@ -649,6 +649,9 @@ describe("assistant address follow-up carryover", () => {
|
||||
user_message: followupMessage,
|
||||
useMock: true
|
||||
} as any);
|
||||
if (second.reply_type !== "factual") {
|
||||
throw new Error(JSON.stringify({ calls, secondReplyType: second.reply_type, secondDebug: second.debug }, null, 2));
|
||||
}
|
||||
expect(second.ok).toBe(true);
|
||||
expect(second.reply_type).toBe("factual");
|
||||
|
||||
@@ -727,6 +730,9 @@ describe("assistant address follow-up carryover", () => {
|
||||
user_message: followupMessage,
|
||||
useMock: true
|
||||
} as any);
|
||||
if (second.reply_type !== "factual") {
|
||||
throw new Error(JSON.stringify({ calls, secondReplyType: second.reply_type, secondDebug: second.debug }, null, 2));
|
||||
}
|
||||
expect(second.ok).toBe(true);
|
||||
expect(second.reply_type).toBe("factual");
|
||||
|
||||
@@ -826,6 +832,9 @@ describe("assistant address follow-up carryover", () => {
|
||||
useMock: true
|
||||
} as any);
|
||||
expect(second.ok).toBe(true);
|
||||
if (second.reply_type !== "factual") {
|
||||
throw new Error(JSON.stringify({ calls, secondReplyType: second.reply_type, secondDebug: second.debug }, null, 2));
|
||||
}
|
||||
expect(second.reply_type).toBe("factual");
|
||||
expect(second.debug?.address_retry_audit?.attempted).toBe(false);
|
||||
|
||||
@@ -834,6 +843,96 @@ describe("assistant address follow-up carryover", () => {
|
||||
expect(normalizerService.normalize).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("switches from document drilldown to bank operations on a pronoun payment follow-up", async () => {
|
||||
const calls: Array<{ message: string; options?: any }> = [];
|
||||
const firstMessage = "покажи документы по жуковке 51";
|
||||
const followupMessage = "а по нему платежи?";
|
||||
|
||||
const addressQueryService = {
|
||||
tryHandle: vi.fn(async (message: string, options?: any) => {
|
||||
calls.push({ message, options });
|
||||
if (message === followupMessage && !options?.followupContext) {
|
||||
return null;
|
||||
}
|
||||
if (options?.followupContext?.previous_intent === "bank_operations_by_counterparty") {
|
||||
return buildAddressLaneResult({
|
||||
reply_text: "Собран список банковских операций по контрагенту.",
|
||||
debug: {
|
||||
...buildAddressLaneResult().debug,
|
||||
detected_intent: "bank_operations_by_counterparty",
|
||||
selected_recipe: "address_bank_operations_by_counterparty_v1",
|
||||
extracted_filters: {
|
||||
sort: "period_desc",
|
||||
limit: 20,
|
||||
counterparty: "жуковке 51"
|
||||
},
|
||||
anchor_type: "counterparty",
|
||||
anchor_value_raw: "жуковке 51",
|
||||
anchor_value_resolved: "ТСЖ \\Жуковка 51\\",
|
||||
reasons: ["address_action_detected", "bank_ops_by_counterparty_signal_detected", "address_followup_context_applied"]
|
||||
}
|
||||
});
|
||||
}
|
||||
return buildAddressLaneResult({
|
||||
debug: {
|
||||
...buildAddressLaneResult().debug,
|
||||
detected_intent: "list_documents_by_counterparty",
|
||||
selected_recipe: "address_documents_by_counterparty_v1",
|
||||
extracted_filters: {
|
||||
sort: "period_desc",
|
||||
limit: 20,
|
||||
counterparty: "жуковке 51"
|
||||
},
|
||||
anchor_type: "counterparty",
|
||||
anchor_value_raw: "жуковке 51",
|
||||
anchor_value_resolved: "ТСЖ \\Жуковка 51\\"
|
||||
}
|
||||
});
|
||||
})
|
||||
} 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-docs-to-bank-pronoun-${Date.now()}`;
|
||||
const first = await service.handleMessage({
|
||||
session_id: sessionId,
|
||||
user_message: firstMessage,
|
||||
useMock: true
|
||||
} as any);
|
||||
expect(first.ok).toBe(true);
|
||||
expect(first.reply_type).toBe("factual");
|
||||
|
||||
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(2);
|
||||
expect(calls[1].message).toMatch(/банковские операции|платежи/i);
|
||||
expect(calls[1].options?.followupContext?.previous_intent).toBe("bank_operations_by_counterparty");
|
||||
expect(calls[1].options?.followupContext?.target_intent).toBe("bank_operations_by_counterparty");
|
||||
expect(calls[1].options?.followupContext?.previous_anchor_type).toBe("counterparty");
|
||||
expect(normalizerService.normalize).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reuses last real address context after intermediate clarification fallback", async () => {
|
||||
const calls: Array<{ message: string; options?: any }> = [];
|
||||
const lifecycleFollowupMessage = "А кто из них новые?";
|
||||
|
||||
@@ -235,6 +235,56 @@ describe("assistant MCP discovery response policy", () => {
|
||||
expect(result.reason_codes).toContain("mcp_discovery_response_policy_keep_factual_address_continuation_target");
|
||||
});
|
||||
|
||||
it("keeps factual suggested-intent pivot replies over discovery clarification candidates", () => {
|
||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||
currentReply: "Найдены банковские операции по контрагенту Жуковка 51.",
|
||||
currentReplySource: "address_query_runtime_v1",
|
||||
currentReplyType: "factual",
|
||||
addressRuntimeMeta: {
|
||||
detected_intent: "bank_operations_by_counterparty",
|
||||
dialog_continuation_contract_v2: {
|
||||
decision: "switch_to_suggested",
|
||||
target_intent: "bank_operations_by_counterparty",
|
||||
intent_selection_mode: "switch_to_suggested_intent",
|
||||
suggested_intent_pivot_signal: true
|
||||
},
|
||||
assistant_mcp_discovery_entry_point_v1: entryPoint({
|
||||
turn_input: {
|
||||
adapter_status: "ready",
|
||||
should_run_discovery: true,
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "turnover",
|
||||
explicit_entity_candidates: ["ТСЖ \\Жуковка 51\\"],
|
||||
unsupported_but_understood_family: "counterparty_value_or_turnover"
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
user_facing_response_allowed: true,
|
||||
business_fact_answer_allowed: false,
|
||||
requires_user_clarification: true,
|
||||
answer_draft: {
|
||||
answer_mode: "needs_clarification",
|
||||
headline: "Нужно уточнить контекст перед поиском в 1С.",
|
||||
confirmed_lines: [],
|
||||
inference_lines: [],
|
||||
unknown_lines: [],
|
||||
limitation_lines: [],
|
||||
next_step_line: "Уточните контрагента, период или организацию."
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.applied).toBe(false);
|
||||
expect(result.decision).toBe("keep_current_reply");
|
||||
expect(result.reply_text).toBe("Найдены банковские операции по контрагенту Жуковка 51.");
|
||||
expect(result.reason_codes).toContain("mcp_discovery_response_policy_keep_factual_suggested_intent_pivot_target");
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_candidate_applied");
|
||||
});
|
||||
|
||||
it("overrides an exact ranking-shaped address reply when open-scope ranking still needs organization", () => {
|
||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||
currentReply:
|
||||
|
||||
@@ -691,6 +691,52 @@ describe("assistantTransitionPolicy", () => {
|
||||
expect(carryover?.followupSelectionMode).toBe("carry_previous_intent");
|
||||
});
|
||||
|
||||
it("switches from documents to suggested bank operations on a pronoun follow-up with payment cue", () => {
|
||||
const policy = buildPolicy({
|
||||
findLastAddressAssistantItem: () => ({
|
||||
text: "Собран список документов по контрагенту Жуковка 51.",
|
||||
debug: {
|
||||
detected_intent: "list_documents_by_counterparty",
|
||||
extracted_filters: {
|
||||
counterparty: "Р–СѓРєРѕРІРєР° 51"
|
||||
},
|
||||
anchor_type: "counterparty",
|
||||
anchor_value_resolved: "РўРЎР– \\Р–СѓРєРѕРІРєР° 51\\"
|
||||
}
|
||||
}),
|
||||
buildAddressFollowupOffer: () => ({
|
||||
enabled: true,
|
||||
source_intent: "list_documents_by_counterparty",
|
||||
suggested_intents: ["bank_operations_by_counterparty", "list_contracts_by_counterparty"]
|
||||
}),
|
||||
hasAddressFollowupContextSignal: () => true,
|
||||
hasReferentialPointer: () => true
|
||||
});
|
||||
|
||||
const carryover = policy.resolveAddressFollowupCarryoverContext("а по нему платежи?", [], null, null, null);
|
||||
|
||||
expect(carryover?.followupContext?.previous_intent).toBe("bank_operations_by_counterparty");
|
||||
expect(carryover?.followupContext?.target_intent).toBe("bank_operations_by_counterparty");
|
||||
expect(carryover?.followupContext?.previous_anchor_type).toBe("counterparty");
|
||||
expect(carryover?.followupSelectionMode).toBe("switch_to_suggested_intent");
|
||||
expect(carryover?.hasSuggestedIntentPivotSignal).toBe(true);
|
||||
|
||||
const contract = policy.buildAddressDialogContinuationContractV2(
|
||||
"а по нему платежи?",
|
||||
"Покажи платежи, связанные с этим объектом",
|
||||
carryover,
|
||||
{
|
||||
predecomposeContract: {
|
||||
intent: "unknown"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
expect(contract.decision).toBe("switch_to_suggested");
|
||||
expect(contract.target_intent).toBe("bank_operations_by_counterparty");
|
||||
expect(contract.decision_reasons).toContain("suggested_intent_followup_pivot");
|
||||
});
|
||||
|
||||
it("keeps root-scoped carryover for foreign accounting pivot over inventory drilldown", () => {
|
||||
const policy = buildPolicy({
|
||||
findLastAddressAssistantItem: () => ({
|
||||
|
||||
Reference in New Issue
Block a user