Усилить семантический контур агента и большой прогон
This commit is contained in:
@@ -72,6 +72,15 @@ describe("addressIntentResolver regression bridges", () => {
|
||||
expect(result.reasons).toContain("unicode_business_overview_debt_position_deferred_to_discovery");
|
||||
});
|
||||
|
||||
it("defers colloquial multi-surface company overview to discovery before exact VAT", () => {
|
||||
const result = resolveAddressIntent(
|
||||
"\u041f\u043e-\u0447\u0435\u043b\u043e\u0432\u0435\u0447\u0435\u0441\u043a\u0438 \u043f\u043e\u0441\u043c\u043e\u0442\u0440\u0438 \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441 \u0437\u0430 2020: \u0447\u0442\u043e \u043f\u043e \u0431\u0438\u0437\u043d\u0435\u0441\u0443 \u0432\u0438\u0434\u043d\u043e \u0432 1\u0421 \u043f\u043e \u0434\u0435\u043d\u044c\u0433\u0430\u043c, \u041d\u0414\u0421, \u0434\u043e\u043b\u0433\u0430\u043c, \u0441\u043a\u043b\u0430\u0434\u0443, \u043a\u043b\u0438\u0435\u043d\u0442\u0430\u043c/\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\u0430\u043c \u0438 \u0433\u0434\u0435 \u043f\u043e\u043a\u0430 \u043d\u0435\u043b\u044c\u0437\u044f \u0434\u0435\u043b\u0430\u0442\u044c \u0432\u044b\u0432\u043e\u0434?"
|
||||
);
|
||||
|
||||
expect(result.intent).toBe("unknown");
|
||||
expect(result.reasons).toContain("unicode_business_overview_multi_surface_deferred_to_discovery");
|
||||
});
|
||||
|
||||
it("detects specific counterparty turnover wording as revenue profile", () => {
|
||||
const result = resolveAddressIntent(
|
||||
"\u043a\u0430\u043a\u043e\u0439 \u043e\u0431\u043e\u0440\u043e\u0442 \u0431\u044b\u043b \u0441\u0432\u043a"
|
||||
|
||||
@@ -55,6 +55,231 @@ describe("address navigation state", () => {
|
||||
expect(evolved.navigation_history[0]?.action).toBe("open");
|
||||
});
|
||||
|
||||
it("captures organization focus from applied business overview discovery turns", () => {
|
||||
const base = createEmptyAddressNavigationState("asst-bo", "2026-04-12T10:00:00.000Z");
|
||||
const assistantItem = {
|
||||
message_id: "msg-bo1",
|
||||
session_id: "asst-bo",
|
||||
role: "assistant",
|
||||
text: "Коротко: по данным ООО Альтернатива Плюс за 2020 подтвержден бизнес-обзор.",
|
||||
reply_type: "partial_coverage",
|
||||
created_at: "2026-04-12T10:10:00.000Z",
|
||||
trace_id: "address-bo",
|
||||
debug: {
|
||||
detected_mode: "address_query",
|
||||
detected_intent: "unknown",
|
||||
selected_recipe: null,
|
||||
extracted_filters: {},
|
||||
anchor_type: "unknown",
|
||||
mcp_discovery_response_applied: true,
|
||||
mcp_discovery_selected_chain_id: "business_overview",
|
||||
assistant_mcp_discovery_entry_point_v1: {
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
entry_status: "bridge_executed",
|
||||
turn_input: {
|
||||
turn_meaning_ref: {
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||
explicit_date_scope: "2020"
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference"
|
||||
}
|
||||
}
|
||||
},
|
||||
dialog_continuation_contract_v2: {
|
||||
decision: "new_topic"
|
||||
}
|
||||
}
|
||||
} as any;
|
||||
|
||||
const evolved = evolveAddressNavigationStateWithAssistantItem(base, assistantItem, 2);
|
||||
expect(evolved.result_sets[0]?.route_id).toBe("business_overview");
|
||||
expect(evolved.result_sets[0]?.filters.organization).toBe("ООО Альтернатива Плюс");
|
||||
expect(evolved.session_context.active_focus_object?.object_type).toBe("organization");
|
||||
expect(evolved.session_context.active_focus_object?.label).toBe("ООО Альтернатива Плюс");
|
||||
expect(evolved.session_context.organization_scope).toBe("ООО Альтернатива Плюс");
|
||||
expect(evolved.session_context.date_scope.period_from).toBe("2020-01-01");
|
||||
expect(evolved.session_context.date_scope.period_to).toBe("2020-12-31");
|
||||
});
|
||||
|
||||
it("captures counterparty focus from applied discovery turns even when the response stayed in chat mode", () => {
|
||||
const base = createEmptyAddressNavigationState("asst-vf", "2026-04-12T10:00:00.000Z");
|
||||
const assistantItem = {
|
||||
message_id: "msg-vf1",
|
||||
session_id: "asst-vf",
|
||||
role: "assistant",
|
||||
text: "\u041f\u043e \u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a \u0437\u0430 2020 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u044b \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0438 \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0434\u0435\u043d\u044c\u0433\u0438.",
|
||||
reply_type: "partial_coverage",
|
||||
created_at: "2026-04-12T10:12:00.000Z",
|
||||
trace_id: "chat-vf",
|
||||
debug: {
|
||||
detected_mode: "chat",
|
||||
detected_intent: "unknown",
|
||||
selected_recipe: null,
|
||||
extracted_filters: {},
|
||||
anchor_type: "unknown",
|
||||
mcp_discovery_response_applied: true,
|
||||
mcp_discovery_selected_chain_id: "value_flow_comparison",
|
||||
assistant_mcp_discovery_entry_point_v1: {
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
entry_status: "bridge_executed",
|
||||
turn_input: {
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "compare_incoming_outgoing",
|
||||
explicit_entity_candidates: ["\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a"],
|
||||
stale_replay_forbidden: true,
|
||||
explicit_date_scope: "2020"
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_factual"
|
||||
}
|
||||
}
|
||||
},
|
||||
dialog_continuation_contract_v2: {
|
||||
decision: "new_topic"
|
||||
}
|
||||
}
|
||||
} as any;
|
||||
|
||||
const evolved = evolveAddressNavigationStateWithAssistantItem(base, assistantItem, 2);
|
||||
expect(evolved.result_sets[0]?.route_id).toBe("value_flow_comparison");
|
||||
expect(evolved.result_sets[0]?.intent).toBe("customer_revenue_and_payments");
|
||||
expect(evolved.result_sets[0]?.filters.counterparty).toBe("\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a");
|
||||
expect(evolved.session_context.active_focus_object?.object_type).toBe("counterparty");
|
||||
expect(evolved.session_context.active_focus_object?.label).toBe("\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a");
|
||||
expect(evolved.session_context.date_scope.period_from).toBe("2020-01-01");
|
||||
expect(evolved.session_context.date_scope.period_to).toBe("2020-12-31");
|
||||
});
|
||||
|
||||
it("keeps selected counterparty focus for company boundary summaries", () => {
|
||||
const initial = createEmptyAddressNavigationState("asst-boundary", "2026-04-12T10:00:00.000Z");
|
||||
const assistantItem = {
|
||||
message_id: "msg-boundary",
|
||||
session_id: "asst-boundary",
|
||||
role: "assistant",
|
||||
text:
|
||||
"Коротко: по компании Альтернатива Плюс подтвержден company-level денежный срез.\n" +
|
||||
"Отдельно по выбранному контрагенту Группа СВК: суммы компании на него не переношу.",
|
||||
created_at: "2026-04-12T10:05:00.000Z",
|
||||
debug: {
|
||||
detected_intent: "unknown",
|
||||
mcp_discovery_selected_chain_id: "business_overview",
|
||||
mcp_discovery_response_applied: true,
|
||||
extracted_filters: {
|
||||
organization: "Альтернатива Плюс"
|
||||
},
|
||||
assistant_mcp_discovery_entry_point_v1: {
|
||||
turn_input: {
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "broad_evaluation",
|
||||
business_overview_separate_entity_candidates: ["Группа СВК"],
|
||||
explicit_organization_scope: "Альтернатива Плюс"
|
||||
},
|
||||
data_need_graph: {
|
||||
business_fact_family: "business_overview",
|
||||
subject_candidates: []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} as any;
|
||||
|
||||
const evolved = evolveAddressNavigationStateWithAssistantItem(initial, assistantItem, 1);
|
||||
|
||||
expect(evolved.session_context.organization_scope).toBe("Альтернатива Плюс");
|
||||
expect(evolved.session_context.active_focus_object?.object_type).toBe("counterparty");
|
||||
expect(evolved.session_context.active_focus_object?.label).toBe("Группа СВК");
|
||||
expect(evolved.session_context.comparison_scope?.organization?.label).toBe("Альтернатива Плюс");
|
||||
expect(evolved.session_context.comparison_scope?.counterparty?.label).toBe("Группа СВК");
|
||||
expect(evolved.navigation_history[0]?.target_object_id).toBe("counterparty:группа свк");
|
||||
});
|
||||
|
||||
it("carries comparison proof bundles through organization clarification", () => {
|
||||
const org = "\u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
||||
const counterparty = "\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a";
|
||||
const valueFlowBundle = {
|
||||
counterparty,
|
||||
incoming_customer_revenue: { total_amount_human_ru: "20 653 490 руб." },
|
||||
outgoing_supplier_payout: { total_amount_human_ru: "2 129 651 руб." },
|
||||
net_amount_human_ru: "18 523 839 руб."
|
||||
};
|
||||
const documentBundle = { counterparty, document_count: 19 };
|
||||
const initial = createEmptyAddressNavigationState("asst-proof", "2026-04-12T10:00:00.000Z");
|
||||
const boundaryItem = {
|
||||
message_id: "msg-boundary-proof",
|
||||
session_id: "asst-proof",
|
||||
role: "assistant",
|
||||
text: `Коротко: уточните организацию для сравнения ${counterparty}.`,
|
||||
created_at: "2026-04-12T10:05:00.000Z",
|
||||
debug: {
|
||||
detected_intent: "unknown",
|
||||
mcp_discovery_selected_chain_id: "business_overview",
|
||||
mcp_discovery_response_applied: true,
|
||||
extracted_filters: {},
|
||||
assistant_mcp_discovery_entry_point_v1: {
|
||||
turn_input: {
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "broad_evaluation",
|
||||
business_overview_separate_entity_candidates: [counterparty],
|
||||
previous_counterparty_value_flow_bundle: valueFlowBundle,
|
||||
previous_counterparty_document_bundle: documentBundle
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} as any;
|
||||
|
||||
const afterBoundary = evolveAddressNavigationStateWithAssistantItem(initial, boundaryItem, 1);
|
||||
expect(afterBoundary.session_context.comparison_scope?.organization).toBeNull();
|
||||
expect(afterBoundary.session_context.comparison_scope?.counterparty?.label).toBe(counterparty);
|
||||
expect(afterBoundary.session_context.comparison_scope?.proof_bundles?.counterparty_value_flow_bundle).toEqual(
|
||||
valueFlowBundle
|
||||
);
|
||||
|
||||
const clarifiedItem = {
|
||||
message_id: "msg-boundary-org",
|
||||
session_id: "asst-proof",
|
||||
role: "assistant",
|
||||
text: `Коротко: по компании ${org} подтвержден company-level срез. Отдельно по ${counterparty}: границу не теряю.`,
|
||||
created_at: "2026-04-12T10:06:00.000Z",
|
||||
debug: {
|
||||
detected_intent: "business_overview",
|
||||
mcp_discovery_selected_chain_id: "business_overview",
|
||||
mcp_discovery_response_applied: true,
|
||||
extracted_filters: { organization: org },
|
||||
assistant_mcp_discovery_entry_point_v1: {
|
||||
turn_input: {
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "broad_evaluation",
|
||||
business_overview_separate_entity_candidates: [counterparty],
|
||||
explicit_organization_scope: org
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} as any;
|
||||
|
||||
const afterClarification = evolveAddressNavigationStateWithAssistantItem(afterBoundary, clarifiedItem, 2);
|
||||
expect(afterClarification.session_context.comparison_scope?.organization?.label).toBe(org);
|
||||
expect(afterClarification.session_context.comparison_scope?.counterparty?.label).toBe(counterparty);
|
||||
expect(afterClarification.session_context.comparison_scope?.proof_bundles?.counterparty_value_flow_bundle).toEqual(
|
||||
valueFlowBundle
|
||||
);
|
||||
expect(afterClarification.session_context.comparison_scope?.proof_bundles?.counterparty_document_bundle).toEqual(
|
||||
documentBundle
|
||||
);
|
||||
});
|
||||
|
||||
it("tracks drilldown event for follow-up continuation turn", () => {
|
||||
const initial = normalizeAddressNavigationState(
|
||||
{
|
||||
|
||||
@@ -2145,6 +2145,43 @@ describe("address compose stage utf8 headers", () => {
|
||||
expect(reply.semantics?.balance_confirmed).toBe(true);
|
||||
});
|
||||
|
||||
it("explains purchase-date anchor for confirmed VAT tax-period reply", () => {
|
||||
const reply = composeFactualReply(
|
||||
"vat_liability_confirmed_for_tax_period",
|
||||
[
|
||||
{
|
||||
period: "2015-02-28T23:59:59Z",
|
||||
registrator: "VAT_BOOK_SALES",
|
||||
account_dt: "68.02",
|
||||
account_kt: "",
|
||||
amount: 3500000,
|
||||
analytics: []
|
||||
},
|
||||
{
|
||||
period: "2015-02-28T23:59:59Z",
|
||||
registrator: "VAT_BOOK_PURCHASES",
|
||||
account_dt: "19",
|
||||
account_kt: "",
|
||||
amount: 868612,
|
||||
analytics: []
|
||||
}
|
||||
],
|
||||
{
|
||||
userMessage: "ндс можешь прикинуть на дату покупки рабочей станции?",
|
||||
periodFrom: "2015-02-01",
|
||||
periodTo: "2015-02-28",
|
||||
asOfDate: "2016-03-31",
|
||||
organizationHint: "ООО Альтернатива Плюс",
|
||||
useRubCurrency: true
|
||||
}
|
||||
);
|
||||
|
||||
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
|
||||
expect(reply.text).toContain("Якорь периода: дата покупки из вопроса/контекста");
|
||||
expect(reply.text).toContain("01.02.2015..28.02.2015");
|
||||
expect(reply.text).toContain("книг продаж/покупок");
|
||||
});
|
||||
|
||||
it("formats VAT forecast amounts in rubles and emphasizes numbers when requested", () => {
|
||||
const reply = composeFactualReply(
|
||||
"vat_payable_forecast",
|
||||
|
||||
@@ -236,6 +236,216 @@ describe("assistant address lane response runtime adapter", () => {
|
||||
expect(String((runtime.response as any).assistant_reply)).toContain("исходящих платежей/списаний");
|
||||
});
|
||||
|
||||
it("aligns final route metadata when a business-overview discovery candidate replaces a stale exact reply", () => {
|
||||
const finalizeAddressTurn = vi.fn((input) => ({
|
||||
response: {
|
||||
ok: true,
|
||||
assistant_reply: input.assistantReply,
|
||||
reply_type: input.replyType,
|
||||
debug: input.debug
|
||||
}
|
||||
}));
|
||||
|
||||
const runtime = runAssistantAddressLaneResponseRuntime({
|
||||
sessionId: "asst-business-overview-address",
|
||||
userMessage: "Собери короткий бизнес-итог по компании и отдельно по СВК.",
|
||||
effectiveAddressUserMessage: "Собери короткий бизнес-итог по компании и отдельно по СВК.",
|
||||
addressLane: {
|
||||
handled: true,
|
||||
reply_text: "Контрагент: Группа СВК. Найдено документов: 19.",
|
||||
reply_type: "factual",
|
||||
debug: {
|
||||
detected_intent: "list_documents_by_counterparty",
|
||||
selected_recipe: "address_documents_by_counterparty_v1",
|
||||
response_type: "FACTUAL_LIST"
|
||||
}
|
||||
},
|
||||
llmPreDecomposeMeta: {
|
||||
mcpDiscoveryRuntimeEntryPoint: {
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
|
||||
entry_status: "bridge_executed",
|
||||
hot_runtime_wired: false,
|
||||
discovery_attempted: true,
|
||||
turn_input: {
|
||||
adapter_status: "ready",
|
||||
should_run_discovery: true,
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "broad_evaluation",
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||
explicit_date_scope: "2020",
|
||||
raw_message: "Собери короткий бизнес-итог по компании и отдельно по СВК."
|
||||
},
|
||||
data_need_graph: {
|
||||
business_fact_family: "business_overview",
|
||||
reason_codes: ["data_need_graph_family_business_overview"]
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
user_facing_response_allowed: true,
|
||||
business_fact_answer_allowed: true,
|
||||
requires_user_clarification: false,
|
||||
planner: {
|
||||
selected_chain_id: "business_overview"
|
||||
},
|
||||
pilot: {
|
||||
pilot_scope: "business_overview_route_template_v1",
|
||||
derived_business_overview: {
|
||||
period_scope: "2020",
|
||||
incoming_customer_revenue: {
|
||||
total_amount_human_ru: "47 628 853,03 руб."
|
||||
},
|
||||
outgoing_supplier_payout: {
|
||||
total_amount_human_ru: "43 763 351,53 руб."
|
||||
},
|
||||
net_amount_human_ru: "3 865 501,50 руб.",
|
||||
net_direction: "net_incoming"
|
||||
}
|
||||
},
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference",
|
||||
headline: "Business overview answer",
|
||||
confirmed_lines: ["Confirmed company business overview facts"],
|
||||
inference_lines: [],
|
||||
unknown_lines: ["Pure profit is not proven"],
|
||||
limitation_lines: [],
|
||||
next_step_line: null
|
||||
}
|
||||
},
|
||||
reason_codes: ["runtime_entry_point_bridge_executed"]
|
||||
}
|
||||
},
|
||||
knownOrganizations: [],
|
||||
activeOrganization: null,
|
||||
sanitizeOutgoingAssistantText: (text) => String(text ?? ""),
|
||||
buildAddressDebugPayload: (debug) => ({ ...(debug as Record<string, unknown>) }),
|
||||
buildAddressFollowupOffer: () => null,
|
||||
mergeKnownOrganizations: (items) => items,
|
||||
toNonEmptyString: (value) => (typeof value === "string" && value.trim() ? value.trim() : null),
|
||||
appendItem: () => {},
|
||||
getSession: () => ({ session_id: "asst-business-overview-address", updated_at: "", items: [], investigation_state: null } as any),
|
||||
persistSession: () => {},
|
||||
cloneConversation: (items) => items,
|
||||
logEvent: () => {},
|
||||
messageIdFactory: () => "msg-business-overview-address",
|
||||
finalizeAddressTurn
|
||||
});
|
||||
|
||||
expect((runtime.response as any).debug).toEqual(
|
||||
expect.objectContaining({
|
||||
mcp_discovery_response_applied: true,
|
||||
detected_intent: "business_overview",
|
||||
selected_recipe: "business_overview",
|
||||
mcp_discovery_effective_response_route: "business_overview"
|
||||
})
|
||||
);
|
||||
expect(String((runtime.response as any).assistant_reply)).toContain("47 628 853,03");
|
||||
});
|
||||
|
||||
it("keeps comparison-scope proof augmentation business-facing for organization clarification", () => {
|
||||
const finalizeAddressTurn = vi.fn((input) => ({
|
||||
response: {
|
||||
ok: true,
|
||||
assistant_reply: input.assistantReply,
|
||||
reply_type: input.replyType,
|
||||
debug: input.debug
|
||||
}
|
||||
}));
|
||||
|
||||
const runtime = runAssistantAddressLaneResponseRuntime({
|
||||
sessionId: "asst-comparison-proof",
|
||||
userMessage: "ООО Альтернатива Плюс",
|
||||
effectiveAddressUserMessage: "ООО Альтернатива Плюс",
|
||||
addressLane: {
|
||||
handled: true,
|
||||
reply_text:
|
||||
"Коротко: по компании Альтернатива Плюс подтвержден company-level денежный срез: входящие 285 819 547,57 руб.",
|
||||
reply_type: "partial_coverage",
|
||||
debug: {}
|
||||
},
|
||||
llmPreDecomposeMeta: {
|
||||
mcpDiscoveryRuntimeEntryPoint: {
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
|
||||
entry_status: "bridge_executed",
|
||||
hot_runtime_wired: true,
|
||||
discovery_attempted: true,
|
||||
turn_input: {
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "business_overview",
|
||||
business_overview_separate_entity_candidates: ["Группа СВК"],
|
||||
explicit_organization_scope: "Альтернатива Плюс"
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "not_applicable",
|
||||
user_facing_response_allowed: false,
|
||||
business_fact_answer_allowed: false,
|
||||
requires_user_clarification: false
|
||||
},
|
||||
reason_codes: []
|
||||
}
|
||||
},
|
||||
knownOrganizations: [],
|
||||
activeOrganization: null,
|
||||
sanitizeOutgoingAssistantText: (text) => String(text ?? ""),
|
||||
buildAddressDebugPayload: () => ({}),
|
||||
buildAddressFollowupOffer: () => null,
|
||||
mergeKnownOrganizations: (items) => items,
|
||||
toNonEmptyString: (value) => (typeof value === "string" && value.trim() ? value.trim() : null),
|
||||
appendItem: () => {},
|
||||
getSession: () =>
|
||||
({
|
||||
session_id: "asst-comparison-proof",
|
||||
updated_at: "",
|
||||
items: [],
|
||||
investigation_state: null,
|
||||
address_navigation_state: {
|
||||
session_context: {
|
||||
comparison_scope: {
|
||||
organization: { label: "Альтернатива Плюс" },
|
||||
counterparty: { label: "Группа СВК" },
|
||||
proof_bundles: {
|
||||
counterparty_value_flow_bundle: {
|
||||
counterparty: "Группа СВК",
|
||||
incoming_customer_revenue: { total_amount_human_ru: "20 653 490 руб." },
|
||||
outgoing_supplier_payout: { total_amount_human_ru: "2 129 651 руб." },
|
||||
net_amount_human_ru: "18 523 839 руб.",
|
||||
net_direction: "net_incoming"
|
||||
},
|
||||
counterparty_document_bundle: {
|
||||
document_count: 19
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}) as any,
|
||||
persistSession: () => {},
|
||||
cloneConversation: (items) => items,
|
||||
logEvent: () => {},
|
||||
messageIdFactory: () => "msg-comparison-proof",
|
||||
finalizeAddressTurn
|
||||
});
|
||||
|
||||
const reply = String((runtime.response as any).assistant_reply);
|
||||
expect(reply).toContain("по компании ООО Альтернатива Плюс");
|
||||
expect(reply).toContain("получили 20 653 490 руб.");
|
||||
expect(reply).toContain("документы: 19");
|
||||
expect(reply).not.toContain("company-level");
|
||||
expect(reply).not.toContain("reusable bundle");
|
||||
expect((runtime.response as any).debug.comparison_scope_response_augmentation_v1).toEqual(
|
||||
expect.objectContaining({
|
||||
applied: true,
|
||||
organization: "ООО Альтернатива Плюс",
|
||||
counterparty: "Группа СВК",
|
||||
document_count: 19
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps debug bounded to shadow contracts when optional enrichment is absent", () => {
|
||||
const runtime = runAssistantAddressLaneResponseRuntime({
|
||||
sessionId: "asst-2",
|
||||
|
||||
@@ -307,6 +307,320 @@ describe("assistant address orchestration runtime adapter", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("recovers business overview proof bundles from session history before MCP discovery", async () => {
|
||||
const valueFlowBundle = {
|
||||
counterparty: "Group SVK",
|
||||
incoming_total: 20653490,
|
||||
outgoing_total: 2129651,
|
||||
net_amount: 18523839
|
||||
};
|
||||
const documentBundle = {
|
||||
counterparty: "Group SVK",
|
||||
document_count: 19
|
||||
};
|
||||
const runMcpDiscoveryRuntimeEntryPoint = vi.fn(async () => ({
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
|
||||
entry_status: "bridge_executed",
|
||||
hot_runtime_wired: false,
|
||||
discovery_attempted: true
|
||||
}));
|
||||
const input = buildInput({
|
||||
userMessage: "Alt Plus",
|
||||
sessionItems: [
|
||||
{
|
||||
role: "assistant",
|
||||
debug: {
|
||||
assistant_mcp_discovery_entry_point_v1: {
|
||||
turn_input: {
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "broad_evaluation",
|
||||
business_overview_separate_entity_candidates: ["Group SVK"],
|
||||
previous_counterparty_value_flow_bundle: valueFlowBundle,
|
||||
previous_counterparty_document_bundle: documentBundle,
|
||||
metadata_scope_hint: "Group SVK"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
runAddressLlmPreDecompose: vi.fn(async () => ({
|
||||
attempted: true,
|
||||
applied: false,
|
||||
effectiveMessage: "Alt Plus",
|
||||
reason: "raw_kept",
|
||||
predecomposeContract: {
|
||||
mode: "unsupported",
|
||||
intent: "unknown",
|
||||
entities: { organization: "Alt Plus" },
|
||||
semantics: { anchor_kind: "organization", anchor_value: "Alt Plus" }
|
||||
}
|
||||
})),
|
||||
resolveAddressFollowupCarryoverContext: vi.fn(() => ({
|
||||
followupContext: {
|
||||
previous_intent: "business_overview",
|
||||
target_intent: "business_overview",
|
||||
previous_discovery_pilot_scope: "business_overview_route_template_v1",
|
||||
previous_discovery_loop_status: "awaiting_clarification",
|
||||
previous_discovery_loop_selected_chain_id: "business_overview",
|
||||
previous_discovery_loop_metadata_scope_hint: "Group SVK",
|
||||
previous_anchor_type: "counterparty",
|
||||
previous_anchor_value: "Group SVK",
|
||||
previous_filters: {}
|
||||
}
|
||||
})),
|
||||
resolveAssistantOrchestrationDecision: vi.fn(() => ({
|
||||
runAddressLane: true,
|
||||
livingMode: "address_data",
|
||||
livingReason: "address_lane_triggered",
|
||||
toolGateDecision: "run_address_lane",
|
||||
toolGateReason: "followup_context_detected",
|
||||
orchestrationContract: {
|
||||
schema_version: "assistant_orchestration_contract_v1",
|
||||
assistant_turn_meaning: {
|
||||
schema_version: "assistant_turn_meaning_v1",
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "broad_evaluation",
|
||||
unsupported_but_understood_family: "broad_business_evaluation"
|
||||
}
|
||||
}
|
||||
})),
|
||||
runMcpDiscoveryRuntimeEntryPoint
|
||||
});
|
||||
|
||||
await buildAssistantAddressOrchestrationRuntime(input);
|
||||
|
||||
expect(runMcpDiscoveryRuntimeEntryPoint).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
followupContext: expect.objectContaining({
|
||||
previous_discovery_bidirectional_value_flow: valueFlowBundle,
|
||||
previous_discovery_document_summary: documentBundle
|
||||
})
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("recovers business overview proof bundles from previous assistant summary text", async () => {
|
||||
const runMcpDiscoveryRuntimeEntryPoint = vi.fn(async () => ({
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
|
||||
entry_status: "bridge_executed",
|
||||
hot_runtime_wired: false,
|
||||
discovery_attempted: true
|
||||
}));
|
||||
const input = buildInput({
|
||||
userMessage: "Alt Plus",
|
||||
sessionItems: [
|
||||
{
|
||||
role: "assistant",
|
||||
text:
|
||||
"\u041e\u0442\u0434\u0435\u043b\u044c\u043d\u043e \u043f\u043e \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0443 Group SVK: " +
|
||||
"\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438 20 653 490 \u0440\u0443\u0431., " +
|
||||
"\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438 2 129 651 \u0440\u0443\u0431., " +
|
||||
"\u0440\u0430\u0441\u0447\u0435\u0442\u043d\u043e\u0435 \u043d\u0435\u0442\u0442\u043e \u0432 \u043d\u0430\u0448\u0443 \u0441\u0442\u043e\u0440\u043e\u043d\u0443 18 523 839 \u0440\u0443\u0431. " +
|
||||
"\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b \u043f\u043e \u0446\u0435\u043f\u043e\u0447\u043a\u0435: \u043d\u0430\u0439\u0434\u0435\u043d\u043e 19."
|
||||
}
|
||||
],
|
||||
runAddressLlmPreDecompose: vi.fn(async () => ({
|
||||
attempted: true,
|
||||
applied: false,
|
||||
effectiveMessage: "Alt Plus",
|
||||
reason: "raw_kept",
|
||||
predecomposeContract: {
|
||||
mode: "unsupported",
|
||||
intent: "unknown",
|
||||
entities: { organization: "Alt Plus" }
|
||||
}
|
||||
})),
|
||||
resolveAddressFollowupCarryoverContext: vi.fn(() => ({
|
||||
followupContext: {
|
||||
previous_intent: "business_overview",
|
||||
target_intent: "business_overview",
|
||||
previous_discovery_pilot_scope: "business_overview_route_template_v1",
|
||||
previous_discovery_loop_selected_chain_id: "business_overview",
|
||||
previous_discovery_loop_metadata_scope_hint: "Group SVK",
|
||||
previous_anchor_type: "counterparty",
|
||||
previous_anchor_value: "Group SVK",
|
||||
previous_filters: {}
|
||||
}
|
||||
})),
|
||||
resolveAssistantOrchestrationDecision: vi.fn(() => ({
|
||||
runAddressLane: true,
|
||||
livingMode: "address_data",
|
||||
livingReason: "address_lane_triggered",
|
||||
toolGateDecision: "run_address_lane",
|
||||
toolGateReason: "followup_context_detected",
|
||||
orchestrationContract: {
|
||||
schema_version: "assistant_orchestration_contract_v1",
|
||||
assistant_turn_meaning: {
|
||||
schema_version: "assistant_turn_meaning_v1",
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "broad_evaluation"
|
||||
}
|
||||
}
|
||||
})),
|
||||
runMcpDiscoveryRuntimeEntryPoint
|
||||
});
|
||||
|
||||
await buildAssistantAddressOrchestrationRuntime(input);
|
||||
|
||||
expect(runMcpDiscoveryRuntimeEntryPoint).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
followupContext: expect.objectContaining({
|
||||
previous_discovery_bidirectional_value_flow: expect.objectContaining({
|
||||
counterparty: "Group SVK",
|
||||
incoming_customer_revenue: { total_amount_human_ru: "20 653 490 \u0440\u0443\u0431." },
|
||||
outgoing_supplier_payout: { total_amount_human_ru: "2 129 651 \u0440\u0443\u0431." },
|
||||
net_amount_human_ru: "18 523 839 \u0440\u0443\u0431."
|
||||
}),
|
||||
previous_discovery_document_summary: expect.objectContaining({
|
||||
counterparty: "Group SVK",
|
||||
document_count: 19
|
||||
})
|
||||
})
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("recovers business overview proof bundles from navigation comparison state", async () => {
|
||||
const valueFlowBundle = {
|
||||
counterparty: "Group SVK",
|
||||
incoming_customer_revenue: { total_amount_human_ru: "20 653 490 руб." },
|
||||
outgoing_supplier_payout: { total_amount_human_ru: "2 129 651 руб." },
|
||||
net_amount_human_ru: "18 523 839 руб."
|
||||
};
|
||||
const documentBundle = { counterparty: "Group SVK", document_count: 19 };
|
||||
const runMcpDiscoveryRuntimeEntryPoint = vi.fn(async () => ({
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
|
||||
entry_status: "bridge_executed",
|
||||
hot_runtime_wired: false,
|
||||
discovery_attempted: true
|
||||
}));
|
||||
const input = buildInput({
|
||||
userMessage: "Alt Plus",
|
||||
sessionAddressNavigationState: {
|
||||
session_context: {
|
||||
comparison_scope: {
|
||||
organization: null,
|
||||
counterparty: { label: "Group SVK" },
|
||||
proof_bundles: {
|
||||
counterparty_value_flow_bundle: valueFlowBundle,
|
||||
counterparty_document_bundle: documentBundle
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
runAddressLlmPreDecompose: vi.fn(async () => ({
|
||||
attempted: true,
|
||||
applied: false,
|
||||
effectiveMessage: "Alt Plus",
|
||||
reason: "raw_kept",
|
||||
predecomposeContract: {
|
||||
mode: "unsupported",
|
||||
intent: "unknown",
|
||||
entities: { organization: "Alt Plus" }
|
||||
}
|
||||
})),
|
||||
resolveAddressFollowupCarryoverContext: vi.fn(() => ({ followupContext: null })),
|
||||
resolveAssistantOrchestrationDecision: vi.fn(() => ({
|
||||
runAddressLane: true,
|
||||
livingMode: "address_data",
|
||||
livingReason: "address_lane_triggered",
|
||||
toolGateDecision: "run_address_lane",
|
||||
toolGateReason: "followup_context_detected",
|
||||
orchestrationContract: {
|
||||
schema_version: "assistant_orchestration_contract_v1",
|
||||
assistant_turn_meaning: {
|
||||
schema_version: "assistant_turn_meaning_v1",
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "broad_evaluation"
|
||||
}
|
||||
}
|
||||
})),
|
||||
runMcpDiscoveryRuntimeEntryPoint
|
||||
});
|
||||
|
||||
await buildAssistantAddressOrchestrationRuntime(input);
|
||||
|
||||
expect(runMcpDiscoveryRuntimeEntryPoint).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
followupContext: expect.objectContaining({
|
||||
previous_discovery_loop_selected_chain_id: "business_overview",
|
||||
previous_discovery_loop_pending_axes: ["organization"],
|
||||
previous_discovery_loop_metadata_scope_hint: "Group SVK",
|
||||
previous_discovery_bidirectional_value_flow: valueFlowBundle,
|
||||
previous_discovery_document_summary: documentBundle
|
||||
})
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("does not infer a counterparty proof bundle from company-only overview totals", async () => {
|
||||
const runMcpDiscoveryRuntimeEntryPoint = vi.fn(async () => ({
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
|
||||
entry_status: "bridge_executed",
|
||||
hot_runtime_wired: false,
|
||||
discovery_attempted: true
|
||||
}));
|
||||
const input = buildInput({
|
||||
userMessage: "who brought money",
|
||||
sessionItems: [
|
||||
{
|
||||
role: "assistant",
|
||||
text:
|
||||
"Деньги: входящие 47 628 853,03 руб., исходящие 43 763 351,53 руб., расчетное операционное нетто 3 865 501,50 руб.\n" +
|
||||
"НДС: продажи 2 002 138,93 руб., покупки 1 784 850,48 руб., НДС к уплате 217 288,45 руб."
|
||||
}
|
||||
],
|
||||
runAddressLlmPreDecompose: vi.fn(async () => ({
|
||||
attempted: true,
|
||||
applied: false,
|
||||
effectiveMessage: "who brought money",
|
||||
reason: "raw_kept",
|
||||
predecomposeContract: {
|
||||
mode: "unsupported",
|
||||
intent: "unknown",
|
||||
entities: { organization: "Alt Plus" }
|
||||
}
|
||||
})),
|
||||
resolveAddressFollowupCarryoverContext: vi.fn(() => ({
|
||||
followupContext: {
|
||||
previous_intent: "business_overview",
|
||||
target_intent: "business_overview",
|
||||
previous_discovery_pilot_scope: "business_overview_route_template_v1",
|
||||
previous_discovery_loop_selected_chain_id: "business_overview",
|
||||
previous_filters: { organization: "Alt Plus" }
|
||||
}
|
||||
})),
|
||||
resolveAssistantOrchestrationDecision: vi.fn(() => ({
|
||||
runAddressLane: true,
|
||||
livingMode: "address_data",
|
||||
livingReason: "address_lane_triggered",
|
||||
toolGateDecision: "run_address_lane",
|
||||
toolGateReason: "followup_context_detected",
|
||||
orchestrationContract: {
|
||||
schema_version: "assistant_orchestration_contract_v1",
|
||||
assistant_turn_meaning: {
|
||||
schema_version: "assistant_turn_meaning_v1",
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "broad_evaluation"
|
||||
}
|
||||
}
|
||||
})),
|
||||
runMcpDiscoveryRuntimeEntryPoint
|
||||
});
|
||||
|
||||
await buildAssistantAddressOrchestrationRuntime(input);
|
||||
|
||||
const call = runMcpDiscoveryRuntimeEntryPoint.mock.calls[0]?.[0] as any;
|
||||
expect(call.followupContext.previous_discovery_bidirectional_value_flow).toBeUndefined();
|
||||
expect(call.followupContext.previous_discovery_document_summary).toBeUndefined();
|
||||
});
|
||||
|
||||
it("passes grounded discovery follow-up carryover into MCP discovery entry point for a short year switch", async () => {
|
||||
const runMcpDiscoveryRuntimeEntryPoint = vi.fn(async () => ({
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
|
||||
@@ -176,6 +176,31 @@ describe("assistant living chat runtime adapter", () => {
|
||||
expect(executeLlmChat).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("handles first-turn smalltalk deterministically without relying on LLM", async () => {
|
||||
const resolveDataScopeProbe = vi.fn(async () => ({
|
||||
status: "resolved",
|
||||
channel: "default",
|
||||
organizations: [],
|
||||
error: null
|
||||
}));
|
||||
const input = buildRuntimeInput({
|
||||
userMessage: "приветик - че как там дела",
|
||||
resolveDataScopeProbe,
|
||||
buildAssistantProactiveOrganizationOfferReply: () => ""
|
||||
});
|
||||
|
||||
const output = await runAssistantLivingChatRuntime(input);
|
||||
|
||||
expect(output.handled).toBe(true);
|
||||
expect(output.chatText).toContain("Привет! Всё нормально.");
|
||||
expect(output.chatText).not.toContain("llm-text");
|
||||
expect(output.debug?.living_chat_response_source).toBe("deterministic_smalltalk");
|
||||
expect(output.debug?.living_chat_proactive_scope_offer_applied).toBe(false);
|
||||
expect(output.debug?.living_chat_data_scope_probe_org_count).toBe(0);
|
||||
expect(input.__spies.executeLlmChat).not.toHaveBeenCalled();
|
||||
expect(resolveDataScopeProbe).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("builds deterministic broad business evaluation summary from grounded continuity instead of replaying lifecycle noise", async () => {
|
||||
const executeLlmChat = vi.fn(async () => "raw-llm");
|
||||
const input = buildRuntimeInput({
|
||||
@@ -438,6 +463,7 @@ describe("assistant living chat runtime adapter", () => {
|
||||
"ООО Лайсвуд",
|
||||
"РАЙМ"
|
||||
]);
|
||||
expect(input.__spies.executeLlmChat).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not add proactive organization offer after the session already has assistant context", async () => {
|
||||
|
||||
@@ -475,6 +475,7 @@ describe("assistant MCP discovery response candidate", () => {
|
||||
effective_message:
|
||||
"Дать краткий обзор ООО Альтернатива Плюс за 2020: входящие, исходящие, нетто и банковскую границу.",
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс"
|
||||
,asked_action_family: "broad_evaluation"
|
||||
},
|
||||
data_need_graph: {
|
||||
business_fact_family: "business_overview",
|
||||
@@ -778,6 +779,199 @@ describe("assistant MCP discovery response candidate", () => {
|
||||
expect(candidate.reply_text).not.toContain("wide overview");
|
||||
});
|
||||
|
||||
it("keeps broad overview layers when direct-money reason is present", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
turn_input: {
|
||||
adapter_status: "ready",
|
||||
turn_meaning_ref: {
|
||||
raw_message:
|
||||
"Дай взрослый бизнес-обзор ООО Альтернатива Плюс за 2020 год: деньги, нетто, НДС, долги, склад, клиенты, поставщики и что нельзя утверждать.",
|
||||
asked_action_family: "broad_evaluation",
|
||||
unsupported_but_understood_family: "broad_business_evaluation",
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||
explicit_date_scope: "2020"
|
||||
},
|
||||
data_need_graph: {
|
||||
business_fact_family: "business_overview",
|
||||
ranking_need: null,
|
||||
reason_codes: [
|
||||
"data_need_graph_family_business_overview",
|
||||
"data_need_graph_business_overview_direct_money_answer"
|
||||
]
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
user_facing_response_allowed: true,
|
||||
business_fact_answer_allowed: true,
|
||||
requires_user_clarification: false,
|
||||
pilot: {
|
||||
pilot_scope: "business_overview_route_template_v1",
|
||||
derived_business_overview: {
|
||||
period_scope: "2020",
|
||||
incoming_customer_revenue: {
|
||||
total_amount_human_ru: "47 628 853,03 руб."
|
||||
},
|
||||
outgoing_supplier_payout: {
|
||||
total_amount_human_ru: "43 763 351,53 руб."
|
||||
},
|
||||
net_amount_human_ru: "3 865 501,50 руб.",
|
||||
net_direction: "net_incoming",
|
||||
top_customers: [
|
||||
{
|
||||
axis_value: "СБЕРБАНК, ПАО",
|
||||
total_amount_human_ru: "12 792 194,31 руб.",
|
||||
is_likely_financial_institution: true
|
||||
},
|
||||
{
|
||||
axis_value: "Группа СВК",
|
||||
total_amount_human_ru: "12 093 465 руб."
|
||||
}
|
||||
],
|
||||
top_suppliers: [
|
||||
{
|
||||
axis_value: "Департамент капитального ремонта города Москвы",
|
||||
total_amount_human_ru: "9 612 904,90 руб."
|
||||
}
|
||||
],
|
||||
tax_position: {
|
||||
sales_vat_amount_human_ru: "2 717 288,45 руб.",
|
||||
purchase_vat_amount_human_ru: "2 500 000 руб.",
|
||||
net_vat_amount_human_ru: "217 288,45 руб.",
|
||||
net_vat_direction: "vat_to_pay"
|
||||
},
|
||||
debt_position: {
|
||||
receivables: { total_amount_human_ru: "25 000 000 руб." },
|
||||
payables: { total_amount_human_ru: "3 318 824,40 руб." },
|
||||
net_debt_position_amount_human_ru: "21 681 175,60 руб.",
|
||||
net_debt_position_direction: "net_receivable"
|
||||
},
|
||||
inventory_position: {
|
||||
total_amount_human_ru: "716 418,33 руб.",
|
||||
rows_matched: 34
|
||||
}
|
||||
}
|
||||
},
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference",
|
||||
headline: "wide overview should not leak",
|
||||
confirmed_lines: [],
|
||||
inference_lines: [],
|
||||
unknown_lines: [],
|
||||
limitation_lines: [],
|
||||
next_step_line: null
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
expect(candidate.reply_text).toContain("Что видно:");
|
||||
expect(candidate.reply_text).toContain("НДС:");
|
||||
expect(candidate.reply_text).toContain("Долги:");
|
||||
expect(candidate.reply_text).toContain("Склад:");
|
||||
expect(candidate.reply_text).toContain("Группа СВК");
|
||||
expect(candidate.reply_text).not.toContain("по деньгам плюс");
|
||||
expect(candidate.reply_text).not.toContain("wide overview");
|
||||
});
|
||||
|
||||
it("keeps counterparty leaders for money followups instead of compact cashflow", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
turn_input: {
|
||||
adapter_status: "ready",
|
||||
turn_meaning_ref: {
|
||||
raw_message:
|
||||
"Раскрой деньги подробнее: сколько получили, сколько заплатили, какой чистый денежный поток, кто главный клиент и главный поставщик в 2020.",
|
||||
asked_action_family: "broad_evaluation",
|
||||
unsupported_but_understood_family: "broad_business_evaluation",
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||
explicit_date_scope: "2020"
|
||||
},
|
||||
data_need_graph: {
|
||||
business_fact_family: "business_overview",
|
||||
ranking_need: null,
|
||||
reason_codes: [
|
||||
"data_need_graph_family_business_overview",
|
||||
"data_need_graph_business_overview_direct_money_answer"
|
||||
]
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
user_facing_response_allowed: true,
|
||||
business_fact_answer_allowed: true,
|
||||
requires_user_clarification: false,
|
||||
pilot: {
|
||||
pilot_scope: "business_overview_route_template_v1",
|
||||
derived_business_overview: {
|
||||
period_scope: "2020",
|
||||
incoming_customer_revenue: {
|
||||
total_amount_human_ru: "47 628 853,03 руб."
|
||||
},
|
||||
outgoing_supplier_payout: {
|
||||
total_amount_human_ru: "43 763 351,53 руб."
|
||||
},
|
||||
net_amount_human_ru: "3 865 501,50 руб.",
|
||||
net_direction: "net_incoming",
|
||||
top_customers: [
|
||||
{
|
||||
axis_value: "СБЕРБАНК, ПАО",
|
||||
total_amount_human_ru: "12 792 194,31 руб.",
|
||||
is_likely_financial_institution: true
|
||||
},
|
||||
{
|
||||
axis_value: "Группа СВК",
|
||||
total_amount_human_ru: "12 093 465 руб."
|
||||
}
|
||||
],
|
||||
top_suppliers: [
|
||||
{
|
||||
axis_value: "Департамент капитального ремонта города Москвы",
|
||||
total_amount_human_ru: "9 612 904,90 руб."
|
||||
}
|
||||
],
|
||||
tax_position: {
|
||||
net_vat_amount_human_ru: "217 288,45 руб.",
|
||||
net_vat_direction: "vat_to_pay"
|
||||
},
|
||||
debt_position: {
|
||||
receivables: { total_amount_human_ru: "25 000 000 руб." },
|
||||
payables: { total_amount_human_ru: "3 318 824,40 руб." },
|
||||
net_debt_position_amount_human_ru: "21 681 175,60 руб.",
|
||||
net_debt_position_direction: "net_receivable"
|
||||
},
|
||||
inventory_position: {
|
||||
total_amount_human_ru: "716 418,33 руб.",
|
||||
rows_matched: 34
|
||||
}
|
||||
}
|
||||
},
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference",
|
||||
headline: "wide overview should not leak",
|
||||
confirmed_lines: [],
|
||||
inference_lines: [],
|
||||
unknown_lines: [],
|
||||
limitation_lines: [],
|
||||
next_step_line: null
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
expect(candidate.reply_text).toContain("47 628 853,03");
|
||||
expect(candidate.reply_text).toContain("43 763 351,53");
|
||||
expect(candidate.reply_text).toContain("3 865 501,50");
|
||||
expect(candidate.reply_text).toContain("СБЕРБАНК, ПАО");
|
||||
expect(candidate.reply_text).toContain("Группа СВК");
|
||||
expect(candidate.reply_text).toContain("Департамент капитального ремонта города Москвы");
|
||||
expect(candidate.reply_text).not.toContain("по деньгам плюс");
|
||||
expect(candidate.reply_text).not.toContain("НДС:");
|
||||
expect(candidate.reply_text).not.toContain("Склад:");
|
||||
expect(candidate.reply_text).not.toContain("wide overview");
|
||||
});
|
||||
|
||||
it("labels organization-scoped bidirectional value-flow continuations as company scope", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
@@ -902,6 +1096,83 @@ describe("assistant MCP discovery response candidate", () => {
|
||||
expect(candidate.reply_text).not.toContain("Самый крупный подтвержденный клиент");
|
||||
});
|
||||
|
||||
it("answers colloquial money leader follow-ups with leaders first", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
turn_input: {
|
||||
adapter_status: "ready",
|
||||
turn_meaning_ref: {
|
||||
raw_message:
|
||||
"А по деньгам в этом же году кто больше всего занес и кому больше всего ушло? Банк не называй обычным клиентом без оговорки.",
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "broad_evaluation",
|
||||
explicit_organization_scope: "ООО Альтернатива Плюс",
|
||||
explicit_date_scope: "2020"
|
||||
},
|
||||
data_need_graph: {
|
||||
business_fact_family: "business_overview",
|
||||
ranking_need: "top_desc",
|
||||
reason_codes: [
|
||||
"data_need_graph_family_business_overview",
|
||||
"data_need_graph_business_overview_direct_money_answer"
|
||||
]
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
user_facing_response_allowed: true,
|
||||
business_fact_answer_allowed: true,
|
||||
requires_user_clarification: false,
|
||||
pilot: {
|
||||
pilot_scope: "business_overview_route_template_v1",
|
||||
derived_business_overview: {
|
||||
period_scope: "2020",
|
||||
incoming_customer_revenue: { total_amount_human_ru: "47 628 853,03 руб." },
|
||||
outgoing_supplier_payout: { total_amount_human_ru: "43 763 351,53 руб." },
|
||||
net_amount_human_ru: "3 865 501,50 руб.",
|
||||
net_direction: "net_incoming",
|
||||
top_customers: [
|
||||
{
|
||||
axis_value: "СБЕРБАНК, ПАО",
|
||||
total_amount_human_ru: "12 792 194,31 руб.",
|
||||
counterparty_role_hint: "financial_institution"
|
||||
},
|
||||
{
|
||||
axis_value: "Группа СВК",
|
||||
total_amount_human_ru: "12 093 465 руб.",
|
||||
counterparty_role_hint: "ordinary_counterparty"
|
||||
}
|
||||
],
|
||||
top_suppliers: [
|
||||
{
|
||||
axis_value: "Департамент капитального ремонта города Москвы.",
|
||||
total_amount_human_ru: "9 612 904,90 руб."
|
||||
}
|
||||
],
|
||||
yearly_breakdown: []
|
||||
}
|
||||
},
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference",
|
||||
headline: "Company summary.",
|
||||
confirmed_lines: [],
|
||||
inference_lines: [],
|
||||
unknown_lines: [],
|
||||
limitation_lines: [],
|
||||
next_step_line: null
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const firstLine = candidate.reply_text?.split("\n")[0] ?? "";
|
||||
expect(firstLine).toContain("больше всего занес СБЕРБАНК, ПАО");
|
||||
expect(firstLine).toContain("не называю его обычной клиентской выручкой");
|
||||
expect(firstLine).toContain("крупнейший небанковский входящий контрагент: Группа СВК");
|
||||
expect(firstLine).toContain("больше всего ушло Департамент капитального ремонта города Москвы.");
|
||||
expect(firstLine).not.toContain("выглядит как бизнес");
|
||||
});
|
||||
|
||||
it("mentions separate counterparty scope in company plus counterparty business summaries", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
@@ -1107,6 +1378,128 @@ describe("assistant MCP discovery response candidate", () => {
|
||||
expect(candidate.reply_text).toContain("ранее подтвержденный контрагентский срез");
|
||||
});
|
||||
|
||||
it("answers selected counterparty boundary summaries from previous bundles without asking for organization", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
turn_input: {
|
||||
adapter_status: "ready",
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "broad_evaluation",
|
||||
business_overview_separate_entity_candidates: ["Группа СВК"],
|
||||
previous_counterparty_value_flow_bundle: {
|
||||
counterparty: "Группа СВК",
|
||||
incoming_customer_revenue: {
|
||||
total_amount_human_ru: "20 653 490 руб.",
|
||||
rows_with_amount: 26,
|
||||
rows_matched: 26
|
||||
},
|
||||
outgoing_supplier_payout: {
|
||||
total_amount_human_ru: "2 129 651 руб.",
|
||||
rows_with_amount: 1,
|
||||
rows_matched: 1
|
||||
},
|
||||
net_amount_human_ru: "18 523 839 руб.",
|
||||
net_direction: "net_incoming"
|
||||
},
|
||||
previous_counterparty_document_bundle: {
|
||||
counterparty: "Группа СВК",
|
||||
document_count: 19
|
||||
}
|
||||
},
|
||||
data_need_graph: {
|
||||
business_fact_family: "business_overview",
|
||||
subject_candidates: [],
|
||||
clarification_gaps: ["organization"],
|
||||
reason_codes: ["data_need_graph_family_business_overview", "data_need_graph_has_clarification_gaps"]
|
||||
}
|
||||
},
|
||||
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: "Нужно уточнить контекст перед поиском в 1С.",
|
||||
confirmed_lines: [],
|
||||
inference_lines: [],
|
||||
unknown_lines: [],
|
||||
limitation_lines: [],
|
||||
next_step_line: "Уточните организацию."
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
expect(candidate.candidate_status).toBe("clarification_candidate");
|
||||
expect(candidate.reply_text).toContain("уточните, по какой компании");
|
||||
expect(candidate.reply_text).toContain("Группа СВК");
|
||||
expect(candidate.reply_text).toContain("20 653 490 руб.");
|
||||
expect(candidate.reply_text).toContain("документы по цепочке: найдено 19");
|
||||
expect(candidate.reply_text).toContain("Нельзя утверждать");
|
||||
expect(candidate.reply_text).not.toContain("перед поиском");
|
||||
});
|
||||
|
||||
it("keeps organization clarification boundary summaries compact", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
turn_input: {
|
||||
adapter_status: "ready",
|
||||
turn_meaning_ref: {
|
||||
raw_message: "ООО Альтернатива Плюс",
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "broad_evaluation",
|
||||
business_overview_separate_entity_candidates: ["Группа СВК"],
|
||||
explicit_organization_scope: "Альтернатива Плюс"
|
||||
},
|
||||
data_need_graph: {
|
||||
business_fact_family: "business_overview",
|
||||
subject_candidates: [],
|
||||
metadata_scope_hint: "Группа СВК",
|
||||
reason_codes: ["data_need_graph_family_business_overview"]
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
user_facing_response_allowed: true,
|
||||
business_fact_answer_allowed: true,
|
||||
requires_user_clarification: false,
|
||||
pilot: {
|
||||
pilot_scope: "business_overview_route_template_v1",
|
||||
derived_business_overview: {
|
||||
period_scope: "all_time",
|
||||
incoming_customer_revenue: { total_amount_human_ru: "285 819 547,57 руб." },
|
||||
outgoing_supplier_payout: { total_amount_human_ru: "137 963 720,37 руб." },
|
||||
net_amount_human_ru: "147 855 827,20 руб.",
|
||||
net_direction: "net_incoming",
|
||||
top_customers: [{ axis_value: "Комитет государственных услуг г. Москвы", total_amount_human_ru: "133 839 880,82 руб." }],
|
||||
top_suppliers: [{ axis_value: "ГТК-Интер, ООО", total_amount_human_ru: "44 943 460 руб." }]
|
||||
}
|
||||
},
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference",
|
||||
headline: "Company summary.",
|
||||
confirmed_lines: [],
|
||||
inference_lines: [],
|
||||
unknown_lines: [],
|
||||
limitation_lines: [],
|
||||
next_step_line: null
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const lines = candidate.reply_text?.split("\n").filter(Boolean) ?? [];
|
||||
expect(lines.length).toBeLessThanOrEqual(3);
|
||||
expect(lines[0]).toContain("по компании Альтернатива Плюс");
|
||||
expect(lines[0]).toContain("285 819 547,57");
|
||||
expect(candidate.reply_text).toContain("Отдельно по выбранному контрагенту Группа СВК");
|
||||
expect(candidate.reply_text).toContain("Нельзя утверждать");
|
||||
expect(candidate.reply_text).not.toContain("Метод:");
|
||||
expect(candidate.reply_text).not.toContain("Для ответа именно про чистую прибыль");
|
||||
});
|
||||
|
||||
it("localizes value-flow evidence without leaking pilot mechanics", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
|
||||
@@ -310,6 +310,67 @@ describe("assistant MCP discovery response policy", () => {
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_exact_matched_factual_address_reply");
|
||||
});
|
||||
|
||||
it("overrides exact document-list replies for explicit counterparty received-paid-net questions", () => {
|
||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||
currentReply: "Контрагент: Группа СВК. Найдено документов: 19.",
|
||||
currentReplySource: "address_query_runtime_v1",
|
||||
currentReplyType: "factual",
|
||||
addressRuntimeMeta: {
|
||||
detected_intent: "list_documents_by_counterparty",
|
||||
selected_recipe: "address_documents_by_counterparty_v1",
|
||||
mcp_call_status: "matched_non_empty",
|
||||
truth_mode: "confirmed",
|
||||
capability_binding_status: "bound",
|
||||
capability_binding_violations: [],
|
||||
answer_shape_contract: {
|
||||
reply_type: "factual",
|
||||
capability_contract_id: "documents_drilldown"
|
||||
},
|
||||
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: ["Группа СВК"],
|
||||
reason_codes: ["data_need_graph_built", "data_need_graph_bidirectional_value_flow"]
|
||||
},
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "net_value_flow",
|
||||
explicit_entity_candidates: ["Группа СВК"],
|
||||
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting"
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
user_facing_response_allowed: true,
|
||||
business_fact_answer_allowed: true,
|
||||
requires_user_clarification: false,
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference",
|
||||
headline: "Группа СВК: получили 20 653 490 руб., заплатили 2 129 651 руб.; нетто 18 523 839 руб. в нашу сторону.",
|
||||
confirmed_lines: ["получили 20 653 490 руб.; заплатили 2 129 651 руб.; нетто 18 523 839 руб."],
|
||||
inference_lines: [],
|
||||
unknown_lines: [],
|
||||
limitation_lines: ["Это денежный срез, не бухгалтерская прибыль."],
|
||||
next_step_line: null
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.applied).toBe(true);
|
||||
expect(result.decision).toBe("apply_candidate");
|
||||
expect(result.reply_source).toBe("mcp_discovery_response_candidate_guarded");
|
||||
expect(result.reply_text).toContain("20 653 490");
|
||||
expect(result.reply_text).toContain("2 129 651");
|
||||
expect(result.reason_codes).toContain("mcp_discovery_response_policy_value_flow_action_conflict_allows_candidate_override");
|
||||
expect(result.reason_codes).toContain("mcp_discovery_response_policy_semantic_conflict_allows_candidate_override");
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_exact_document_list_address_reply");
|
||||
});
|
||||
|
||||
it("keeps exact matched inventory address replies over stale metadata discovery candidates", () => {
|
||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||
currentReply: "По товару Шкаф картотечный 1000*400*2100 цепочка поставки и продажи подтверждена.",
|
||||
|
||||
@@ -328,8 +328,17 @@ describe("assistant MCP discovery runtime bridge", () => {
|
||||
});
|
||||
|
||||
expect(result.bridge_status).toBe("answer_draft_ready");
|
||||
expect(result.hot_runtime_wired).toBe(true);
|
||||
expect(result.business_fact_answer_allowed).toBe(true);
|
||||
expect(result.planner.selected_chain_id).toBe("value_flow_comparison");
|
||||
expect(result.hot_runtime_wired).toBe(true);
|
||||
expect(result.execution_handoff).toMatchObject({
|
||||
handoff_status: "ready_for_guarded_response",
|
||||
selected_chain_id: "value_flow_comparison",
|
||||
allowed_hot_chain: true,
|
||||
can_use_guarded_response: true
|
||||
});
|
||||
expect(result.reason_codes).toContain("runtime_bridge_wired_to_guarded_hot_assistant_answer");
|
||||
expect(result.pilot.derived_bidirectional_value_flow).toMatchObject({
|
||||
period_scope: "2020",
|
||||
incoming_customer_revenue: {
|
||||
@@ -389,6 +398,14 @@ describe("assistant MCP discovery runtime bridge", () => {
|
||||
expect(result.requires_user_clarification).toBe(false);
|
||||
expect(result.business_fact_answer_allowed).toBe(true);
|
||||
expect(result.planner.selected_chain_id).toBe("value_flow_comparison");
|
||||
expect(result.hot_runtime_wired).toBe(true);
|
||||
expect(result.execution_handoff).toMatchObject({
|
||||
handoff_status: "ready_for_guarded_response",
|
||||
selected_chain_id: "value_flow_comparison",
|
||||
allowed_hot_chain: true,
|
||||
can_use_guarded_response: true
|
||||
});
|
||||
expect(result.reason_codes).toContain("runtime_bridge_wired_to_guarded_hot_assistant_answer");
|
||||
expect(result.planner.required_axes).toContain("all_time_scope");
|
||||
expect(result.pilot.mcp_execution_performed).toBe(true);
|
||||
expect(result.pilot.derived_bidirectional_value_flow).toMatchObject({
|
||||
@@ -581,6 +598,11 @@ describe("assistant MCP discovery runtime bridge", () => {
|
||||
executable_now: true,
|
||||
enablement_reason: null
|
||||
});
|
||||
expect(result.execution_handoff).toMatchObject({
|
||||
handoff_status: "ready_for_guarded_response",
|
||||
allowed_hot_chain: true,
|
||||
can_use_guarded_response: true
|
||||
});
|
||||
expect(result.pilot.derived_business_overview?.inventory_quality_events?.evidence_status).toBe(
|
||||
"reviewed_no_quality_events_found"
|
||||
);
|
||||
|
||||
@@ -166,6 +166,34 @@ describe("assistant MCP discovery turn input adapter", () => {
|
||||
expect(result.reason_codes).toContain("mcp_discovery_bidirectional_value_flow_signal_detected");
|
||||
});
|
||||
|
||||
it("normalizes role-prefixed counterparty carryover before document discovery", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage: "Покажи документы по этой цепочке и не смешивай Группа СВК с организацией ООО Альтернатива Плюс.",
|
||||
assistantTurnMeaning: {
|
||||
asked_domain_family: "counterparty",
|
||||
asked_action_family: "list_documents",
|
||||
explicit_intent_candidate: "list_documents_by_counterparty"
|
||||
},
|
||||
predecomposeContract: {
|
||||
entities: { counterparty: null, organization: null },
|
||||
period: { scope: "unspecified", period_from: null, period_to: null }
|
||||
},
|
||||
followupContext: {
|
||||
previous_intent: "customer_revenue_and_payments",
|
||||
target_intent: "list_documents_by_counterparty",
|
||||
previous_discovery_pilot_scope: "counterparty_bidirectional_value_flow_query_movements_v1",
|
||||
previous_discovery_loop_metadata_scope_hint: "контрагенту группа свк",
|
||||
previous_anchor_type: "counterparty",
|
||||
previous_anchor_value: "контрагенту группа свк",
|
||||
previous_filters: { counterparty: "контрагенту группа свк" }
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.turn_meaning_ref?.explicit_entity_candidates).toEqual(["группа свк"]);
|
||||
expect(result.turn_meaning_ref?.metadata_scope_hint).toBe("группа свк");
|
||||
expect(result.data_need_graph?.subject_candidates).toEqual(["группа свк"]);
|
||||
});
|
||||
|
||||
it("extracts compact scoped counterparty from net follow-up wording when LLM entities are empty", () => {
|
||||
const orgName = "ООО Альтернатива Плюс";
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
@@ -1653,6 +1681,7 @@ describe("assistant MCP discovery turn input adapter", () => {
|
||||
expect(result.should_run_discovery).toBe(true);
|
||||
expect(result.semantic_data_need).toBe("business overview evidence with bounded analyst interpretation");
|
||||
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
|
||||
expect(result.data_need_graph?.subject_candidates).toEqual([]);
|
||||
expect(result.data_need_graph?.time_scope_need).toBe("all_time_scope");
|
||||
expect(result.data_need_graph?.clarification_gaps).toEqual([]);
|
||||
expect(result.turn_meaning_ref).toMatchObject({
|
||||
@@ -1699,6 +1728,22 @@ describe("assistant MCP discovery turn input adapter", () => {
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_value_flow_signal_detected");
|
||||
});
|
||||
|
||||
it("routes colloquial multi-surface company overview into business overview over exact VAT", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage:
|
||||
"\u041f\u043e-\u0447\u0435\u043b\u043e\u0432\u0435\u0447\u0435\u0441\u043a\u0438 \u043f\u043e\u0441\u043c\u043e\u0442\u0440\u0438 \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441 \u0437\u0430 2020: \u0447\u0442\u043e \u043f\u043e \u0431\u0438\u0437\u043d\u0435\u0441\u0443 \u0432\u0438\u0434\u043d\u043e \u0432 1\u0421 \u043f\u043e \u0434\u0435\u043d\u044c\u0433\u0430\u043c, \u041d\u0414\u0421, \u0434\u043e\u043b\u0433\u0430\u043c, \u0441\u043a\u043b\u0430\u0434\u0443, \u043a\u043b\u0438\u0435\u043d\u0442\u0430\u043c/\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\u0430\u043c \u0438 \u0433\u0434\u0435 \u043f\u043e\u043a\u0430 \u043d\u0435\u043b\u044c\u0437\u044f \u0434\u0435\u043b\u0430\u0442\u044c \u0432\u044b\u0432\u043e\u0434?",
|
||||
assistantTurnMeaning: {
|
||||
explicit_intent_candidate: "vat_liability_confirmed_for_tax_period",
|
||||
explicit_organization_scope: "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.semantic_data_need).toBe("business overview evidence with bounded analyst interpretation");
|
||||
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
|
||||
expect(result.turn_meaning_ref?.asked_action_family).toBe("broad_evaluation");
|
||||
expect(result.turn_meaning_ref?.explicit_intent_candidate).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps explicit year out of the organization scope for raw business overview wording", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage:
|
||||
@@ -1789,6 +1834,36 @@ describe("assistant MCP discovery turn input adapter", () => {
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_counterparty_from_predecompose");
|
||||
});
|
||||
|
||||
it("keeps previous organization scope when all-time business overview says company in general", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage:
|
||||
"Теперь за все доступное время дай обзор компании в целом, но не тащи НДС за 2020 как подтвержденную общую налоговую позицию.",
|
||||
assistantTurnMeaning: {
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "broad_evaluation",
|
||||
explicit_organization_scope: "в целом",
|
||||
unsupported_but_understood_family: "broad_business_evaluation",
|
||||
stale_replay_forbidden: true
|
||||
},
|
||||
followupContext: {
|
||||
previous_discovery_pilot_scope: "business_overview_route_template_v1",
|
||||
previous_discovery_loop_provided_axes: ["organization"],
|
||||
previous_filters: {
|
||||
organization: "ООО Альтернатива Плюс",
|
||||
period_from: "2020-01-01",
|
||||
period_to: "2020-12-31"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.adapter_status).toBe("ready");
|
||||
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
|
||||
expect(result.data_need_graph?.time_scope_need).toBe("all_time_scope");
|
||||
expect(result.turn_meaning_ref?.explicit_organization_scope).toBe("ООО Альтернатива Плюс");
|
||||
expect(result.reason_codes).toContain("mcp_discovery_all_time_scope_signal_detected");
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_organization_scope_from_semantic_text");
|
||||
});
|
||||
|
||||
it("grounds organization aliases from known organizations for clean-session earnings questions", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage: "скока денег альтернатива заработала за 20 год?",
|
||||
@@ -3690,6 +3765,135 @@ describe("assistant MCP discovery turn input adapter", () => {
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_not_applicable_for_supported_exact_turn");
|
||||
});
|
||||
|
||||
it("routes brief compare boundary wording to business overview without implicit current date", () => {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const orgName =
|
||||
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
|
||||
const counterpartyName = "\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a";
|
||||
const valueFlowBundle = {
|
||||
counterparty: counterpartyName,
|
||||
incoming_total: 20653490,
|
||||
outgoing_total: 2129651,
|
||||
net_amount: 18523839
|
||||
};
|
||||
const documentBundle = {
|
||||
counterparty: counterpartyName,
|
||||
document_count: 19
|
||||
};
|
||||
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage:
|
||||
"\u0418 \u043a\u043e\u0440\u043e\u0442\u043a\u043e \u0441\u0440\u0430\u0432\u043d\u0438: \u0447\u0442\u043e \u0443 \u043d\u0430\u0441 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043e \u043f\u043e \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438, \u0447\u0442\u043e \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e \u043f\u043e \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u043c\u0443 \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0443, \u0438 \u043a\u0430\u043a\u0438\u0435 \u0432\u044b\u0432\u043e\u0434\u044b \u043d\u0435\u043b\u044c\u0437\u044f \u0434\u0435\u043b\u0430\u0442\u044c?",
|
||||
assistantTurnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "turnover",
|
||||
explicit_intent_candidate: "list_documents_by_counterparty",
|
||||
explicit_entity_candidates: ["\u043d\u0430\u0441 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043e"]
|
||||
},
|
||||
followupContext: {
|
||||
previous_discovery_pilot_scope: "counterparty_document_evidence_query_documents_v1",
|
||||
previous_intent: "list_documents_by_counterparty",
|
||||
target_intent: "list_documents_by_counterparty",
|
||||
previous_filters: {
|
||||
organization: orgName,
|
||||
counterparty: counterpartyName,
|
||||
as_of_date: today
|
||||
},
|
||||
previous_anchor_type: "counterparty",
|
||||
previous_anchor_value: counterpartyName,
|
||||
previous_discovery_bidirectional_value_flow: valueFlowBundle,
|
||||
previous_discovery_document_summary: documentBundle
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.adapter_status).toBe("ready");
|
||||
expect(result.should_run_discovery).toBe(true);
|
||||
expect(result.semantic_data_need).toBe("business overview evidence with bounded analyst interpretation");
|
||||
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
|
||||
expect(result.data_need_graph?.subject_candidates).toEqual([]);
|
||||
expect(result.turn_meaning_ref).toMatchObject({
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "broad_evaluation",
|
||||
business_overview_separate_entity_candidates: [counterpartyName],
|
||||
previous_counterparty_value_flow_bundle: valueFlowBundle,
|
||||
previous_counterparty_document_bundle: documentBundle,
|
||||
explicit_organization_scope: orgName,
|
||||
unsupported_but_understood_family: "broad_business_evaluation",
|
||||
stale_replay_forbidden: true
|
||||
});
|
||||
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
|
||||
expect(result.turn_meaning_ref?.explicit_date_scope).toBeUndefined();
|
||||
expect(result.reason_codes).toContain(
|
||||
"mcp_discovery_business_overview_preserved_explicit_counterparty_summary_scope"
|
||||
);
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_counterparty_from_raw_scope");
|
||||
});
|
||||
|
||||
it("continues business overview organization clarification without current-date reset", () => {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const orgName = "ООО Альтернатива Плюс";
|
||||
const counterpartyName = "Группа СВК";
|
||||
const valueFlowBundle = {
|
||||
counterparty: counterpartyName,
|
||||
incoming_total: 20653490,
|
||||
outgoing_total: 2129651,
|
||||
net_amount: 18523839
|
||||
};
|
||||
const documentBundle = {
|
||||
counterparty: counterpartyName,
|
||||
document_count: 19
|
||||
};
|
||||
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage: orgName,
|
||||
assistantTurnMeaning: {
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "broad_evaluation",
|
||||
explicit_date_scope: today,
|
||||
stale_replay_forbidden: true
|
||||
},
|
||||
predecomposeContract: {
|
||||
entities: { organization: orgName },
|
||||
period: { as_of_date: today }
|
||||
},
|
||||
followupContext: {
|
||||
previous_discovery_loop_status: "awaiting_clarification",
|
||||
previous_discovery_loop_selected_chain_id: "business_overview",
|
||||
previous_discovery_loop_pending_axes: ["organization"],
|
||||
previous_discovery_loop_provided_axes: ["all_time_scope", "metadata_scope"],
|
||||
previous_discovery_loop_asked_domain_family: "business_overview",
|
||||
previous_discovery_loop_asked_action_family: "broad_evaluation",
|
||||
previous_discovery_loop_unsupported_family: "broad_business_evaluation",
|
||||
previous_discovery_loop_metadata_scope_hint: counterpartyName,
|
||||
previous_anchor_type: "counterparty",
|
||||
previous_anchor_value: counterpartyName,
|
||||
previous_discovery_bidirectional_value_flow: valueFlowBundle,
|
||||
previous_discovery_document_summary: documentBundle
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.source_signal).toBe("assistant_turn_meaning");
|
||||
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
|
||||
expect(result.data_need_graph?.time_scope_need).toBe("all_time_scope");
|
||||
expect(result.turn_meaning_ref).toMatchObject({
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "broad_evaluation",
|
||||
business_overview_separate_entity_candidates: [counterpartyName],
|
||||
previous_counterparty_value_flow_bundle: valueFlowBundle,
|
||||
previous_counterparty_document_bundle: documentBundle,
|
||||
explicit_organization_scope: orgName,
|
||||
unsupported_but_understood_family: "broad_business_evaluation",
|
||||
stale_replay_forbidden: true
|
||||
});
|
||||
expect(result.turn_meaning_ref?.explicit_date_scope).toBeUndefined();
|
||||
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
|
||||
expect(result.reason_codes).toContain("mcp_discovery_all_time_scope_from_followup_context");
|
||||
expect(result.reason_codes).toContain(
|
||||
"mcp_discovery_business_overview_preserved_explicit_counterparty_summary_scope"
|
||||
);
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_date_scope_from_followup_context");
|
||||
});
|
||||
|
||||
it("preserves explicit counterparty scope for company plus counterparty business summaries", () => {
|
||||
const orgName = "ООО Альтернатива Плюс";
|
||||
const counterpartyName = "Группа СВК";
|
||||
|
||||
@@ -339,6 +339,71 @@ describe("assistantTransitionPolicy", () => {
|
||||
expect(carryover?.followupContext?.previous_anchor_value).toBe("Workstation Focus");
|
||||
});
|
||||
|
||||
it("keeps selected counterparty document follow-up through stale discovery guard", () => {
|
||||
const policy = buildPolicy({
|
||||
findLastAddressAssistantItem: () => ({
|
||||
text: "Confirmed value-flow profile for SVK Group.",
|
||||
debug: {
|
||||
detected_intent: "customer_revenue_and_payments",
|
||||
extracted_filters: {
|
||||
organization: "ACME",
|
||||
period_from: "2017-01-01",
|
||||
period_to: "2017-12-31"
|
||||
},
|
||||
assistant_mcp_discovery_entry_point_v1: {
|
||||
bridge: {
|
||||
loop_state: {
|
||||
selected_chain_id: "value_flow_comparison"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
resolveAssistantTurnMeaning: () => ({
|
||||
stale_replay_forbidden: true,
|
||||
explicit_intent_candidate: "list_documents_by_counterparty"
|
||||
}),
|
||||
resolveAddressIntent: () => ({ intent: "list_documents_by_counterparty" }),
|
||||
resolveAddressIntentFamily: (intent: unknown) =>
|
||||
["customer_revenue_and_payments", "list_documents_by_counterparty"].includes(String(intent ?? ""))
|
||||
? "counterparty"
|
||||
: toNonEmptyString(intent),
|
||||
hasAddressFollowupContextSignal: () => false,
|
||||
hasReferentialPointer: () => false,
|
||||
findRecentInventoryRootFrame: () => null,
|
||||
findRecentAddressFilterValue: () => null
|
||||
});
|
||||
|
||||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||||
"Show docs for selected counterparty",
|
||||
[],
|
||||
null,
|
||||
{
|
||||
predecomposeContract: {
|
||||
intent: "list_documents_by_counterparty"
|
||||
}
|
||||
},
|
||||
{
|
||||
session_context: {
|
||||
active_focus_object: {
|
||||
object_type: "counterparty",
|
||||
label: "SVK Group"
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
expect(carryover?.followupContext?.target_intent).toBe("list_documents_by_counterparty");
|
||||
expect(carryover?.followupContext?.previous_anchor_type).toBe("counterparty");
|
||||
expect(carryover?.followupContext?.previous_anchor_value).toBe("SVK Group");
|
||||
expect(carryover?.followupContext?.previous_filters).toMatchObject({
|
||||
organization: "ACME",
|
||||
counterparty: "SVK Group",
|
||||
period_from: "2017-01-01",
|
||||
period_to: "2017-12-31"
|
||||
});
|
||||
});
|
||||
|
||||
it("hydrates follow-up organization from shared assistant authority when local history filters are empty", () => {
|
||||
const policy = buildPolicy({
|
||||
findLastAddressAssistantItem: () => ({
|
||||
@@ -1628,6 +1693,164 @@ describe("assistantTransitionPolicy", () => {
|
||||
expect(carryover?.followupContext?.target_intent).toBe("customer_revenue_and_payments");
|
||||
});
|
||||
|
||||
it("carries selected counterparty proof bundles through business overview organization clarification", () => {
|
||||
const orgName = "ООО Альтернатива Плюс";
|
||||
const counterpartyName = "Группа СВК";
|
||||
const valueFlowBundle = {
|
||||
counterparty: counterpartyName,
|
||||
incoming_total: 20653490,
|
||||
outgoing_total: 2129651,
|
||||
net_amount: 18523839
|
||||
};
|
||||
const documentBundle = {
|
||||
counterparty: counterpartyName,
|
||||
document_count: 19
|
||||
};
|
||||
const policy = buildPolicy({
|
||||
findLastAddressAssistantItem: () => ({
|
||||
role: "assistant",
|
||||
text: "Коротко: уточните, по какой компании/организации сравнить выбранного контрагента Группа СВК.",
|
||||
debug: {
|
||||
execution_lane: "living_chat",
|
||||
detected_intent: "business_overview",
|
||||
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: {
|
||||
turn_meaning_ref: {
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "broad_evaluation",
|
||||
business_overview_separate_entity_candidates: [counterpartyName],
|
||||
previous_counterparty_value_flow_bundle: valueFlowBundle,
|
||||
previous_counterparty_document_bundle: documentBundle,
|
||||
metadata_scope_hint: counterpartyName,
|
||||
unsupported_but_understood_family: "broad_business_evaluation",
|
||||
stale_replay_forbidden: true
|
||||
}
|
||||
},
|
||||
bridge: {
|
||||
bridge_status: "needs_clarification",
|
||||
business_fact_answer_allowed: false,
|
||||
pilot: {
|
||||
pilot_scope: "business_overview_route_template_v1"
|
||||
},
|
||||
loop_state: {
|
||||
schema_version: "assistant_mcp_discovery_loop_state_v1",
|
||||
loop_status: "awaiting_clarification",
|
||||
selected_chain_id: "business_overview",
|
||||
pilot_scope: "business_overview_route_template_v1",
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "broad_evaluation",
|
||||
unsupported_but_understood_family: "broad_business_evaluation",
|
||||
pending_axes: ["organization"],
|
||||
provided_axes: ["all_time_scope", "metadata_scope"],
|
||||
metadata_scope_hint: counterpartyName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
resolveOrganizationSelectionFromMessage: (message: string) =>
|
||||
String(message ?? "").includes("Альтернатива") ? orgName : null,
|
||||
hasAddressFollowupContextSignal: () => false
|
||||
});
|
||||
|
||||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||||
orgName,
|
||||
[],
|
||||
null,
|
||||
{
|
||||
predecomposeContract: {
|
||||
mode: "unsupported",
|
||||
intent: "unknown",
|
||||
entities: { organization: orgName },
|
||||
semantics: { anchor_kind: "organization", anchor_value: orgName }
|
||||
}
|
||||
},
|
||||
null
|
||||
);
|
||||
|
||||
expect(carryover?.followupContext?.previous_discovery_loop_status).toBe("awaiting_clarification");
|
||||
expect(carryover?.followupContext?.previous_discovery_loop_selected_chain_id).toBe("business_overview");
|
||||
expect(carryover?.followupContext?.previous_discovery_loop_provided_axes).toEqual([
|
||||
"all_time_scope",
|
||||
"metadata_scope"
|
||||
]);
|
||||
expect(carryover?.followupContext?.previous_discovery_loop_metadata_scope_hint).toBe(counterpartyName);
|
||||
expect(carryover?.followupContext?.previous_discovery_bidirectional_value_flow).toEqual(valueFlowBundle);
|
||||
expect(carryover?.followupContext?.previous_discovery_document_summary).toEqual(documentBundle);
|
||||
});
|
||||
|
||||
it("recovers selected counterparty proof bundles from prior boundary-summary text", () => {
|
||||
const orgName = "ООО Альтернатива Плюс";
|
||||
const counterpartyName = "Группа СВК";
|
||||
const policy = buildPolicy({
|
||||
findLastAddressAssistantItem: () => ({
|
||||
role: "assistant",
|
||||
text:
|
||||
"Коротко: уточните, по какой компании/организации сравнить выбранного контрагента Группа СВК.\n" +
|
||||
"Отдельно по контрагенту Группа СВК: подтверждено получили 20 653 490 руб., заплатили 2 129 651 руб., расчетное нетто в нашу сторону 18 523 839 руб. Основа: проверенные входящие платежи и исходящие платежи и документы по цепочке: найдено 19.",
|
||||
debug: {
|
||||
execution_lane: "living_chat",
|
||||
detected_intent: "business_overview",
|
||||
mcp_discovery_response_applied: true,
|
||||
assistant_mcp_discovery_entry_point_v1: {
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
entry_status: "bridge_executed",
|
||||
bridge: {
|
||||
bridge_status: "needs_clarification",
|
||||
business_fact_answer_allowed: false,
|
||||
pilot: {
|
||||
pilot_scope: "business_overview_route_template_v1"
|
||||
},
|
||||
loop_state: {
|
||||
schema_version: "assistant_mcp_discovery_loop_state_v1",
|
||||
loop_status: "awaiting_clarification",
|
||||
selected_chain_id: "business_overview",
|
||||
pilot_scope: "business_overview_route_template_v1",
|
||||
asked_domain_family: "business_overview",
|
||||
asked_action_family: "broad_evaluation",
|
||||
pending_axes: ["organization"],
|
||||
provided_axes: ["all_time_scope", "metadata_scope"],
|
||||
metadata_scope_hint: counterpartyName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
resolveOrganizationSelectionFromMessage: (message: string) =>
|
||||
String(message ?? "").includes("Альтернатива") ? orgName : null,
|
||||
hasAddressFollowupContextSignal: () => false
|
||||
});
|
||||
|
||||
const carryover = policy.resolveAddressFollowupCarryoverContext(
|
||||
orgName,
|
||||
[],
|
||||
null,
|
||||
{
|
||||
predecomposeContract: {
|
||||
mode: "unsupported",
|
||||
intent: "unknown",
|
||||
entities: { organization: orgName },
|
||||
semantics: { anchor_kind: "organization", anchor_value: orgName }
|
||||
}
|
||||
},
|
||||
null
|
||||
);
|
||||
|
||||
expect(carryover?.followupContext?.previous_discovery_bidirectional_value_flow).toMatchObject({
|
||||
counterparty: counterpartyName,
|
||||
incoming_customer_revenue: { total_amount_human_ru: "20 653 490 руб." },
|
||||
outgoing_supplier_payout: { total_amount_human_ru: "2 129 651 руб." },
|
||||
net_amount_human_ru: "18 523 839 руб."
|
||||
});
|
||||
expect(carryover?.followupContext?.previous_discovery_document_summary).toMatchObject({
|
||||
counterparty: counterpartyName,
|
||||
document_count: 19
|
||||
});
|
||||
});
|
||||
|
||||
it("carries grounded metadata downstream route hints into followup context", () => {
|
||||
const policy = buildPolicy({
|
||||
findLastAddressAssistantItem: () => null,
|
||||
|
||||
Reference in New Issue
Block a user