Укрепить семантическую арбитрацию follow-up после жирного прогона

This commit is contained in:
2026-05-08 21:57:41 +03:00
parent ba23b056b8
commit a15cbb3fcb
18 changed files with 898 additions and 37 deletions
@@ -0,0 +1,31 @@
import { describe, expect, it } from "vitest";
import { runAddressDecomposeStage } from "../src/services/address_runtime/decomposeStage";
describe("counterparty follow-up over stale inventory frame", () => {
it("keeps bare pronoun document follow-up on the active counterparty lane", () => {
const result = runAddressDecomposeStage("а по нему документы?", {
previous_intent: "list_contracts_by_counterparty",
target_intent: "list_documents_by_counterparty",
previous_filters: {
counterparty: 'ТСЖ "Жуковка 51"',
organization: "ООО Альтернатива Плюс",
as_of_date: "2026-04-16"
},
previous_anchor_type: "counterparty",
previous_anchor_value: 'ТСЖ "Жуковка 51"',
root_intent: "inventory_on_hand_as_of_date",
root_filters: {
organization: "ООО Альтернатива Плюс",
as_of_date: "2026-04-16"
},
root_anchor_type: "organization",
root_anchor_value: "ООО Альтернатива Плюс",
current_frame_kind: "inventory_root"
});
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("list_documents_by_counterparty");
expect(result?.filters.extracted_filters.counterparty).toBe('ТСЖ "Жуковка 51"');
expect(result?.baseReasons).not.toContain("intent_adjusted_to_inventory_followup_context");
});
});
@@ -29,6 +29,15 @@ describe("addressIntentResolver regression bridges", () => {
expect(result.intent).toBe("customer_revenue_and_payments");
});
it("detects customer concentration wording as customer revenue ranking", () => {
const result = resolveAddressIntent(
"\u041a\u0442\u043e \u043a\u0440\u0443\u043f\u043d\u0435\u0439\u0448\u0438\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u044b \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u044b \u041f\u043b\u044e\u0441 \u0438 \u043d\u0430\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0431\u0438\u0437\u043d\u0435\u0441 \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \u043e\u0442 \u043e\u0434\u043d\u043e\u0433\u043e \u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b\u044f?"
);
expect(result.intent).toBe("customer_revenue_and_payments");
expect(result.reasons).toContain("unicode_customer_concentration_bridge_signal_detected");
});
it("defers top-year company revenue wording to business overview discovery", () => {
const result = resolveAddressIntent("какой у нас самый доходный год");
@@ -172,6 +172,45 @@ 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 replies when the discovery turn asks for movements", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "Documents for SVK: invoice 1, act 2.",
currentReplySource: "address_query_runtime_v1",
currentReplyType: "factual",
addressRuntimeMeta: {
detected_intent: "list_documents_by_counterparty",
selected_recipe: "address_list_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: "address_list_documents_by_counterparty"
},
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: ["SVK"],
explicit_date_scope: "2020"
}
}
})
}
});
expect(result.applied).toBe(true);
expect(result.decision).toBe("apply_candidate");
expect(result.reply_text).toContain("Confirmed fact");
expect(result.reason_codes).toContain("mcp_discovery_response_policy_semantic_conflict_allows_candidate_override");
expect(result.reason_codes).toContain("mcp_discovery_response_policy_evidence_lane_conflict_allows_candidate_override");
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_exact_matched_factual_address_reply");
});
it("keeps exact matched inventory address replies over stale metadata discovery candidates", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "По товару Шкаф картотечный 1000*400*2100 цепочка поставки и продажи подтверждена.",
@@ -209,6 +248,62 @@ describe("assistant MCP discovery response policy", () => {
expect(result.reason_codes).toContain("mcp_discovery_response_policy_semantic_conflict_allows_candidate_override");
});
it("lets metadata discovery override a stale exact address answer outside protected inventory chains", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "ИП Калинин Н.М. | сумма: 216600",
currentReplySource: "address_query_runtime_v1",
currentReplyType: "factual",
addressRuntimeMeta: {
detected_intent: "customer_revenue_and_payments",
selected_recipe: "address_customer_revenue_and_payments_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: "address_customer_revenue_and_payments"
},
assistant_mcp_discovery_entry_point_v1: entryPoint({
turn_input: {
adapter_status: "ready",
should_run_discovery: true,
turn_meaning_ref: {
asked_domain_family: "metadata",
asked_action_family: "inspect_surface",
unsupported_but_understood_family: "1c_metadata_surface"
},
data_need_graph: {
business_fact_family: "schema_surface",
decomposition_candidates: ["inspect_metadata_surface"]
}
},
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: "Metadata surface confirmed.",
confirmed_lines: ["Available metadata object sets: Catalog.Counterparties"],
inference_lines: [],
unknown_lines: [],
limitation_lines: [],
next_step_line: null
}
}
})
}
});
expect(result.applied).toBe(true);
expect(result.decision).toBe("apply_candidate");
expect(result.reply_text).toContain("Catalog.Counterparties");
expect(result.reason_codes).toContain("mcp_discovery_response_policy_metadata_candidate_priority");
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_exact_matched_factual_address_reply");
});
it("keeps aligned factual address lane answers when the exact lane already matched the same semantic intent", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "ИП Калинин Н.М. | сумма: 216600 | операций: 2",
@@ -0,0 +1,33 @@
import { describe, expect, it } from "vitest";
import { buildAssistantMcpDiscoveryTurnInput } from "../src/services/assistantMcpDiscoveryTurnInputAdapter";
describe("assistant MCP discovery turn input adapter garbage entity regressions", () => {
it("does not treat document and movement lane words as counterparty candidates", () => {
const followupContext = {
previous_intent: "customer_revenue_and_payments" as const,
previous_discovery_pilot_scope: "counterparty_value_flow_query_movements_v1",
previous_filters: {
counterparty: "Группа СВК",
organization: "ООО Альтернатива Плюс",
period_from: "2020-01-01",
period_to: "2020-12-31"
},
previous_anchor_type: "counterparty" as const,
previous_anchor_value: "Группа СВК"
};
const documents = buildAssistantMcpDiscoveryTurnInput({
userMessage: "а по документам?",
followupContext
});
const movements = buildAssistantMcpDiscoveryTurnInput({
userMessage: "а по движениям?",
followupContext
});
expect(documents.turn_meaning_ref?.explicit_entity_candidates).toEqual(["Группа СВК"]);
expect(movements.turn_meaning_ref?.explicit_entity_candidates).toEqual(["Группа СВК"]);
expect(documents.turn_meaning_ref?.explicit_entity_candidates).not.toContain("документам");
expect(movements.turn_meaning_ref?.explicit_entity_candidates).not.toContain("движениям");
});
});
@@ -56,6 +56,51 @@ describe("assistantMetaFollowupPolicy", () => {
).toBeNull();
});
it("detects 1C schema/catalog questions as data-scope meta even when base detector misses them", () => {
const signals = policy.resolveMetaSignalSet({
rawUserMessage:
"\u041a\u0430\u043a\u0438\u0435 \u0441\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a\u0438 1\u0421 \u0435\u0441\u0442\u044c \u043f\u043e \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430\u043c?",
repairedRawUserMessage: "",
effectiveAddressUserMessage: "",
repairedEffectiveAddressUserMessage: ""
});
expect(signals.dataScopeMetaQuery).toBe(true);
expect(signals.capabilityMetaQuery).toBe(false);
});
it("detects document field/link questions as schema meta rather than fake counterparty documents", () => {
const signals = policy.resolveMetaSignalSet({
rawUserMessage:
"\u041a\u0430\u043a\u0438\u0435 \u043f\u043e\u043b\u044f \u0438 \u0441\u0432\u044f\u0437\u0438 \u0441\u0442\u043e\u0438\u0442 \u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0443 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u043e\u0432 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u0438 \u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u044f?",
repairedRawUserMessage: "",
effectiveAddressUserMessage: "",
repairedEffectiveAddressUserMessage: ""
});
expect(signals.dataScopeMetaQuery).toBe(true);
expect(signals.capabilityMetaQuery).toBe(false);
});
it("does not turn debt classification wording into capability meta help", () => {
const noisyPolicy = createAssistantMetaFollowupPolicy({
hasAssistantDataScopeMetaQuestionSignal: () => false,
shouldHandleAsAssistantCapabilityMetaQuery: () => true,
hasMetaAnswerFollowupSignal: () => false,
hasAnswerInspectionFollowupSignal: () => false
});
const signals = noisyPolicy.resolveMetaSignalSet({
rawUserMessage:
"\u042d\u0442\u043e \u043c\u043e\u0436\u043d\u043e \u0441\u0447\u0438\u0442\u0430\u0442\u044c \u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a\u043e\u0439 \u0438\u043b\u0438 \u043f\u043e\u043a\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0442\u043a\u0440\u044b\u0442\u043e\u0439 \u0437\u0430\u0434\u043e\u043b\u0436\u0435\u043d\u043d\u043e\u0441\u0442\u044c\u044e?",
repairedRawUserMessage: "",
effectiveAddressUserMessage: "",
repairedEffectiveAddressUserMessage: ""
});
expect(signals.capabilityMetaQuery).toBe(false);
});
it("detects evaluative meta follow-up over grounded answer", () => {
const detected = policy.isMetaFollowupOverGroundedAnswer({
followupContext: { previous_intent: "vat_payable_forecast" },
@@ -227,4 +227,50 @@ describe("assistantTurnMeaningPolicy", () => {
expect(supplierQuality.unsupported_but_understood_family).toBe("broad_business_evaluation");
expect(supplierQuality.reason_codes).toContain("broad_business_evaluation_current_turn_signal");
});
it("keeps selected-object profitability as an inventory exact intent instead of company overview", () => {
const policy = buildPolicy();
const meaning = policy.resolveAssistantTurnMeaning({
rawUserMessage:
'\u041f\u043e \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u043c\u0443 \u043e\u0431\u044a\u0435\u043a\u0442\u0443 "\u0427\u0435\u0442\u043a\u0438 \u041f\u043e\u0441\u0442 (84*117)": \u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438 \u043d\u0430 \u043f\u0440\u043e\u0434\u0430\u0436\u0435, \u043a\u0430\u043a\u0438\u0435 \u0437\u0430\u043a\u0443\u043f\u043e\u0447\u043d\u044b\u0435 \u0438 \u043f\u0440\u043e\u0434\u0430\u0436\u043d\u044b\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b \u044d\u0442\u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0430\u044e\u0442?'
});
expect(meaning.explicit_intent_candidate).toBe("inventory_profitability_for_item");
expect(meaning.asked_domain_family).toBe("inventory");
expect(meaning.unsupported_but_understood_family).toBeNull();
expect(meaning.stale_replay_forbidden).toBe(false);
expect(meaning.reason_codes).not.toContain("broad_business_evaluation_current_turn_signal");
});
it("treats paired receivables/payables position as business overview instead of one debt side", () => {
const policy = buildPolicy();
const meaning = policy.resolveAssistantTurnMeaning({
rawUserMessage:
"\u041a\u0430\u043a\u0430\u044f \u0434\u0435\u0431\u0438\u0442\u043e\u0440\u043a\u0430 \u0438 \u043a\u0440\u0435\u0434\u0438\u0442\u043e\u0440\u043a\u0430 \u043d\u0430 30.09.2020 \u043f\u043e \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438?"
});
expect(meaning.explicit_intent_candidate).toBeNull();
expect(meaning.asked_domain_family).toBe("business_summary");
expect(meaning.asked_action_family).toBe("broad_evaluation");
expect(meaning.unsupported_but_understood_family).toBe("broad_business_evaluation");
expect(meaning.reason_codes).toContain("broad_business_evaluation_current_turn_signal");
});
it("treats debt overdue classification follow-up as business interpretation, not capability help", () => {
const policy = buildPolicy({
resolveAddressIntent: () => ({ intent: "unknown", confidence: "low" })
});
const meaning = policy.resolveAssistantTurnMeaning({
rawUserMessage:
"\u042d\u0442\u043e \u043c\u043e\u0436\u043d\u043e \u0441\u0447\u0438\u0442\u0430\u0442\u044c \u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a\u043e\u0439 \u0438\u043b\u0438 \u043f\u043e\u043a\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0442\u043a\u0440\u044b\u0442\u043e\u0439 \u0437\u0430\u0434\u043e\u043b\u0436\u0435\u043d\u043d\u043e\u0441\u0442\u044c\u044e?"
});
expect(meaning.explicit_intent_candidate).toBeNull();
expect(meaning.asked_domain_family).toBe("business_summary");
expect(meaning.asked_action_family).toBe("broad_evaluation");
expect(meaning.unsupported_but_understood_family).toBe("broad_business_evaluation");
});
});