ARCH: ввести resumable discovery loop state для clarification follow-up

This commit is contained in:
2026-04-23 11:54:36 +03:00
parent 0d3b33578e
commit e4cab85dd9
11 changed files with 715 additions and 11 deletions
@@ -110,6 +110,46 @@ describe("assistant MCP discovery runtime bridge", () => {
expect(result.answer_draft.next_step_line).not.toContain("Уточните контрагента");
});
it("emits a resumable loop state for clarification-driven ranking chains", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
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: "explicit_period",
comparison_need: null,
ranking_need: "top_desc",
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
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"]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020"
},
deps: buildDeps([])
});
expect(result.bridge_status).toBe("needs_clarification");
expect(result.loop_state).toMatchObject({
loop_status: "awaiting_clarification",
selected_chain_id: "value_flow_ranking",
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
ranking_need: "top_desc",
explicit_date_scope: "2020"
});
expect(result.loop_state.pending_axes).toContain("organization");
expect(result.loop_state.provided_axes).toContain("aggregate_axis");
expect(result.reason_codes).toContain("runtime_bridge_loop_state_awaiting_clarification");
});
it("produces a bounded ranked value-flow answer when period and organization are known", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
dataNeedGraph: {
@@ -1598,6 +1598,81 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.data_need_graph?.clarification_gaps).toEqual([]);
});
it("resumes an open-scope ranking from saved loop state even without a previous pilot scope", () => {
const orgName = "ООО Альтернатива Плюс";
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "по ООО Альтернатива Плюс",
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"],
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_from: "2020-01-01",
period_to: "2020-12-31"
}
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.source_signal).toBe("followup_context");
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
seeded_ranking_need: "top_desc",
explicit_organization_scope: orgName,
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_value_or_turnover",
stale_replay_forbidden: true
});
expect(result.data_need_graph?.ranking_need).toBe("top_desc");
expect(result.reason_codes).toContain("mcp_discovery_resumed_from_saved_loop_state");
});
it("resolves metadata lane choice from saved loop state even without a previous pilot scope", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "по движениям",
followupContext: {
previous_discovery_loop_status: "awaiting_clarification",
previous_discovery_loop_selected_chain_id: "metadata_lane_clarification",
previous_discovery_loop_pending_axes: ["lane_family_choice"],
previous_discovery_loop_asked_domain_family: "metadata",
previous_discovery_loop_asked_action_family: "resolve_next_lane",
previous_discovery_loop_unsupported_family: "metadata_lane_choice_clarification",
previous_filters: {
counterparty: "Группа СВК",
period_from: "2020-01-01",
period_to: "2020-12-31"
},
previous_discovery_metadata_ambiguity_detected: true,
previous_discovery_metadata_ambiguity_entity_sets: ["Документ", "РегистрНакопления"]
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.source_signal).toBe("followup_context");
expect(result.semantic_data_need).toBe("movement evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "movements",
asked_action_family: "list_movements",
explicit_entity_candidates: ["Группа СВК"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "movement_evidence",
stale_replay_forbidden: true
});
expect(result.reason_codes).toContain("mcp_discovery_resumed_from_saved_loop_state");
});
it("keeps seeded ranking through a year-switch follow-up after organization clarification", () => {
const orgName = "ООО Альтернатива Плюс";
const result = buildAssistantMcpDiscoveryTurnInput({
@@ -1332,6 +1332,88 @@ describe("assistantTransitionPolicy", () => {
expect(carryover?.followupContext?.target_intent).toBe("customer_revenue_and_payments");
});
it("carries resumable discovery loop state into followup context", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => null,
hasAddressFollowupContextSignal: () => true
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
"ООО Альтернатива Плюс",
[
{
role: "assistant",
text: "Нужно уточнить организацию, чтобы продолжить рейтинг.",
debug: {
execution_lane: "living_chat",
mcp_discovery_response_applied: true,
assistant_mcp_discovery_entry_point_v1: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
entry_status: "bridge_executed",
turn_input: {
data_need_graph: {
business_fact_family: "value_flow",
ranking_need: "top_desc",
subject_candidates: [],
clarification_gaps: ["organization"]
},
turn_meaning_ref: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020",
seeded_ranking_need: "top_desc"
}
},
bridge: {
bridge_status: "needs_clarification",
business_fact_answer_allowed: false,
pilot: {
pilot_scope: "counterparty_value_flow_query_movements_v1"
},
loop_state: {
schema_version: "assistant_mcp_discovery_loop_state_v1",
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
loop_status: "awaiting_clarification",
selected_chain_id: "value_flow_ranking",
pilot_scope: "counterparty_value_flow_query_movements_v1",
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
unsupported_but_understood_family: "counterparty_value_or_turnover",
ranking_need: "top_desc",
pending_axes: ["organization"],
provided_axes: ["aggregate_axis", "amount", "coverage_target"],
explicit_entity_candidates: [],
explicit_organization_scope: null,
explicit_date_scope: "2020"
},
answer_draft: {
answer_mode: "needs_clarification"
}
}
}
}
}
],
null,
null,
null
);
expect(carryover?.followupContext?.previous_discovery_loop_status).toBe("awaiting_clarification");
expect(carryover?.followupContext?.previous_discovery_loop_selected_chain_id).toBe("value_flow_ranking");
expect(carryover?.followupContext?.previous_discovery_loop_pending_axes).toEqual(["organization"]);
expect(carryover?.followupContext?.previous_discovery_loop_provided_axes).toEqual([
"aggregate_axis",
"amount",
"coverage_target"
]);
expect(carryover?.followupContext?.previous_discovery_loop_asked_domain_family).toBe("counterparty_value");
expect(carryover?.followupContext?.previous_discovery_loop_asked_action_family).toBe("turnover");
expect(carryover?.followupContext?.previous_discovery_loop_unsupported_family).toBe(
"counterparty_value_or_turnover"
);
});
it("carries grounded metadata downstream route hints into followup context", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => null,