ARCH: замкнуть multi-hop ranking clarification loop

This commit is contained in:
2026-04-23 12:20:39 +03:00
parent e4cab85dd9
commit e18b0922ad
14 changed files with 363 additions and 20 deletions
@@ -256,6 +256,46 @@ describe("assistant MCP discovery answer adapter", () => {
expect(draft.next_step_line).not.toContain("Уточните контрагента");
});
it("asks for both organization and period when an open ranking still misses both axes", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "turnover",
aggregation_need: null,
time_scope_need: "period_required",
comparison_need: null,
ranking_need: "top_desc",
proof_expectation: "clarification_required",
clarification_gaps: ["organization", "period"],
decomposition_candidates: ["collect_scoped_movements", "aggregate_ranked_axis_values", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: [
"data_need_graph_built",
"data_need_graph_ranking_top_desc",
"data_need_graph_open_scope_total_needs_organization",
"data_need_graph_has_clarification_gaps"
]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
seeded_ranking_need: "top_desc"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([]));
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.answer_mode).toBe("needs_clarification");
expect(draft.headline).toContain("период");
expect(draft.headline).toContain("организац");
expect(draft.next_step_line).toContain("период");
expect(draft.next_step_line).toContain("организац");
});
it("asks for organization rather than counterparty on open bidirectional comparison when only the period is known", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
@@ -325,6 +325,89 @@ describe("assistant MCP discovery runtime entry point", () => {
expect(result.bridge?.planner.selected_chain_id).toBe("value_flow_comparison");
});
it("keeps the same ranking loop after organization-only clarification when period is still missing", async () => {
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
userMessage: "по ООО Альтернатива Плюс",
predecomposeContract: {
entities: { organization: "ООО Альтернатива Плюс" }
},
followupContext: {
previous_discovery_loop_status: "awaiting_clarification",
previous_discovery_loop_selected_chain_id: "value_flow_ranking",
previous_discovery_loop_pending_axes: ["organization", "period"],
previous_discovery_loop_provided_axes: ["aggregate_axis", "amount", "coverage_target"],
previous_discovery_loop_asked_domain_family: "counterparty_value",
previous_discovery_loop_asked_action_family: "turnover",
previous_discovery_loop_unsupported_family: "counterparty_value_or_turnover",
previous_discovery_ranking_need: "top_desc"
},
deps: buildDeps([])
});
expect(result.entry_status).toBe("bridge_executed");
expect(result.turn_input.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_organization_scope: "ООО Альтернатива Плюс",
seeded_ranking_need: "top_desc"
});
expect(result.turn_input.turn_meaning_ref?.explicit_date_scope).toBeUndefined();
expect(result.bridge?.bridge_status).toBe("needs_clarification");
expect(result.bridge?.planner.selected_chain_id).toBe("value_flow_ranking");
expect(result.bridge?.loop_state).toMatchObject({
loop_status: "awaiting_clarification",
selected_chain_id: "value_flow_ranking",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: null,
ranking_need: "top_desc"
});
expect(result.bridge?.loop_state.pending_axes).toContain("period");
expect(result.bridge?.loop_state.pending_axes).not.toContain("organization");
expect(result.bridge?.answer_draft.next_step_line).toContain("период");
expect(result.bridge?.answer_draft.next_step_line).not.toContain("организацию");
});
it("completes the same ranking loop after the second clarification provides the period", async () => {
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
userMessage: "за 2020 год",
followupContext: {
previous_discovery_loop_status: "awaiting_clarification",
previous_discovery_loop_selected_chain_id: "value_flow_ranking",
previous_discovery_loop_pending_axes: ["period"],
previous_discovery_loop_provided_axes: ["aggregate_axis", "amount", "coverage_target", "organization"],
previous_discovery_loop_asked_domain_family: "counterparty_value",
previous_discovery_loop_asked_action_family: "turnover",
previous_discovery_loop_unsupported_family: "counterparty_value_or_turnover",
previous_discovery_ranking_need: "top_desc",
previous_filters: {
organization: "ООО Альтернатива Плюс"
}
},
deps: buildDeps([
{ Period: "2020-01-10T00:00:00", Amount: 1200, Counterparty: "СВК-А" },
{ Period: "2020-03-11T00:00:00", Amount: 800, Counterparty: "СВК-Б" },
{ Period: "2020-05-12T00:00:00", Amount: 900, Counterparty: "СВК-А" }
])
});
expect(result.entry_status).toBe("bridge_executed");
expect(result.turn_input.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020",
seeded_ranking_need: "top_desc"
});
expect(result.bridge?.bridge_status).toBe("answer_draft_ready");
expect(result.bridge?.planner.selected_chain_id).toBe("value_flow_ranking");
expect(result.bridge?.pilot.derived_ranked_value_flow?.ranked_values[0]).toMatchObject({
axis_value: "СВК-А",
total_amount: 2100
});
expect(result.bridge?.loop_state.loop_status).toBe("ready_for_next_hop");
expect(result.bridge?.loop_state.pending_axes).toEqual([]);
});
it.skip("keeps mirrored predecompose organization and counterparty out of the subject lane for open comparison (utf8-safe)", async () => {
const orgName = "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
@@ -1335,6 +1335,7 @@ describe("assistant MCP discovery turn input adapter", () => {
"probe_coverage"
]);
});
it("keeps organization as scope for open bidirectional comparison wording instead of inventing a subject candidate", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "что больше: входящие или исходящие деньги за 2020 год по ООО Альтернатива Плюс?",
@@ -1638,6 +1639,45 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.reason_codes).toContain("mcp_discovery_resumed_from_saved_loop_state");
});
it("does not keep an implicit today date while a ranking clarification loop still needs period", () => {
const todayIso = new Date().toISOString().slice(0, 10);
const orgName = "ООО Альтернатива Плюс";
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "по ООО Альтернатива Плюс",
assistantTurnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: todayIso,
explicit_intent_candidate: "customer_revenue_and_payments"
},
predecomposeContract: {
entities: { organization: orgName }
},
followupContext: {
previous_discovery_loop_status: "awaiting_clarification",
previous_discovery_loop_selected_chain_id: "value_flow_ranking",
previous_discovery_loop_pending_axes: ["organization", "period"],
previous_discovery_loop_provided_axes: ["aggregate_axis", "amount", "coverage_target"],
previous_discovery_loop_asked_domain_family: "counterparty_value",
previous_discovery_loop_asked_action_family: "turnover",
previous_discovery_loop_unsupported_family: "counterparty_value_or_turnover",
previous_discovery_ranking_need: "top_desc",
previous_filters: {
period_to: todayIso
}
}
});
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
seeded_ranking_need: "top_desc",
explicit_organization_scope: orgName
});
expect(result.turn_meaning_ref?.explicit_date_scope).toBeUndefined();
expect(result.data_need_graph?.clarification_gaps).toEqual(["period"]);
});
it("resolves metadata lane choice from saved loop state even without a previous pilot scope", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "по движениям",