Закрепить phase83 planner-brain replay ассистента

This commit is contained in:
2026-06-03 15:56:35 +03:00
parent f24cddc492
commit 5ed25bad81
22 changed files with 1439 additions and 28 deletions
@@ -117,6 +117,28 @@ describe("assistant MCP discovery answer adapter", () => {
expect(draft.must_not_claim).toContain("Do not claim a confirmed business fact when confirmed_facts is empty.");
});
it("states zero rubles for checked payout searches with no matched rows", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "payout",
explicit_entity_candidates: ["Группа СВК"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_payouts_or_outflow"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([]));
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
const answerText = [draft.headline, ...draft.confirmed_lines, ...draft.unknown_lines].join("\n");
expect(draft.answer_mode).toBe("checked_sources_only");
expect(answerText).toContain("0 руб.");
expect(answerText).toContain("исходящих платежей/списаний не найдено");
expect(answerText).toContain("Группа СВК");
expect(answerText).toContain("2020");
});
it("turns generic document evidence into a bounded document answer draft", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
@@ -367,6 +367,42 @@ describe("assistant MCP discovery data need graph", () => {
);
});
it("routes broad-overview incoming-vs-outgoing money comparison to value-flow comparison", () => {
const result = buildAssistantMcpDiscoveryDataNeedGraph({
semanticDataNeed: "business overview evidence with bounded analyst interpretation",
rawUtterance:
"Хорошо. А что по ООО Альтернатива Плюс больше в 2020 году: входящие или исходящие деньги?",
turnMeaning: {
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
unsupported_but_understood_family: "broad_business_evaluation",
explicit_entity_candidates: ["Группа СВК", "ООО Альтернатива Плюс больше"],
metadata_scope_hint: "Группа СВК",
explicit_date_scope: "2020",
explicit_organization_scope: "ООО Альтернатива Плюс"
}
});
expect(result.business_fact_family).toBe("value_flow");
expect(result.subject_candidates).toEqual([]);
expect(result.metadata_scope_hint).toBeNull();
expect(result.action_family).toBe("net_value_flow");
expect(result.comparison_need).toBe("incoming_vs_outgoing");
expect(result.clarification_gaps).toEqual([]);
expect(result.decomposition_candidates).toEqual([
"collect_incoming_movements",
"collect_outgoing_movements",
"probe_coverage"
]);
expect(result.reason_codes).toContain(
"data_need_graph_business_overview_bidirectional_money_comparison_routed_to_value_flow"
);
expect(result.reason_codes).toContain(
"data_need_graph_organization_scoped_value_comparison_ignores_stale_subject"
);
expect(result.reason_codes).toContain("data_need_graph_comparison_incoming_vs_outgoing");
});
it("treats a generic incoming total as an understood open-scope ask that still needs organization", () => {
const result = buildAssistantMcpDiscoveryDataNeedGraph({
semanticDataNeed: "counterparty value-flow evidence",
@@ -1170,6 +1170,41 @@ describe("assistant MCP discovery pilot executor", () => {
});
});
it("prefers normalized metadata scope hint over inflected entity candidate", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "metadata",
asked_action_family: "inspect_catalog",
explicit_entity_candidates: ["контрагентам", "контрагент"],
metadata_scope_hint: "контрагент",
unsupported_but_understood_family: "1c_metadata_surface"
}
});
const deps = buildMetadataDeps([
{
FullName: "Справочник.Контрагенты",
MetaType: "Справочник",
attributes: [{ Name: "Наименование" }, { Name: "ИНН" }]
}
]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.evidence.evidence_status).toBe("confirmed");
expect(result.derived_metadata_surface).toMatchObject({
metadata_scope: "контрагент",
requested_meta_types: ["Справочник"],
matched_rows: 1,
available_entity_sets: ["Справочник"],
matched_objects: ["Справочник.Контрагенты"]
});
expect(deps.executeAddressMcpMetadata).toHaveBeenCalledTimes(1);
expect(deps.executeAddressMcpMetadata.mock.calls[0]?.[0]).toMatchObject({
meta_type: ["Справочник"],
name_mask: "контрагент"
});
});
it("executes catalog drilldown through a narrowed metadata probe seeded from the confirmed surface object", async () => {
const planner = planAssistantMcpDiscovery({
metadataSurface: {
@@ -1750,6 +1785,75 @@ describe("assistant MCP discovery pilot executor", () => {
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(2);
});
it("executes organization-scoped value-flow comparison without stale counterparty subject", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
metadata_scope_hint: null,
business_fact_family: "value_flow",
action_family: "net_value_flow",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: "incoming_vs_outgoing",
ranking_need: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["collect_incoming_movements", "collect_outgoing_movements", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: [
"data_need_graph_built",
"data_need_graph_family_value_flow",
"data_need_graph_comparison_incoming_vs_outgoing",
"data_need_graph_organization_scoped_value_comparison_ignores_stale_subject"
]
},
turnMeaning: {
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
explicit_entity_candidates: ["SVK", "OOO Alternative Plus more"],
metadata_scope_hint: "SVK",
explicit_organization_scope: "OOO Alternative Plus",
explicit_date_scope: "2020",
unsupported_but_understood_family: "broad_business_evaluation"
}
});
const deps = buildSequentialDeps([
{
rows: [{ Period: "2020-01-15T00:00:00", Amount: 10000, Organization: "OOO Alternative Plus" }]
},
{
rows: [{ Period: "2020-03-10T00:00:00", Amount: 4000, Organization: "OOO Alternative Plus" }]
}
]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.pilot_scope).toBe("counterparty_bidirectional_value_flow_query_movements_v1");
expect(result.derived_value_flow).toBeNull();
expect(result.derived_bidirectional_value_flow).toMatchObject({
counterparty: null,
period_scope: "2020",
net_amount: 6000,
net_direction: "net_incoming",
incoming_customer_revenue: {
rows_matched: 1,
total_amount: 10000
},
outgoing_supplier_payout: {
rows_matched: 1,
total_amount: 4000
}
});
expect(result.reason_codes).toContain("pilot_bidirectional_value_flow_recipes_selected");
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(2);
const serializedCalls = deps.executeAddressMcpQuery.mock.calls
.map((call) => JSON.stringify(call[0]))
.join("\n");
expect(serializedCalls).not.toContain("SVK");
});
it("prefers clean counterparty candidates for bidirectional net value-flow probes", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
@@ -1280,6 +1280,10 @@ describe("assistant MCP discovery response candidate", () => {
);
expect(candidate.reply_text?.split("\n")[0]).toContain("по компании ООО Альтернатива Плюс за период 2020");
expect(candidate.reply_text).toContain("Основа: проверенные входящие платежи и исходящие платежи по этой компании в 1С.");
expect(candidate.reply_text).toContain("нетто в сторону входящих платежей: 3 865 501,50 руб.");
expect(candidate.reply_text).not.toContain("Коротко:");
expect(candidate.reply_text).not.toContain("по этому контрагенту");
expect(candidate.reply_text).not.toContain("по контрагенту запрошенному контрагенту");
});
@@ -1918,7 +1922,7 @@ describe("assistant MCP discovery response candidate", () => {
);
const firstLine = candidate.reply_text?.split("\n")[0] ?? "";
expect(firstLine).toContain("Коротко:");
expect(firstLine).not.toContain("Коротко:");
expect(firstLine).toContain("SVK");
expect(firstLine).toContain("получили 12 500,50 руб.");
expect(firstLine).toContain("заплатили 4 000 руб.");
@@ -57,6 +57,7 @@ describe("assistant MCP discovery response policy", () => {
mcpDiscoveryRuntimeEntryPoint: entryPoint({
bridge: {
bridge_status: "answer_draft_ready",
hot_runtime_wired: true,
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
@@ -79,6 +80,56 @@ describe("assistant MCP discovery response policy", () => {
expect(result.reply_text).not.toContain("Коротко:");
});
it("keeps current organization clarification over a checked-sources value-flow candidate", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply:
"Нужно уточнить организацию, чтобы не смешивать компании в одном ответе.\nСейчас в доступном контуре вижу такие организации:\n- ООО Альтернатива Плюс\n- ООО Ромашка",
currentReplySource: "address_query_runtime_v1",
currentReplyType: "partial_coverage",
addressRuntimeMeta: {
truth_mode: "clarification_required",
answer_shape: "clarification_required",
answer_shape_contract: {
answer_shape: "clarification_required",
reply_type: "partial_coverage"
},
assistant_mcp_discovery_entry_point_v1: entryPoint({
turn_input: {
adapter_status: "ready",
should_run_discovery: true,
data_need_graph: {
business_fact_family: "value_flow",
subject_candidates: [],
clarification_gaps: ["organization"],
reason_codes: ["data_need_graph_open_scope_total_needs_organization"]
}
},
bridge: {
bridge_status: "checked_sources_only",
user_facing_response_allowed: true,
business_fact_answer_allowed: false,
requires_user_clarification: true,
answer_draft: {
answer_mode: "checked_sources_only",
headline: "В проверенном срезе 1С входящих денежных поступлений не найдено.",
confirmed_lines: ["В проверенном срезе 1С входящих денежных поступлений не найдено."],
inference_lines: [],
unknown_lines: [],
limitation_lines: [],
next_step_line: null
}
}
})
}
});
expect(result.applied).toBe(false);
expect(result.reply_text).toContain("Нужно уточнить организацию");
expect(result.reply_text).toContain("ООО Альтернатива Плюс");
expect(result.reply_text).not.toContain("не найдено");
expect(result.reason_codes).toContain("mcp_discovery_response_policy_keep_current_clarification_required_reply");
});
it("keeps the current reply when the turn is not an unsupported boundary", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "regular chat",
@@ -236,6 +287,7 @@ describe("assistant MCP discovery response policy", () => {
},
bridge: {
bridge_status: "answer_draft_ready",
hot_runtime_wired: true,
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
@@ -341,6 +393,68 @@ describe("assistant MCP discovery response policy", () => {
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_exact_matched_factual_address_reply");
});
it("uses a movement clarification candidate over a stale inventory-item clarification", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "Need item before I can continue.",
currentReplySource: "address_query_runtime_v1",
currentReplyType: "partial_coverage",
addressRuntimeMeta: {
detected_intent: "inventory_purchase_documents_for_item",
selected_recipe: "address_inventory_purchase_documents_for_item_v1",
capability_id: "inventory_inventory_purchase_documents_for_item",
truth_mode: "clarification_required",
answer_shape: "clarification_required",
missing_required_filters: ["item"],
answer_shape_contract: {
answer_shape: "clarification_required",
reply_type: "partial_coverage",
capability_contract_id: "inventory_inventory_purchase_documents_for_item"
},
assistant_mcp_discovery_entry_point_v1: entryPoint({
turn_input: {
adapter_status: "ready",
should_run_discovery: true,
turn_meaning_ref: {
asked_domain_family: "movements",
asked_action_family: "list_movements",
explicit_entity_candidates: ["Group SVK"],
unsupported_but_understood_family: "movement_evidence"
},
data_need_graph: {
business_fact_family: "movement_evidence",
action_family: "list_movements",
clarification_gaps: ["period"]
}
},
bridge: {
bridge_status: "needs_clarification",
user_facing_response_allowed: true,
business_fact_answer_allowed: false,
requires_user_clarification: true,
answer_draft: {
answer_mode: "needs_clarification",
headline: "I can continue by movements for Group SVK, but I need a period.",
confirmed_lines: [],
inference_lines: [],
unknown_lines: ["The period is required before searching movements."],
limitation_lines: [],
next_step_line: "Specify the period and I will search movements for Group SVK."
}
}
})
}
});
expect(result.applied).toBe(true);
expect(result.decision).toBe("apply_candidate");
expect(result.reply_text).toContain("movements for Group SVK");
expect(result.reply_text).not.toContain("Need item");
expect(result.reason_codes).toContain(
"mcp_discovery_response_policy_clarification_candidate_priority_over_stale_address_clarification"
);
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_current_clarification_required_reply");
});
it("overrides exact document-list replies for explicit counterparty received-paid-net questions", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "Контрагент: Группа СВК. Найдено документов: 19.",
@@ -1509,4 +1623,95 @@ describe("assistant MCP discovery response policy", () => {
"mcp_discovery_response_policy_keep_broad_business_summary_over_clarification_candidate"
);
});
it("lets a grounded bidirectional value-flow candidate override a stale address clarification reply", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply:
"Проверка данных за 2020 год по контрагенту «Группа СВК» не удалась — такой точный или аналогичный вариант имени не найден.",
currentReplySource: "address_query_runtime_v1",
currentReplyType: "partial_coverage",
addressRuntimeMeta: {
truth_mode: "clarification_required",
answer_shape: "clarification_required",
detected_intent: "counterparty_population_and_roles",
selected_recipe: "address_counterparty_population_roles_v1",
assistant_mcp_discovery_entry_point_v1: entryPoint({
hot_runtime_wired: true,
turn_input: {
adapter_status: "ready",
should_run_discovery: true,
turn_meaning_ref: {
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_entity_candidates: ["Группа СВК"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting"
},
data_need_graph: {
business_fact_family: "value_flow",
action_family: "net_value_flow",
comparison_need: "incoming_vs_outgoing",
clarification_gaps: []
}
},
bridge: {
bridge_status: "answer_draft_ready",
hot_runtime_wired: true,
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
route_candidate: {
selected_chain_id: "value_flow_comparison"
},
execution_handoff: {
mcp_execution_performed: true,
can_use_guarded_response: true,
must_keep_internal_mechanics_hidden: true
},
pilot: {
pilot_scope: "counterparty_bidirectional_value_flow_query_movements_v1",
mcp_execution_performed: true,
derived_bidirectional_value_flow: {
counterparty: "Группа СВК",
period_scope: "2020",
net_amount_human_ru: "12 093 465 руб.",
net_direction: "net_incoming",
coverage_limited_by_probe_limit: false,
incoming_customer_revenue: {
total_amount_human_ru: "12 093 465 руб."
},
outgoing_supplier_payout: {
total_amount_human_ru: "0 руб."
}
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "Двусторонний денежный поток подтвержден.",
confirmed_lines: [
"1C bidirectional value-flow rows were checked for counterparty Группа СВК: incoming=found, outgoing=found"
],
inference_lines: [
"Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows"
],
unknown_lines: ["Full bidirectional value-flow outside the checked period is not proven by this MCP discovery pilot"],
limitation_lines: [],
next_step_line: null
}
}
})
}
});
expect(result.applied).toBe(true);
expect(result.decision).toBe("apply_candidate");
expect(result.reply_text).toContain("Группа СВК");
expect(result.reply_text).toContain("получили 12 093 465 руб.");
expect(result.reply_text).toContain("заплатили 0 руб.");
expect(result.reply_text).not.toContain("не найден");
expect(result.reason_codes).toContain(
"mcp_discovery_response_policy_grounded_value_flow_candidate_priority_over_current_clarification"
);
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_current_clarification_required_reply");
});
});
@@ -88,6 +88,36 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.turn_meaning_ref?.explicit_organization_scope).toBeUndefined();
});
it("does not treat no-counterparty organization scope as a counterparty entity", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage:
"Хочу быстрый денежный срез по одной организации без привязки к контрагенту. Сколько вообще входящих денег было за 2020 год?",
assistantTurnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "counterparty_value_or_turnover",
explicit_entity_candidates: ["одной организации без привязки"],
explicit_organization_scope: "без привязки к контрагенту",
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_value_or_turnover",
stale_replay_forbidden: true
},
predecomposeContract: {
entities: {},
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
},
knownOrganizations: ["ООО Альтернатива Плюс", "ООО Ромашка"]
});
expect(result.adapter_status).toBe("ready");
expect(result.data_need_graph?.business_fact_family).toBe("value_flow");
expect(result.data_need_graph?.subject_candidates).toEqual([]);
expect(result.data_need_graph?.clarification_gaps).toContain("organization");
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.turn_meaning_ref?.explicit_organization_scope).toBeUndefined();
expect(result.turn_meaning_ref?.explicit_date_scope).toBe("2020");
expect(result.reason_codes).not.toContain("mcp_discovery_counterparty_from_raw_scope");
});
it("keeps payout wording as outgoing supplier-payout discovery", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "сколько мы заплатили Группа СВК за 2020 год?",