Укрепить семантическую арбитрацию follow-up после жирного прогона
This commit is contained in:
+31
@@ -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",
|
||||
|
||||
+33
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user