ARCH: стабилизировать continuity и защитить exact-ответы от discovery

This commit is contained in:
dctouch 2026-04-21 18:43:05 +03:00
parent b542b65b81
commit 429bd3d8ec
8 changed files with 603 additions and 18 deletions

View File

@ -0,0 +1,138 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase20_continuity_stabilization",
"domain": "address_phase20_continuity_stabilization",
"title": "Phase 20 continuity stabilization replay",
"description": "Targeted AGENT replay for the continuity stabilization slice after assistant-stage1--I0x_DLqDb. The scenario validates that temporal tail words no longer turn into pseudo-counterparties, exact debt snapshots are not overwritten by discovery, and VAT/date follow-ups keep the previous period instead of drifting into unrelated carryover.",
"bindings": {},
"steps": [
{
"step_id": "step_01_top_client_all_time",
"title": "All-time top client question stays in ranking semantics and does not invent counterparty time",
"question": "кто у нас самый доходный клиент за все время?",
"required_answer_patterns_any": [
"(?i)клиент|контрагент",
"(?i)доходн|выручк|заработ"
],
"forbidden_answer_patterns": [
"(?i)не найден контрагент.*время",
"(?i)контрагент с названием\\s+\"?время\"?"
],
"criticality": "critical",
"semantic_tags": [
"value_flow_ranking",
"temporal_tail_not_entity"
]
},
{
"step_id": "step_02_top_year_all_time",
"title": "Top year question stays in yearly ranking semantics and does not invent pseudo-entity year",
"question": "какой у нас самый доходный год?",
"required_answer_patterns_any": [
"(?i)20\\d{2}|19\\d{2}",
"(?i)доходн|выручк|заработ"
],
"forbidden_answer_patterns": [
"(?i)не найден контрагент.*год",
"(?i)контрагент с названием\\s+\"?год\"?"
],
"criticality": "critical",
"semantic_tags": [
"value_flow_ranking",
"year_tail_not_entity"
]
},
{
"step_id": "step_03_receivables_as_of_may_2017",
"title": "Receivables snapshot for May 2017 answers directly instead of being overwritten by discovery",
"question": "кто нам должен денег на май 2017?",
"allowed_reply_types": [
"factual",
"factual_with_explanation"
],
"required_direct_answer_patterns_any": [
"(?i)долж",
"(?i)дебитор",
"(?i)задолж"
],
"forbidden_direct_answer_patterns": [
"(?i)partial_coverage",
"(?i)не удалось ответить",
"(?i)не найден контрагент"
],
"criticality": "critical",
"semantic_tags": [
"receivables_snapshot",
"exact_not_overwritten"
]
},
{
"step_id": "step_04_vat_for_same_period",
"title": "VAT payable follow-up keeps the carried May 2017 period",
"question": "а какой ндс мы должны примерно заплатить за этот период?",
"required_answer_patterns_all": [
"(?i)ндс",
"(?i)май|2017",
"(?i)заплат|к уплате|уплат"
],
"forbidden_answer_patterns": [
"(?i)текущ(ий|его) период",
"(?i)не найден контрагент"
],
"criticality": "critical",
"semantic_tags": [
"vat_followup",
"period_carryover"
]
},
{
"step_id": "step_05_payables_today",
"title": "Today payables snapshot answers directly and keeps debt semantics",
"question": "мы должны комуто денег на сегодня?",
"allowed_reply_types": [
"factual",
"factual_with_explanation"
],
"required_direct_answer_patterns_any": [
"(?i)должны",
"(?i)кредитор",
"(?i)задолж",
"(?i)долг к оплате|к оплате"
],
"forbidden_direct_answer_patterns": [
"(?i)partial_coverage",
"(?i)не удалось ответить",
"(?i)не найден контрагент"
],
"criticality": "critical",
"semantic_tags": [
"payables_snapshot",
"exact_not_overwritten"
]
},
{
"step_id": "step_06_receivables_pronoun_followup",
"title": "Short follow-up a nam resolves to mirrored receivables instead of fake counterparty time",
"question": "а нам?",
"allowed_reply_types": [
"factual",
"factual_with_explanation"
],
"required_direct_answer_patterns_any": [
"(?i)нам должны",
"(?i)дебитор",
"(?i)задолж"
],
"forbidden_direct_answer_patterns": [
"(?i)не найден контрагент.*время",
"(?i)контрагент с названием\\s+\"?время\"?",
"(?i)partial_coverage"
],
"criticality": "critical",
"semantic_tags": [
"pronoun_followup",
"garbage_anchor_forbidden"
]
}
]
}

View File

@ -89,6 +89,56 @@ function isDiscoveryReadyAddressCandidate(input, entryPoint) {
turnInput?.should_run_discovery === true &&
(source === "address_lane" || source === "address_exact" || source === "address_query_runtime_v1"));
}
function hasAlignedFactualAddressReply(input, entryPoint) {
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
if (toNonEmptyString(input.currentReplyType) !== "factual") {
return false;
}
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
const turnInput = toRecordObject(entryPoint?.turn_input);
const turnMeaning = toRecordObject(turnInput?.turn_meaning_ref);
const askedDomain = toNonEmptyString(turnMeaning?.asked_domain_family);
const askedAction = toNonEmptyString(turnMeaning?.asked_action_family);
if (detectedIntent === "counterparty_activity_lifecycle") {
return askedDomain === "counterparty_lifecycle" || askedAction === "activity_duration";
}
if (detectedIntent === "supplier_payouts_profile") {
return askedDomain === "counterparty_value" && askedAction === "payout";
}
if (detectedIntent === "customer_revenue_and_payments") {
return askedDomain === "counterparty_value" && askedAction === "turnover";
}
return false;
}
function hasMatchedFactualAddressContinuationTarget(input) {
if (toNonEmptyString(input.currentReplyType) !== "factual") {
return false;
}
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
const dialogContinuationContract = toRecordObject(input.addressRuntimeMeta?.dialogContinuationContract);
const targetIntent = toNonEmptyString(dialogContinuationContract?.target_intent);
return Boolean(detectedIntent && targetIntent && detectedIntent === targetIntent);
}
function hasFullConfirmedFactualAddressReply(input, entryPoint) {
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
if (toNonEmptyString(input.currentReplyType) !== "factual") {
return false;
}
const truthGateStatus = toNonEmptyString(input.addressRuntimeMeta?.truth_gate_contract_status);
if (truthGateStatus === "full_confirmed") {
return true;
}
const truthAnswerPolicy = toRecordObject(input.addressRuntimeMeta?.assistant_truth_answer_policy_v1);
const truthGate = toRecordObject(truthAnswerPolicy?.truth_gate);
const sourceTruthGateStatus = toNonEmptyString(truthGate?.source_truth_gate_status);
const coverageStatus = toNonEmptyString(truthGate?.coverage_status);
const groundingStatus = toNonEmptyString(truthGate?.grounding_status);
return sourceTruthGateStatus === "full_confirmed" || (coverageStatus === "full" && groundingStatus === "grounded");
}
function applyAssistantMcpDiscoveryResponsePolicy(input) {
const currentReply = String(input.currentReply ?? "");
const currentReplySource = toNonEmptyString(input.currentReplySource) ?? toNonEmptyString(input.livingChatSource) ?? "unknown";
@ -99,6 +149,9 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
const discoveryReadyChatCandidate = isDiscoveryReadyChatCandidate(input, entryPoint);
const discoveryReadyDeepCandidate = isDiscoveryReadyDeepCandidate(input, entryPoint);
const discoveryReadyAddressCandidate = isDiscoveryReadyAddressCandidate(input, entryPoint);
const alignedFactualAddressReply = hasAlignedFactualAddressReply(input, entryPoint);
const matchedFactualAddressContinuationTarget = hasMatchedFactualAddressContinuationTarget(input);
const fullConfirmedFactualAddressReply = hasFullConfirmedFactualAddressReply(input, entryPoint);
if (!entryPoint) {
pushReason(reasonCodes, "mcp_discovery_response_policy_no_entry_point");
}
@ -114,6 +167,15 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
if (!discoveryReadyAddressCandidate) {
pushReason(reasonCodes, "mcp_discovery_response_policy_not_discovery_ready_address_candidate");
}
if (alignedFactualAddressReply) {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_aligned_factual_address_reply");
}
if (matchedFactualAddressContinuationTarget) {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_factual_address_continuation_target");
}
if (fullConfirmedFactualAddressReply) {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_full_confirmed_factual_address_reply");
}
if (!ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status)) {
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_status_not_allowed");
}
@ -128,6 +190,9 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
}
const canApply = Boolean(entryPoint) &&
(unsupportedBoundary || discoveryReadyChatCandidate || discoveryReadyDeepCandidate || discoveryReadyAddressCandidate) &&
!alignedFactualAddressReply &&
!matchedFactualAddressContinuationTarget &&
!fullConfirmedFactualAddressReply &&
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
candidate.eligible_for_future_hot_runtime &&
Boolean(toNonEmptyString(candidate.reply_text)) &&

View File

@ -7,7 +7,10 @@ const SUPPORTED_ADDRESS_INTENTS = new Set([
"payables_confirmed_as_of_date",
"list_documents_by_counterparty",
"customer_revenue_and_payments",
"inventory_on_hand_as_of_date"
"inventory_on_hand_as_of_date",
"vat_liability_confirmed_for_tax_period",
"vat_payable_confirmed_as_of_date",
"vat_payable_forecast"
]);
function fallbackCompactWhitespace(value) {
return String(value ?? "").replace(/\s+/g, " ").trim();
@ -85,8 +88,23 @@ function detectCounterpartyTurnoverFamily(text) {
"\u0434\u043e\u0445\u043e\u0434",
"\u0431\u044b\u043b",
"\u0431\u044b\u043b\u0430",
"\u0432\u0440\u0435\u043c\u044f",
"\u0432\u0440\u0435\u043c\u0435\u043d\u0438",
"\u0433\u043e\u0434",
"\u0433\u043e\u0434\u0430",
"\u043f\u0435\u0440\u0438\u043e\u0434",
"\u043f\u0435\u0440\u0438\u043e\u0434\u0430",
"\u043c\u0435\u0441\u044f\u0446",
"\u043c\u0435\u0441\u044f\u0446\u0430",
"\u043a\u0432\u0430\u0440\u0442\u0430\u043b",
"\u043a\u0432\u0430\u0440\u0442\u0430\u043b\u0430",
"turnover",
"revenue"
"revenue",
"time",
"year",
"period",
"month",
"quarter"
]);
const entity = rawEntity && !ignored.has(rawEntity) ? rawEntity : null;
return {
@ -136,6 +154,8 @@ function createAssistantTurnMeaningPolicy(deps = {}) {
? "receivables"
: explicitIntentCandidate?.startsWith("payables_")
? "payables"
: explicitIntentCandidate?.startsWith("vat_")
? "vat"
: explicitIntentCandidate?.startsWith("inventory_")
? "inventory"
: explicitIntentCandidate?.includes("counterparty")
@ -147,6 +167,12 @@ function createAssistantTurnMeaningPolicy(deps = {}) {
explicitIntentCandidate === "payables_confirmed_as_of_date" ||
explicitIntentCandidate === "inventory_on_hand_as_of_date"
? "confirmed_snapshot"
: explicitIntentCandidate === "vat_liability_confirmed_for_tax_period"
? "confirmed_tax_period"
: explicitIntentCandidate === "vat_payable_confirmed_as_of_date"
? "confirmed_snapshot"
: explicitIntentCandidate === "vat_payable_forecast"
? "forecast"
: explicitIntentCandidate === "list_documents_by_counterparty"
? "list_documents"
: counterpartyTurnover?.family

View File

@ -13,6 +13,7 @@ export type AssistantMcpDiscoveryResponsePolicyDecision = "apply_candidate" | "k
export interface ApplyAssistantMcpDiscoveryResponsePolicyInput {
currentReply: string;
currentReplySource?: string | null;
currentReplyType?: string | null;
livingChatSource?: string | null;
modeDecisionReason?: string | null;
addressRuntimeMeta?: Record<string, unknown> | null;
@ -151,6 +152,69 @@ function isDiscoveryReadyAddressCandidate(
);
}
function hasAlignedFactualAddressReply(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
): boolean {
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
if (toNonEmptyString(input.currentReplyType) !== "factual") {
return false;
}
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
const turnInput = toRecordObject(entryPoint?.turn_input);
const turnMeaning = toRecordObject(turnInput?.turn_meaning_ref);
const askedDomain = toNonEmptyString(turnMeaning?.asked_domain_family);
const askedAction = toNonEmptyString(turnMeaning?.asked_action_family);
if (detectedIntent === "counterparty_activity_lifecycle") {
return askedDomain === "counterparty_lifecycle" || askedAction === "activity_duration";
}
if (detectedIntent === "supplier_payouts_profile") {
return askedDomain === "counterparty_value" && askedAction === "payout";
}
if (detectedIntent === "customer_revenue_and_payments") {
return askedDomain === "counterparty_value" && askedAction === "turnover";
}
return false;
}
function hasMatchedFactualAddressContinuationTarget(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput
): boolean {
if (toNonEmptyString(input.currentReplyType) !== "factual") {
return false;
}
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
const dialogContinuationContract = toRecordObject(input.addressRuntimeMeta?.dialogContinuationContract);
const targetIntent = toNonEmptyString(dialogContinuationContract?.target_intent);
return Boolean(detectedIntent && targetIntent && detectedIntent === targetIntent);
}
function hasFullConfirmedFactualAddressReply(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
): boolean {
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
if (toNonEmptyString(input.currentReplyType) !== "factual") {
return false;
}
const truthGateStatus = toNonEmptyString(input.addressRuntimeMeta?.truth_gate_contract_status);
if (truthGateStatus === "full_confirmed") {
return true;
}
const truthAnswerPolicy = toRecordObject(input.addressRuntimeMeta?.assistant_truth_answer_policy_v1);
const truthGate = toRecordObject(truthAnswerPolicy?.truth_gate);
const sourceTruthGateStatus = toNonEmptyString(truthGate?.source_truth_gate_status);
const coverageStatus = toNonEmptyString(truthGate?.coverage_status);
const groundingStatus = toNonEmptyString(truthGate?.grounding_status);
return sourceTruthGateStatus === "full_confirmed" || (coverageStatus === "full" && groundingStatus === "grounded");
}
export function applyAssistantMcpDiscoveryResponsePolicy(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput
): AssistantMcpDiscoveryResponsePolicyResult {
@ -164,6 +228,9 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
const discoveryReadyChatCandidate = isDiscoveryReadyChatCandidate(input, entryPoint);
const discoveryReadyDeepCandidate = isDiscoveryReadyDeepCandidate(input, entryPoint);
const discoveryReadyAddressCandidate = isDiscoveryReadyAddressCandidate(input, entryPoint);
const alignedFactualAddressReply = hasAlignedFactualAddressReply(input, entryPoint);
const matchedFactualAddressContinuationTarget = hasMatchedFactualAddressContinuationTarget(input);
const fullConfirmedFactualAddressReply = hasFullConfirmedFactualAddressReply(input, entryPoint);
if (!entryPoint) {
pushReason(reasonCodes, "mcp_discovery_response_policy_no_entry_point");
@ -180,6 +247,15 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
if (!discoveryReadyAddressCandidate) {
pushReason(reasonCodes, "mcp_discovery_response_policy_not_discovery_ready_address_candidate");
}
if (alignedFactualAddressReply) {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_aligned_factual_address_reply");
}
if (matchedFactualAddressContinuationTarget) {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_factual_address_continuation_target");
}
if (fullConfirmedFactualAddressReply) {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_full_confirmed_factual_address_reply");
}
if (!ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status)) {
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_status_not_allowed");
}
@ -196,6 +272,9 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
const canApply =
Boolean(entryPoint) &&
(unsupportedBoundary || discoveryReadyChatCandidate || discoveryReadyDeepCandidate || discoveryReadyAddressCandidate) &&
!alignedFactualAddressReply &&
!matchedFactualAddressContinuationTarget &&
!fullConfirmedFactualAddressReply &&
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
candidate.eligible_for_future_hot_runtime &&
Boolean(toNonEmptyString(candidate.reply_text)) &&

View File

@ -5,7 +5,10 @@ const SUPPORTED_ADDRESS_INTENTS = new Set([
"payables_confirmed_as_of_date",
"list_documents_by_counterparty",
"customer_revenue_and_payments",
"inventory_on_hand_as_of_date"
"inventory_on_hand_as_of_date",
"vat_liability_confirmed_for_tax_period",
"vat_payable_confirmed_as_of_date",
"vat_payable_forecast"
]);
function fallbackCompactWhitespace(value) {
@ -89,8 +92,23 @@ function detectCounterpartyTurnoverFamily(text) {
"\u0434\u043e\u0445\u043e\u0434",
"\u0431\u044b\u043b",
"\u0431\u044b\u043b\u0430",
"\u0432\u0440\u0435\u043c\u044f",
"\u0432\u0440\u0435\u043c\u0435\u043d\u0438",
"\u0433\u043e\u0434",
"\u0433\u043e\u0434\u0430",
"\u043f\u0435\u0440\u0438\u043e\u0434",
"\u043f\u0435\u0440\u0438\u043e\u0434\u0430",
"\u043c\u0435\u0441\u044f\u0446",
"\u043c\u0435\u0441\u044f\u0446\u0430",
"\u043a\u0432\u0430\u0440\u0442\u0430\u043b",
"\u043a\u0432\u0430\u0440\u0442\u0430\u043b\u0430",
"turnover",
"revenue"
"revenue",
"time",
"year",
"period",
"month",
"quarter"
]);
const entity = rawEntity && !ignored.has(rawEntity) ? rawEntity : null;
return {
@ -146,6 +164,8 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
? "receivables"
: explicitIntentCandidate?.startsWith("payables_")
? "payables"
: explicitIntentCandidate?.startsWith("vat_")
? "vat"
: explicitIntentCandidate?.startsWith("inventory_")
? "inventory"
: explicitIntentCandidate?.includes("counterparty")
@ -158,6 +178,12 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
explicitIntentCandidate === "payables_confirmed_as_of_date" ||
explicitIntentCandidate === "inventory_on_hand_as_of_date"
? "confirmed_snapshot"
: explicitIntentCandidate === "vat_liability_confirmed_for_tax_period"
? "confirmed_tax_period"
: explicitIntentCandidate === "vat_payable_confirmed_as_of_date"
? "confirmed_snapshot"
: explicitIntentCandidate === "vat_payable_forecast"
? "forecast"
: explicitIntentCandidate === "list_documents_by_counterparty"
? "list_documents"
: counterpartyTurnover?.family

View File

@ -113,11 +113,17 @@ describe("assistant MCP discovery response policy", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "stale exact route answer",
currentReplySource: "address_query_runtime_v1",
currentReplyType: "factual",
addressRuntimeMeta: {
detected_intent: "list_documents_by_counterparty",
assistant_mcp_discovery_entry_point_v1: entryPoint({
turn_input: {
adapter_status: "ready",
should_run_discovery: true
should_run_discovery: true,
turn_meaning_ref: {
asked_domain_family: "counterparty_value",
asked_action_family: "payout"
}
}
})
}
@ -129,6 +135,94 @@ describe("assistant MCP discovery response policy", () => {
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_not_discovery_ready_address_candidate");
});
it("keeps aligned factual address lane answers when the exact lane already matched the same semantic intent", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "ИП Калинин Н.М. | сумма: 216600 | операций: 2",
currentReplySource: "address_query_runtime_v1",
currentReplyType: "factual",
addressRuntimeMeta: {
detected_intent: "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: "counterparty_value",
asked_action_family: "turnover"
}
}
})
}
});
expect(result.applied).toBe(false);
expect(result.decision).toBe("keep_current_reply");
expect(result.reply_text).toBe("ИП Калинин Н.М. | сумма: 216600 | операций: 2");
expect(result.reason_codes).toContain("mcp_discovery_response_policy_keep_aligned_factual_address_reply");
});
it("keeps factual address follow-up replies when they already match the continuation target intent", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "ИП Калинин Н.М. | сумма: 216600 | операций: 2",
currentReplySource: "address_query_runtime_v1",
currentReplyType: "factual",
addressRuntimeMeta: {
detected_intent: "customer_revenue_and_payments",
dialogContinuationContract: {
target_intent: "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: "counterparty_lifecycle",
asked_action_family: "activity_duration"
}
}
})
}
});
expect(result.applied).toBe(false);
expect(result.decision).toBe("keep_current_reply");
expect(result.reason_codes).toContain("mcp_discovery_response_policy_keep_factual_address_continuation_target");
});
it("keeps full-confirmed factual address replies even when discovery has a guarded candidate", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "ООО Ромашка | сумма: 128000 | операций: 3",
currentReplySource: "address_query_runtime_v1",
currentReplyType: "factual",
addressRuntimeMeta: {
detected_intent: "receivables_confirmed_as_of_date",
truth_gate_contract_status: "full_confirmed",
assistant_truth_answer_policy_v1: {
truth_gate: {
coverage_status: "full",
grounding_status: "grounded",
source_truth_gate_status: "full_confirmed"
}
},
assistant_mcp_discovery_entry_point_v1: entryPoint({
turn_input: {
adapter_status: "ready",
should_run_discovery: true,
turn_meaning_ref: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover"
}
}
})
}
});
expect(result.applied).toBe(false);
expect(result.decision).toBe("keep_current_reply");
expect(result.reply_text).toBe("ООО Ромашка | сумма: 128000 | операций: 3");
expect(result.reason_codes).toContain("mcp_discovery_response_policy_keep_full_confirmed_factual_address_reply");
});
it("keeps address lane answers when discovery was not requested for the current turn", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "supported exact route answer",

View File

@ -1013,4 +1013,123 @@ describe("assistantTransitionPolicy", () => {
expect(carryover).toBeNull();
});
it("reuses grounded MCP discovery payout context for a short year-switch follow-up", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => null,
hasAddressFollowupContextSignal: () => true
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
"а теперь за 2021?",
[
{
role: "assistant",
text: "Подтверждены исходящие платежи по Группа СВК за 2020 год.",
debug: {
execution_lane: "living_chat",
mcp_discovery_response_applied: true,
assistant_active_organization: "ООО Альтернатива Плюс",
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_action_family: "payout",
explicit_entity_candidates: ["Группа СВК"],
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
}
},
bridge: {
bridge_status: "answer_draft_ready",
business_fact_answer_allowed: true,
pilot: {
pilot_scope: "counterparty_supplier_payout_query_movements_v1"
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference"
}
}
}
}
}
],
null,
null,
null
);
expect(carryover?.followupSelectionMode).toBe("carry_previous_intent");
expect(carryover?.followupContext?.previous_intent).toBe("supplier_payouts_profile");
expect(carryover?.followupContext?.target_intent).toBe("supplier_payouts_profile");
expect(carryover?.followupContext?.previous_discovery_pilot_scope).toBe(
"counterparty_supplier_payout_query_movements_v1"
);
expect(carryover?.followupContext?.previous_anchor_type).toBe("counterparty");
expect(carryover?.followupContext?.previous_anchor_value).toBe("Группа СВК");
expect(carryover?.followupContext?.previous_filters).toMatchObject({
counterparty: "Группа СВК",
organization: "ООО Альтернатива Плюс",
period_from: "2020-01-01",
period_to: "2020-12-31"
});
});
it("switches to VAT tax-period intent while preserving carried period filters", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({
text: "Подтвержденная дебиторская задолженность на 31.05.2017 собрана.",
debug: {
detected_intent: "receivables_confirmed_as_of_date",
extracted_filters: {
organization: 'ООО "Альтернатива Плюс"',
as_of_date: "2017-05-31",
period_from: "2017-05-01",
period_to: "2017-05-31"
},
anchor_type: "organization",
anchor_value_resolved: 'ООО "Альтернатива Плюс"'
}
}),
hasAddressFollowupContextSignal: () => true,
hasReferentialPointer: (value: unknown) => /этот период/i.test(String(value ?? "")),
resolveAddressIntent: () => ({ intent: "unknown" }),
resolveAddressIntentFamily: (intent: unknown) => {
if (String(intent ?? "").startsWith("receivables_")) return "receivables";
if (String(intent ?? "").startsWith("vat_")) return "vat";
return null;
},
resolveAssistantTurnMeaning: () => ({
schema_version: "assistant_turn_meaning_v1",
asked_domain_family: "vat",
asked_action_family: "confirmed_tax_period",
explicit_intent_candidate: "vat_liability_confirmed_for_tax_period",
explicit_entity_candidates: [],
intent_override_strength: "explicit_current_turn_intent",
stale_replay_forbidden: false
})
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
"а какой ндс мы должны примерно заплатить за этот период?",
[],
"Какой НДС должен быть уплачен за текущий период?",
{
predecomposeContract: {
intent: "unknown"
}
},
null
);
expect(carryover?.followupSelectionMode).toBe("carry_previous_intent");
expect(carryover?.followupContext?.previous_intent).toBe("receivables_confirmed_as_of_date");
expect(carryover?.followupContext?.target_intent).toBe("vat_liability_confirmed_for_tax_period");
expect(carryover?.followupContext?.previous_filters).toMatchObject({
organization: 'ООО "Альтернатива Плюс"',
as_of_date: "2017-05-31",
period_from: "2017-05-01",
period_to: "2017-05-31"
});
});
});

View File

@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
import { createAssistantTurnMeaningPolicy } from "../src/services/assistantTurnMeaningPolicy";
import { resolveAddressIntent } from "../src/services/addressIntentResolver";
function buildPolicy() {
function buildPolicy(overrides: Record<string, unknown> = {}) {
return createAssistantTurnMeaningPolicy({
compactWhitespace: (value: string) => String(value ?? "").replace(/\s+/g, " ").trim(),
repairAddressMojibake: (value: string) => value,
@ -13,7 +13,8 @@ function buildPolicy() {
}
const text = String(value).trim();
return text.length > 0 ? text : null;
}
},
...overrides
});
}
@ -55,4 +56,41 @@ describe("assistantTurnMeaningPolicy", () => {
}
]);
});
it("ignores temporal tail words in all-time revenue ranking questions", () => {
const policy = buildPolicy({
resolveAddressIntent: (text: string) =>
text.includes("\u0434\u043e\u0445\u043e\u0434\u043d\u044b\u0439")
? { intent: "customer_revenue_and_payments", confidence: "high" }
: resolveAddressIntent(text)
});
const meaning = policy.resolveAssistantTurnMeaning({
rawUserMessage:
"\u043a\u0442\u043e \u0443 \u043d\u0430\u0441 \u0441\u0430\u043c\u044b\u0439 \u0434\u043e\u0445\u043e\u0434\u043d\u044b\u0439 \u043a\u043b\u0438\u0435\u043d\u0442 \u0437\u0430 \u0432\u0441\u0435 \u0432\u0440\u0435\u043c\u044f"
});
expect(meaning.explicit_intent_candidate).toBe("customer_revenue_and_payments");
expect(meaning.explicit_entity_candidates).toEqual([]);
expect(meaning.stale_replay_forbidden).toBe(false);
});
it("treats VAT period questions as supported current-turn intent", () => {
const policy = buildPolicy({
resolveAddressIntent: (text: string) =>
text.includes("\u043d\u0434\u0441")
? { intent: "vat_liability_confirmed_for_tax_period", confidence: "high" }
: resolveAddressIntent(text)
});
const meaning = policy.resolveAssistantTurnMeaning({
rawUserMessage:
"\u0430 \u043a\u0430\u043a\u043e\u0439 \u043d\u0434\u0441 \u043c\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c \u0437\u0430 \u044d\u0442\u043e\u0442 \u043f\u0435\u0440\u0438\u043e\u0434"
});
expect(meaning.explicit_intent_candidate).toBe("vat_liability_confirmed_for_tax_period");
expect(meaning.asked_domain_family).toBe("vat");
expect(meaning.asked_action_family).toBe("confirmed_tax_period");
expect(meaning.stale_replay_forbidden).toBe(false);
});
});