ARCH: защитить document→payments pivot от discovery override
This commit is contained in:
parent
65d15c156b
commit
19137a698f
|
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"schema_version": "domain_truth_harness_spec_v1",
|
||||||
|
"scenario_id": "address_truth_harness_phase69_document_to_payments_pronoun_pivot",
|
||||||
|
"domain": "address_phase69_document_to_payments_pronoun_pivot",
|
||||||
|
"title": "Phase 69 document to payments pronoun pivot",
|
||||||
|
"description": "Replay for a human follow-up where the user first asks for documents by counterparty and then pivots with 'а по нему платежи?', expecting the same counterparty to survive and the contour to switch to payments.",
|
||||||
|
"bindings": {},
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step_id": "step_01_documents_by_counterparty",
|
||||||
|
"title": "Open documents for the counterparty",
|
||||||
|
"question": "Покажи документы по Жуковке 51.",
|
||||||
|
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"(?i)жуковк",
|
||||||
|
"(?i)документ|сч[её]т|акт|накладн|строк"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": ["documents_by_counterparty", "pronoun_pivot_seed", "integrity_guard"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_02_payments_by_pronoun_followup",
|
||||||
|
"title": "Pivot to payments via pronoun follow-up",
|
||||||
|
"question": "А по нему платежи?",
|
||||||
|
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
|
||||||
|
"required_answer_patterns_all": [
|
||||||
|
"(?i)жуковк|контрагент",
|
||||||
|
"(?i)платеж|операц|банк|поступлен|списан"
|
||||||
|
],
|
||||||
|
"forbidden_answer_patterns": [
|
||||||
|
"(?i)метадан",
|
||||||
|
"(?i)схем",
|
||||||
|
"(?i)объект[а-я]* 1с",
|
||||||
|
"(?i)регистр",
|
||||||
|
"(?i)уточните .* контрагент"
|
||||||
|
],
|
||||||
|
"criticality": "critical",
|
||||||
|
"semantic_tags": ["payments_followup", "counterparty_pronoun_resolution", "integrity_guard"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -274,6 +274,27 @@ function hasMatchedFactualAddressContinuationTarget(input, entryPoint) {
|
||||||
const targetIntent = toNonEmptyString(dialogContinuationContract?.target_intent);
|
const targetIntent = toNonEmptyString(dialogContinuationContract?.target_intent);
|
||||||
return Boolean(detectedIntent && targetIntent && detectedIntent === targetIntent);
|
return Boolean(detectedIntent && targetIntent && detectedIntent === targetIntent);
|
||||||
}
|
}
|
||||||
|
function hasMatchedFactualSuggestedIntentPivotTarget(input, entryPoint) {
|
||||||
|
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!hasEffectivelyFactualAddressReply(input)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||||
|
const dialogContinuationContract = toRecordObject(input.addressRuntimeMeta?.dialogContinuationContract) ??
|
||||||
|
toRecordObject(input.addressRuntimeMeta?.dialog_continuation_contract_v2);
|
||||||
|
const targetIntent = toNonEmptyString(dialogContinuationContract?.target_intent);
|
||||||
|
const decision = toNonEmptyString(dialogContinuationContract?.decision);
|
||||||
|
const selectionMode = toNonEmptyString(dialogContinuationContract?.intent_selection_mode);
|
||||||
|
const suggestedPivotSignal = dialogContinuationContract?.suggested_intent_pivot_signal === true;
|
||||||
|
return Boolean(detectedIntent &&
|
||||||
|
targetIntent &&
|
||||||
|
detectedIntent === targetIntent &&
|
||||||
|
(decision === "switch_to_suggested" ||
|
||||||
|
selectionMode === "switch_to_suggested_intent" ||
|
||||||
|
suggestedPivotSignal));
|
||||||
|
}
|
||||||
function hasFullConfirmedFactualAddressReply(input, entryPoint) {
|
function hasFullConfirmedFactualAddressReply(input, entryPoint) {
|
||||||
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -309,6 +330,7 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
||||||
const alignedFactualAddressReply = hasAlignedFactualAddressReply(input, entryPoint);
|
const alignedFactualAddressReply = hasAlignedFactualAddressReply(input, entryPoint);
|
||||||
const semanticConflictWithDiscoveryTurnMeaning = hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint);
|
const semanticConflictWithDiscoveryTurnMeaning = hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint);
|
||||||
const matchedFactualAddressContinuationTarget = hasMatchedFactualAddressContinuationTarget(input, entryPoint);
|
const matchedFactualAddressContinuationTarget = hasMatchedFactualAddressContinuationTarget(input, entryPoint);
|
||||||
|
const matchedFactualSuggestedIntentPivotTarget = hasMatchedFactualSuggestedIntentPivotTarget(input, entryPoint);
|
||||||
const fullConfirmedFactualAddressReply = hasFullConfirmedFactualAddressReply(input, entryPoint);
|
const fullConfirmedFactualAddressReply = hasFullConfirmedFactualAddressReply(input, entryPoint);
|
||||||
const runtimeAdjustedExactReply = hasRuntimeAdjustedExactReply(input, entryPoint);
|
const runtimeAdjustedExactReply = hasRuntimeAdjustedExactReply(input, entryPoint);
|
||||||
if (!entryPoint) {
|
if (!entryPoint) {
|
||||||
|
|
@ -335,6 +357,9 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
||||||
if (matchedFactualAddressContinuationTarget) {
|
if (matchedFactualAddressContinuationTarget) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_factual_address_continuation_target");
|
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_factual_address_continuation_target");
|
||||||
}
|
}
|
||||||
|
if (matchedFactualSuggestedIntentPivotTarget) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_factual_suggested_intent_pivot_target");
|
||||||
|
}
|
||||||
if (fullConfirmedFactualAddressReply) {
|
if (fullConfirmedFactualAddressReply) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_full_confirmed_factual_address_reply");
|
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_full_confirmed_factual_address_reply");
|
||||||
}
|
}
|
||||||
|
|
@ -360,6 +385,7 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
||||||
(unsupportedBoundary || discoveryReadyChatCandidate || discoveryReadyDeepCandidate || discoveryReadyAddressCandidate) &&
|
(unsupportedBoundary || discoveryReadyChatCandidate || discoveryReadyDeepCandidate || discoveryReadyAddressCandidate) &&
|
||||||
!alignedFactualAddressReply &&
|
!alignedFactualAddressReply &&
|
||||||
!matchedFactualAddressContinuationTarget &&
|
!matchedFactualAddressContinuationTarget &&
|
||||||
|
!matchedFactualSuggestedIntentPivotTarget &&
|
||||||
!fullConfirmedFactualAddressReply &&
|
!fullConfirmedFactualAddressReply &&
|
||||||
!runtimeAdjustedExactReply &&
|
!runtimeAdjustedExactReply &&
|
||||||
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
|
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,78 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
function normalizeFollowupText(value) {
|
function normalizeFollowupText(value) {
|
||||||
return deps.compactWhitespace(deps.repairAddressMojibake(String(value ?? "")).toLowerCase()).replace(/ё/g, "е");
|
return deps.compactWhitespace(deps.repairAddressMojibake(String(value ?? "")).toLowerCase()).replace(/ё/g, "е");
|
||||||
}
|
}
|
||||||
|
function hasBankOperationsPivotCue(text) {
|
||||||
|
const normalized = normalizeFollowupText(text);
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(?:платеж|платёж|банк|банковск|операц|выписк|поступлен|списан)/iu.test(normalized);
|
||||||
|
}
|
||||||
|
function hasContractsPivotCue(text) {
|
||||||
|
const normalized = normalizeFollowupText(text);
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(?:РґРѕРіРѕРІРѕСЂ)/iu.test(normalized);
|
||||||
|
}
|
||||||
|
function hasDocumentsPivotCue(text) {
|
||||||
|
const normalized = normalizeFollowupText(text);
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(?:документ|счет|счёт|накладн|акт)/iu.test(normalized);
|
||||||
|
}
|
||||||
|
function hasReadableBankOperationsPivotCue(text) {
|
||||||
|
const normalized = normalizeFollowupText(text).replace(/ё/g, "е");
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(?:платеж|оплат|банк|банковск|операц|поступлен|списан|выписк|перевод|payment|bank|transaction)/iu.test(normalized);
|
||||||
|
}
|
||||||
|
function hasReadableContractsPivotCue(text) {
|
||||||
|
const normalized = normalizeFollowupText(text).replace(/ё/g, "е");
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(?:договор|контракт|соглашен|contract|agreement)/iu.test(normalized);
|
||||||
|
}
|
||||||
|
function hasReadableDocumentsPivotCue(text) {
|
||||||
|
const normalized = normalizeFollowupText(text).replace(/ё/g, "е");
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(?:документ|счет|счет-фактур|накладн|акт|реализац|document|invoice|receipt)/iu.test(normalized);
|
||||||
|
}
|
||||||
|
function selectSuggestedIntentByPivotCue(suggestedIntents, userMessage, alternateMessage = null) {
|
||||||
|
if (!Array.isArray(suggestedIntents) || suggestedIntents.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const samples = [userMessage, alternateMessage].filter((item) => deps.toNonEmptyString(item));
|
||||||
|
if (samples.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (suggestedIntents.includes("bank_operations_by_counterparty") &&
|
||||||
|
samples.some((sample) => hasBankOperationsPivotCue(sample) || hasReadableBankOperationsPivotCue(sample))) {
|
||||||
|
return "bank_operations_by_counterparty";
|
||||||
|
}
|
||||||
|
if (suggestedIntents.includes("bank_operations_by_contract") &&
|
||||||
|
samples.some((sample) => hasBankOperationsPivotCue(sample) || hasReadableBankOperationsPivotCue(sample))) {
|
||||||
|
return "bank_operations_by_contract";
|
||||||
|
}
|
||||||
|
if (suggestedIntents.includes("list_contracts_by_counterparty") &&
|
||||||
|
samples.some((sample) => hasContractsPivotCue(sample) || hasReadableContractsPivotCue(sample))) {
|
||||||
|
return "list_contracts_by_counterparty";
|
||||||
|
}
|
||||||
|
if (suggestedIntents.includes("list_documents_by_counterparty") &&
|
||||||
|
samples.some((sample) => hasDocumentsPivotCue(sample) || hasReadableDocumentsPivotCue(sample))) {
|
||||||
|
return "list_documents_by_counterparty";
|
||||||
|
}
|
||||||
|
if (suggestedIntents.includes("list_documents_by_contract") &&
|
||||||
|
samples.some((sample) => hasDocumentsPivotCue(sample) || hasReadableDocumentsPivotCue(sample))) {
|
||||||
|
return "list_documents_by_contract";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
function hasSelectedObjectInventoryScopeSignal(text) {
|
function hasSelectedObjectInventoryScopeSignal(text) {
|
||||||
const normalized = normalizeFollowupText(text);
|
const normalized = normalizeFollowupText(text);
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
|
|
@ -352,6 +424,10 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
const carryoverSourceDebug = previousAddressDebug ??
|
const carryoverSourceDebug = previousAddressDebug ??
|
||||||
(hasOrganizationClarificationContinuation ? lastOrganizationClarificationDebug : null);
|
(hasOrganizationClarificationContinuation ? lastOrganizationClarificationDebug : null);
|
||||||
const followupOffer = carryoverSourceDebug ? deps.buildAddressFollowupOffer(carryoverSourceDebug) : null;
|
const followupOffer = carryoverSourceDebug ? deps.buildAddressFollowupOffer(carryoverSourceDebug) : null;
|
||||||
|
const suggestedIntentFromPivotCue = selectSuggestedIntentByPivotCue(Array.isArray(followupOffer?.suggested_intents) ? followupOffer.suggested_intents : [], userMessage, alternateMessage);
|
||||||
|
const hasSuggestedIntentPivotSignal = Boolean(previousAddressDebug) &&
|
||||||
|
Boolean(followupOffer?.enabled) &&
|
||||||
|
Boolean(suggestedIntentFromPivotCue);
|
||||||
const hasImplicitContinuationSignal = Boolean(previousAddressDebug) &&
|
const hasImplicitContinuationSignal = Boolean(previousAddressDebug) &&
|
||||||
Boolean(followupOffer?.enabled) &&
|
Boolean(followupOffer?.enabled) &&
|
||||||
(deps.isImplicitAddressContinuationByLlm(userMessage, llmPreDecomposeMeta) ||
|
(deps.isImplicitAddressContinuationByLlm(userMessage, llmPreDecomposeMeta) ||
|
||||||
|
|
@ -423,6 +499,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
hasAlternateIndexReferenceSignal ||
|
hasAlternateIndexReferenceSignal ||
|
||||||
hasOrganizationClarificationContinuation ||
|
hasOrganizationClarificationContinuation ||
|
||||||
hasImplicitContinuationSignal ||
|
hasImplicitContinuationSignal ||
|
||||||
|
hasSuggestedIntentPivotSignal ||
|
||||||
inventoryShortFollowupPrimary ||
|
inventoryShortFollowupPrimary ||
|
||||||
inventoryShortFollowupAlternate ||
|
inventoryShortFollowupAlternate ||
|
||||||
hasInventoryRootTemporalFollowupPrimary ||
|
hasInventoryRootTemporalFollowupPrimary ||
|
||||||
|
|
@ -575,9 +652,9 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
if (debtRoleSwapIntent) {
|
if (debtRoleSwapIntent) {
|
||||||
previousIntent = debtRoleSwapIntent;
|
previousIntent = debtRoleSwapIntent;
|
||||||
}
|
}
|
||||||
if (hasImplicitContinuationSignal) {
|
if (hasImplicitContinuationSignal || hasSuggestedIntentPivotSignal) {
|
||||||
const suggestedIntent = Array.isArray(followupOffer?.suggested_intents)
|
const suggestedIntent = Array.isArray(followupOffer?.suggested_intents)
|
||||||
? deps.toNonEmptyString(followupOffer.suggested_intents[0])
|
? suggestedIntentFromPivotCue ?? deps.toNonEmptyString(followupOffer.suggested_intents[0])
|
||||||
: null;
|
: null;
|
||||||
const keepPreviousIntent = shouldKeepPreviousIntentForShortCounterpartyRetargetV2(userMessage, sourceIntent);
|
const keepPreviousIntent = shouldKeepPreviousIntentForShortCounterpartyRetargetV2(userMessage, sourceIntent);
|
||||||
if (suggestedIntent && !keepPreviousIntent) {
|
if (suggestedIntent && !keepPreviousIntent) {
|
||||||
|
|
@ -789,7 +866,8 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
previousAddressAnchor: previousAnchor,
|
previousAddressAnchor: previousAnchor,
|
||||||
previousSourceIntent: sourceIntent,
|
previousSourceIntent: sourceIntent,
|
||||||
followupSelectionMode,
|
followupSelectionMode,
|
||||||
hasImplicitContinuationSignal
|
hasImplicitContinuationSignal,
|
||||||
|
hasSuggestedIntentPivotSignal
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function buildAddressDialogContinuationContractV2(userMessage, effectiveMessage, carryoverMeta, llmPreDecomposeMeta) {
|
function buildAddressDialogContinuationContractV2(userMessage, effectiveMessage, carryoverMeta, llmPreDecomposeMeta) {
|
||||||
|
|
@ -809,6 +887,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
? carryoverTargetIntent ?? rootIntent ?? explicitIntent ?? null
|
? carryoverTargetIntent ?? rootIntent ?? explicitIntent ?? null
|
||||||
: carryoverTargetIntent ?? explicitIntent ?? deps.toNonEmptyString(carryoverMeta?.previousAddressIntent) ?? null;
|
: carryoverTargetIntent ?? explicitIntent ?? deps.toNonEmptyString(carryoverMeta?.previousAddressIntent) ?? null;
|
||||||
const hasImplicitContinuationSignal = Boolean(carryoverMeta?.hasImplicitContinuationSignal);
|
const hasImplicitContinuationSignal = Boolean(carryoverMeta?.hasImplicitContinuationSignal);
|
||||||
|
const hasSuggestedIntentPivotSignal = Boolean(carryoverMeta?.hasSuggestedIntentPivotSignal);
|
||||||
const rewrittenByPredecompose = deps.compactWhitespace(sourceMessage.toLowerCase()) !== deps.compactWhitespace(canonicalMessage.toLowerCase());
|
const rewrittenByPredecompose = deps.compactWhitespace(sourceMessage.toLowerCase()) !== deps.compactWhitespace(canonicalMessage.toLowerCase());
|
||||||
const hasExplicitIntent = Boolean(explicitIntent);
|
const hasExplicitIntent = Boolean(explicitIntent);
|
||||||
const decision = !hasFollowupContext
|
const decision = !hasFollowupContext
|
||||||
|
|
@ -823,6 +902,9 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
if (hasImplicitContinuationSignal) {
|
if (hasImplicitContinuationSignal) {
|
||||||
reasons.push("implicit_continuation_by_llm");
|
reasons.push("implicit_continuation_by_llm");
|
||||||
}
|
}
|
||||||
|
if (hasSuggestedIntentPivotSignal) {
|
||||||
|
reasons.push("suggested_intent_followup_pivot");
|
||||||
|
}
|
||||||
if (rewrittenByPredecompose) {
|
if (rewrittenByPredecompose) {
|
||||||
reasons.push("effective_message_rewritten_by_predecompose");
|
reasons.push("effective_message_rewritten_by_predecompose");
|
||||||
}
|
}
|
||||||
|
|
@ -847,7 +929,8 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
intent_selection_mode: selectionMode,
|
intent_selection_mode: selectionMode,
|
||||||
anchor_type: carryoverMeta?.followupContext?.previous_anchor_type ?? null,
|
anchor_type: carryoverMeta?.followupContext?.previous_anchor_type ?? null,
|
||||||
anchor_value: carryoverMeta?.followupContext?.previous_anchor_value ?? null,
|
anchor_value: carryoverMeta?.followupContext?.previous_anchor_value ?? null,
|
||||||
implicit_continuation_signal: hasImplicitContinuationSignal
|
implicit_continuation_signal: hasImplicitContinuationSignal,
|
||||||
|
suggested_intent_pivot_signal: hasSuggestedIntentPivotSignal
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -391,6 +391,34 @@ function hasMatchedFactualAddressContinuationTarget(
|
||||||
return Boolean(detectedIntent && targetIntent && detectedIntent === targetIntent);
|
return Boolean(detectedIntent && targetIntent && detectedIntent === targetIntent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasMatchedFactualSuggestedIntentPivotTarget(
|
||||||
|
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
||||||
|
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
||||||
|
): boolean {
|
||||||
|
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!hasEffectivelyFactualAddressReply(input)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||||
|
const dialogContinuationContract =
|
||||||
|
toRecordObject(input.addressRuntimeMeta?.dialogContinuationContract) ??
|
||||||
|
toRecordObject(input.addressRuntimeMeta?.dialog_continuation_contract_v2);
|
||||||
|
const targetIntent = toNonEmptyString(dialogContinuationContract?.target_intent);
|
||||||
|
const decision = toNonEmptyString(dialogContinuationContract?.decision);
|
||||||
|
const selectionMode = toNonEmptyString(dialogContinuationContract?.intent_selection_mode);
|
||||||
|
const suggestedPivotSignal = dialogContinuationContract?.suggested_intent_pivot_signal === true;
|
||||||
|
return Boolean(
|
||||||
|
detectedIntent &&
|
||||||
|
targetIntent &&
|
||||||
|
detectedIntent === targetIntent &&
|
||||||
|
(decision === "switch_to_suggested" ||
|
||||||
|
selectionMode === "switch_to_suggested_intent" ||
|
||||||
|
suggestedPivotSignal)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function hasFullConfirmedFactualAddressReply(
|
function hasFullConfirmedFactualAddressReply(
|
||||||
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
||||||
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
||||||
|
|
@ -433,6 +461,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||||
const alignedFactualAddressReply = hasAlignedFactualAddressReply(input, entryPoint);
|
const alignedFactualAddressReply = hasAlignedFactualAddressReply(input, entryPoint);
|
||||||
const semanticConflictWithDiscoveryTurnMeaning = hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint);
|
const semanticConflictWithDiscoveryTurnMeaning = hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint);
|
||||||
const matchedFactualAddressContinuationTarget = hasMatchedFactualAddressContinuationTarget(input, entryPoint);
|
const matchedFactualAddressContinuationTarget = hasMatchedFactualAddressContinuationTarget(input, entryPoint);
|
||||||
|
const matchedFactualSuggestedIntentPivotTarget = hasMatchedFactualSuggestedIntentPivotTarget(input, entryPoint);
|
||||||
const fullConfirmedFactualAddressReply = hasFullConfirmedFactualAddressReply(input, entryPoint);
|
const fullConfirmedFactualAddressReply = hasFullConfirmedFactualAddressReply(input, entryPoint);
|
||||||
const runtimeAdjustedExactReply = hasRuntimeAdjustedExactReply(input, entryPoint);
|
const runtimeAdjustedExactReply = hasRuntimeAdjustedExactReply(input, entryPoint);
|
||||||
|
|
||||||
|
|
@ -460,6 +489,9 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||||
if (matchedFactualAddressContinuationTarget) {
|
if (matchedFactualAddressContinuationTarget) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_factual_address_continuation_target");
|
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_factual_address_continuation_target");
|
||||||
}
|
}
|
||||||
|
if (matchedFactualSuggestedIntentPivotTarget) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_factual_suggested_intent_pivot_target");
|
||||||
|
}
|
||||||
if (fullConfirmedFactualAddressReply) {
|
if (fullConfirmedFactualAddressReply) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_full_confirmed_factual_address_reply");
|
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_full_confirmed_factual_address_reply");
|
||||||
}
|
}
|
||||||
|
|
@ -493,6 +525,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||||
(unsupportedBoundary || discoveryReadyChatCandidate || discoveryReadyDeepCandidate || discoveryReadyAddressCandidate) &&
|
(unsupportedBoundary || discoveryReadyChatCandidate || discoveryReadyDeepCandidate || discoveryReadyAddressCandidate) &&
|
||||||
!alignedFactualAddressReply &&
|
!alignedFactualAddressReply &&
|
||||||
!matchedFactualAddressContinuationTarget &&
|
!matchedFactualAddressContinuationTarget &&
|
||||||
|
!matchedFactualSuggestedIntentPivotTarget &&
|
||||||
!fullConfirmedFactualAddressReply &&
|
!fullConfirmedFactualAddressReply &&
|
||||||
!runtimeAdjustedExactReply &&
|
!runtimeAdjustedExactReply &&
|
||||||
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
|
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,99 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
return deps.compactWhitespace(deps.repairAddressMojibake(String(value ?? "")).toLowerCase()).replace(/ё/g, "е");
|
return deps.compactWhitespace(deps.repairAddressMojibake(String(value ?? "")).toLowerCase()).replace(/ё/g, "е");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasBankOperationsPivotCue(text) {
|
||||||
|
const normalized = normalizeFollowupText(text);
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(?:платеж|платёж|банк|банковск|операц|выписк|поступлен|списан)/iu.test(
|
||||||
|
normalized
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasContractsPivotCue(text) {
|
||||||
|
const normalized = normalizeFollowupText(text);
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(?:РґРѕРіРѕРІРѕСЂ)/iu.test(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasDocumentsPivotCue(text) {
|
||||||
|
const normalized = normalizeFollowupText(text);
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(?:документ|счет|счёт|накладн|акт)/iu.test(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasReadableBankOperationsPivotCue(text) {
|
||||||
|
const normalized = normalizeFollowupText(text).replace(/ё/g, "е");
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(?:платеж|оплат|банк|банковск|операц|поступлен|списан|выписк|перевод|payment|bank|transaction)/iu.test(
|
||||||
|
normalized
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasReadableContractsPivotCue(text) {
|
||||||
|
const normalized = normalizeFollowupText(text).replace(/ё/g, "е");
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(?:договор|контракт|соглашен|contract|agreement)/iu.test(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasReadableDocumentsPivotCue(text) {
|
||||||
|
const normalized = normalizeFollowupText(text).replace(/ё/g, "е");
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /(?:документ|счет|счет-фактур|накладн|акт|реализац|document|invoice|receipt)/iu.test(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectSuggestedIntentByPivotCue(suggestedIntents, userMessage, alternateMessage = null) {
|
||||||
|
if (!Array.isArray(suggestedIntents) || suggestedIntents.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const samples = [userMessage, alternateMessage].filter((item) => deps.toNonEmptyString(item));
|
||||||
|
if (samples.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
suggestedIntents.includes("bank_operations_by_counterparty") &&
|
||||||
|
samples.some((sample) => hasBankOperationsPivotCue(sample) || hasReadableBankOperationsPivotCue(sample))
|
||||||
|
) {
|
||||||
|
return "bank_operations_by_counterparty";
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
suggestedIntents.includes("bank_operations_by_contract") &&
|
||||||
|
samples.some((sample) => hasBankOperationsPivotCue(sample) || hasReadableBankOperationsPivotCue(sample))
|
||||||
|
) {
|
||||||
|
return "bank_operations_by_contract";
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
suggestedIntents.includes("list_contracts_by_counterparty") &&
|
||||||
|
samples.some((sample) => hasContractsPivotCue(sample) || hasReadableContractsPivotCue(sample))
|
||||||
|
) {
|
||||||
|
return "list_contracts_by_counterparty";
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
suggestedIntents.includes("list_documents_by_counterparty") &&
|
||||||
|
samples.some((sample) => hasDocumentsPivotCue(sample) || hasReadableDocumentsPivotCue(sample))
|
||||||
|
) {
|
||||||
|
return "list_documents_by_counterparty";
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
suggestedIntents.includes("list_documents_by_contract") &&
|
||||||
|
samples.some((sample) => hasDocumentsPivotCue(sample) || hasReadableDocumentsPivotCue(sample))
|
||||||
|
) {
|
||||||
|
return "list_documents_by_contract";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function hasSelectedObjectInventoryScopeSignal(text) {
|
function hasSelectedObjectInventoryScopeSignal(text) {
|
||||||
const normalized = normalizeFollowupText(text);
|
const normalized = normalizeFollowupText(text);
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
|
|
@ -478,6 +571,15 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
previousAddressDebug ??
|
previousAddressDebug ??
|
||||||
(hasOrganizationClarificationContinuation ? lastOrganizationClarificationDebug : null);
|
(hasOrganizationClarificationContinuation ? lastOrganizationClarificationDebug : null);
|
||||||
const followupOffer = carryoverSourceDebug ? deps.buildAddressFollowupOffer(carryoverSourceDebug) : null;
|
const followupOffer = carryoverSourceDebug ? deps.buildAddressFollowupOffer(carryoverSourceDebug) : null;
|
||||||
|
const suggestedIntentFromPivotCue = selectSuggestedIntentByPivotCue(
|
||||||
|
Array.isArray(followupOffer?.suggested_intents) ? followupOffer.suggested_intents : [],
|
||||||
|
userMessage,
|
||||||
|
alternateMessage
|
||||||
|
);
|
||||||
|
const hasSuggestedIntentPivotSignal =
|
||||||
|
Boolean(previousAddressDebug) &&
|
||||||
|
Boolean(followupOffer?.enabled) &&
|
||||||
|
Boolean(suggestedIntentFromPivotCue);
|
||||||
const hasImplicitContinuationSignal =
|
const hasImplicitContinuationSignal =
|
||||||
Boolean(previousAddressDebug) &&
|
Boolean(previousAddressDebug) &&
|
||||||
Boolean(followupOffer?.enabled) &&
|
Boolean(followupOffer?.enabled) &&
|
||||||
|
|
@ -588,6 +690,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
hasAlternateIndexReferenceSignal ||
|
hasAlternateIndexReferenceSignal ||
|
||||||
hasOrganizationClarificationContinuation ||
|
hasOrganizationClarificationContinuation ||
|
||||||
hasImplicitContinuationSignal ||
|
hasImplicitContinuationSignal ||
|
||||||
|
hasSuggestedIntentPivotSignal ||
|
||||||
inventoryShortFollowupPrimary ||
|
inventoryShortFollowupPrimary ||
|
||||||
inventoryShortFollowupAlternate ||
|
inventoryShortFollowupAlternate ||
|
||||||
hasInventoryRootTemporalFollowupPrimary ||
|
hasInventoryRootTemporalFollowupPrimary ||
|
||||||
|
|
@ -809,9 +912,9 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
if (debtRoleSwapIntent) {
|
if (debtRoleSwapIntent) {
|
||||||
previousIntent = debtRoleSwapIntent;
|
previousIntent = debtRoleSwapIntent;
|
||||||
}
|
}
|
||||||
if (hasImplicitContinuationSignal) {
|
if (hasImplicitContinuationSignal || hasSuggestedIntentPivotSignal) {
|
||||||
const suggestedIntent = Array.isArray(followupOffer?.suggested_intents)
|
const suggestedIntent = Array.isArray(followupOffer?.suggested_intents)
|
||||||
? deps.toNonEmptyString(followupOffer.suggested_intents[0])
|
? suggestedIntentFromPivotCue ?? deps.toNonEmptyString(followupOffer.suggested_intents[0])
|
||||||
: null;
|
: null;
|
||||||
const keepPreviousIntent = shouldKeepPreviousIntentForShortCounterpartyRetargetV2(userMessage, sourceIntent);
|
const keepPreviousIntent = shouldKeepPreviousIntentForShortCounterpartyRetargetV2(userMessage, sourceIntent);
|
||||||
if (suggestedIntent && !keepPreviousIntent) {
|
if (suggestedIntent && !keepPreviousIntent) {
|
||||||
|
|
@ -1124,7 +1227,8 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
previousAddressAnchor: previousAnchor,
|
previousAddressAnchor: previousAnchor,
|
||||||
previousSourceIntent: sourceIntent,
|
previousSourceIntent: sourceIntent,
|
||||||
followupSelectionMode,
|
followupSelectionMode,
|
||||||
hasImplicitContinuationSignal
|
hasImplicitContinuationSignal,
|
||||||
|
hasSuggestedIntentPivotSignal
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1151,6 +1255,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
? carryoverTargetIntent ?? rootIntent ?? explicitIntent ?? null
|
? carryoverTargetIntent ?? rootIntent ?? explicitIntent ?? null
|
||||||
: carryoverTargetIntent ?? explicitIntent ?? deps.toNonEmptyString(carryoverMeta?.previousAddressIntent) ?? null;
|
: carryoverTargetIntent ?? explicitIntent ?? deps.toNonEmptyString(carryoverMeta?.previousAddressIntent) ?? null;
|
||||||
const hasImplicitContinuationSignal = Boolean(carryoverMeta?.hasImplicitContinuationSignal);
|
const hasImplicitContinuationSignal = Boolean(carryoverMeta?.hasImplicitContinuationSignal);
|
||||||
|
const hasSuggestedIntentPivotSignal = Boolean(carryoverMeta?.hasSuggestedIntentPivotSignal);
|
||||||
const rewrittenByPredecompose =
|
const rewrittenByPredecompose =
|
||||||
deps.compactWhitespace(sourceMessage.toLowerCase()) !== deps.compactWhitespace(canonicalMessage.toLowerCase());
|
deps.compactWhitespace(sourceMessage.toLowerCase()) !== deps.compactWhitespace(canonicalMessage.toLowerCase());
|
||||||
const hasExplicitIntent = Boolean(explicitIntent);
|
const hasExplicitIntent = Boolean(explicitIntent);
|
||||||
|
|
@ -1166,6 +1271,9 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
if (hasImplicitContinuationSignal) {
|
if (hasImplicitContinuationSignal) {
|
||||||
reasons.push("implicit_continuation_by_llm");
|
reasons.push("implicit_continuation_by_llm");
|
||||||
}
|
}
|
||||||
|
if (hasSuggestedIntentPivotSignal) {
|
||||||
|
reasons.push("suggested_intent_followup_pivot");
|
||||||
|
}
|
||||||
if (rewrittenByPredecompose) {
|
if (rewrittenByPredecompose) {
|
||||||
reasons.push("effective_message_rewritten_by_predecompose");
|
reasons.push("effective_message_rewritten_by_predecompose");
|
||||||
}
|
}
|
||||||
|
|
@ -1190,7 +1298,8 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
intent_selection_mode: selectionMode,
|
intent_selection_mode: selectionMode,
|
||||||
anchor_type: carryoverMeta?.followupContext?.previous_anchor_type ?? null,
|
anchor_type: carryoverMeta?.followupContext?.previous_anchor_type ?? null,
|
||||||
anchor_value: carryoverMeta?.followupContext?.previous_anchor_value ?? null,
|
anchor_value: carryoverMeta?.followupContext?.previous_anchor_value ?? null,
|
||||||
implicit_continuation_signal: hasImplicitContinuationSignal
|
implicit_continuation_signal: hasImplicitContinuationSignal,
|
||||||
|
suggested_intent_pivot_signal: hasSuggestedIntentPivotSignal
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -649,6 +649,9 @@ describe("assistant address follow-up carryover", () => {
|
||||||
user_message: followupMessage,
|
user_message: followupMessage,
|
||||||
useMock: true
|
useMock: true
|
||||||
} as any);
|
} as any);
|
||||||
|
if (second.reply_type !== "factual") {
|
||||||
|
throw new Error(JSON.stringify({ calls, secondReplyType: second.reply_type, secondDebug: second.debug }, null, 2));
|
||||||
|
}
|
||||||
expect(second.ok).toBe(true);
|
expect(second.ok).toBe(true);
|
||||||
expect(second.reply_type).toBe("factual");
|
expect(second.reply_type).toBe("factual");
|
||||||
|
|
||||||
|
|
@ -727,6 +730,9 @@ describe("assistant address follow-up carryover", () => {
|
||||||
user_message: followupMessage,
|
user_message: followupMessage,
|
||||||
useMock: true
|
useMock: true
|
||||||
} as any);
|
} as any);
|
||||||
|
if (second.reply_type !== "factual") {
|
||||||
|
throw new Error(JSON.stringify({ calls, secondReplyType: second.reply_type, secondDebug: second.debug }, null, 2));
|
||||||
|
}
|
||||||
expect(second.ok).toBe(true);
|
expect(second.ok).toBe(true);
|
||||||
expect(second.reply_type).toBe("factual");
|
expect(second.reply_type).toBe("factual");
|
||||||
|
|
||||||
|
|
@ -826,6 +832,9 @@ describe("assistant address follow-up carryover", () => {
|
||||||
useMock: true
|
useMock: true
|
||||||
} as any);
|
} as any);
|
||||||
expect(second.ok).toBe(true);
|
expect(second.ok).toBe(true);
|
||||||
|
if (second.reply_type !== "factual") {
|
||||||
|
throw new Error(JSON.stringify({ calls, secondReplyType: second.reply_type, secondDebug: second.debug }, null, 2));
|
||||||
|
}
|
||||||
expect(second.reply_type).toBe("factual");
|
expect(second.reply_type).toBe("factual");
|
||||||
expect(second.debug?.address_retry_audit?.attempted).toBe(false);
|
expect(second.debug?.address_retry_audit?.attempted).toBe(false);
|
||||||
|
|
||||||
|
|
@ -834,6 +843,96 @@ describe("assistant address follow-up carryover", () => {
|
||||||
expect(normalizerService.normalize).not.toHaveBeenCalled();
|
expect(normalizerService.normalize).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("switches from document drilldown to bank operations on a pronoun payment follow-up", async () => {
|
||||||
|
const calls: Array<{ message: string; options?: any }> = [];
|
||||||
|
const firstMessage = "покажи документы по жуковке 51";
|
||||||
|
const followupMessage = "а по нему платежи?";
|
||||||
|
|
||||||
|
const addressQueryService = {
|
||||||
|
tryHandle: vi.fn(async (message: string, options?: any) => {
|
||||||
|
calls.push({ message, options });
|
||||||
|
if (message === followupMessage && !options?.followupContext) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (options?.followupContext?.previous_intent === "bank_operations_by_counterparty") {
|
||||||
|
return buildAddressLaneResult({
|
||||||
|
reply_text: "Собран список банковских операций по контрагенту.",
|
||||||
|
debug: {
|
||||||
|
...buildAddressLaneResult().debug,
|
||||||
|
detected_intent: "bank_operations_by_counterparty",
|
||||||
|
selected_recipe: "address_bank_operations_by_counterparty_v1",
|
||||||
|
extracted_filters: {
|
||||||
|
sort: "period_desc",
|
||||||
|
limit: 20,
|
||||||
|
counterparty: "жуковке 51"
|
||||||
|
},
|
||||||
|
anchor_type: "counterparty",
|
||||||
|
anchor_value_raw: "жуковке 51",
|
||||||
|
anchor_value_resolved: "ТСЖ \\Жуковка 51\\",
|
||||||
|
reasons: ["address_action_detected", "bank_ops_by_counterparty_signal_detected", "address_followup_context_applied"]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return buildAddressLaneResult({
|
||||||
|
debug: {
|
||||||
|
...buildAddressLaneResult().debug,
|
||||||
|
detected_intent: "list_documents_by_counterparty",
|
||||||
|
selected_recipe: "address_documents_by_counterparty_v1",
|
||||||
|
extracted_filters: {
|
||||||
|
sort: "period_desc",
|
||||||
|
limit: 20,
|
||||||
|
counterparty: "жуковке 51"
|
||||||
|
},
|
||||||
|
anchor_type: "counterparty",
|
||||||
|
anchor_value_raw: "жуковке 51",
|
||||||
|
anchor_value_resolved: "ТСЖ \\Жуковка 51\\"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const normalizerService = {
|
||||||
|
normalize: vi.fn(async () => ({
|
||||||
|
assistant_reply: "normalizer_fallback_should_not_be_used",
|
||||||
|
reply_type: "partial_coverage",
|
||||||
|
debug: {}
|
||||||
|
}))
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const sessions = new AssistantSessionStore();
|
||||||
|
const service = new AssistantService(
|
||||||
|
normalizerService,
|
||||||
|
sessions as any,
|
||||||
|
{} as any,
|
||||||
|
{ persistSession: vi.fn() } as any,
|
||||||
|
addressQueryService
|
||||||
|
);
|
||||||
|
|
||||||
|
const sessionId = `asst-address-docs-to-bank-pronoun-${Date.now()}`;
|
||||||
|
const first = await service.handleMessage({
|
||||||
|
session_id: sessionId,
|
||||||
|
user_message: firstMessage,
|
||||||
|
useMock: true
|
||||||
|
} as any);
|
||||||
|
expect(first.ok).toBe(true);
|
||||||
|
expect(first.reply_type).toBe("factual");
|
||||||
|
|
||||||
|
const second = await service.handleMessage({
|
||||||
|
session_id: sessionId,
|
||||||
|
user_message: followupMessage,
|
||||||
|
useMock: true
|
||||||
|
} as any);
|
||||||
|
expect(second.ok).toBe(true);
|
||||||
|
expect(second.reply_type).toBe("factual");
|
||||||
|
|
||||||
|
expect(calls).toHaveLength(2);
|
||||||
|
expect(calls[1].message).toMatch(/банковские операции|платежи/i);
|
||||||
|
expect(calls[1].options?.followupContext?.previous_intent).toBe("bank_operations_by_counterparty");
|
||||||
|
expect(calls[1].options?.followupContext?.target_intent).toBe("bank_operations_by_counterparty");
|
||||||
|
expect(calls[1].options?.followupContext?.previous_anchor_type).toBe("counterparty");
|
||||||
|
expect(normalizerService.normalize).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("reuses last real address context after intermediate clarification fallback", async () => {
|
it("reuses last real address context after intermediate clarification fallback", async () => {
|
||||||
const calls: Array<{ message: string; options?: any }> = [];
|
const calls: Array<{ message: string; options?: any }> = [];
|
||||||
const lifecycleFollowupMessage = "А кто из них новые?";
|
const lifecycleFollowupMessage = "А кто из них новые?";
|
||||||
|
|
|
||||||
|
|
@ -235,6 +235,56 @@ describe("assistant MCP discovery response policy", () => {
|
||||||
expect(result.reason_codes).toContain("mcp_discovery_response_policy_keep_factual_address_continuation_target");
|
expect(result.reason_codes).toContain("mcp_discovery_response_policy_keep_factual_address_continuation_target");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps factual suggested-intent pivot replies over discovery clarification candidates", () => {
|
||||||
|
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||||
|
currentReply: "Найдены банковские операции по контрагенту Жуковка 51.",
|
||||||
|
currentReplySource: "address_query_runtime_v1",
|
||||||
|
currentReplyType: "factual",
|
||||||
|
addressRuntimeMeta: {
|
||||||
|
detected_intent: "bank_operations_by_counterparty",
|
||||||
|
dialog_continuation_contract_v2: {
|
||||||
|
decision: "switch_to_suggested",
|
||||||
|
target_intent: "bank_operations_by_counterparty",
|
||||||
|
intent_selection_mode: "switch_to_suggested_intent",
|
||||||
|
suggested_intent_pivot_signal: true
|
||||||
|
},
|
||||||
|
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",
|
||||||
|
explicit_entity_candidates: ["ТСЖ \\Жуковка 51\\"],
|
||||||
|
unsupported_but_understood_family: "counterparty_value_or_turnover"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bridge: {
|
||||||
|
bridge_status: "answer_draft_ready",
|
||||||
|
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(result.applied).toBe(false);
|
||||||
|
expect(result.decision).toBe("keep_current_reply");
|
||||||
|
expect(result.reply_text).toBe("Найдены банковские операции по контрагенту Жуковка 51.");
|
||||||
|
expect(result.reason_codes).toContain("mcp_discovery_response_policy_keep_factual_suggested_intent_pivot_target");
|
||||||
|
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_candidate_applied");
|
||||||
|
});
|
||||||
|
|
||||||
it("overrides an exact ranking-shaped address reply when open-scope ranking still needs organization", () => {
|
it("overrides an exact ranking-shaped address reply when open-scope ranking still needs organization", () => {
|
||||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||||
currentReply:
|
currentReply:
|
||||||
|
|
|
||||||
|
|
@ -691,6 +691,52 @@ describe("assistantTransitionPolicy", () => {
|
||||||
expect(carryover?.followupSelectionMode).toBe("carry_previous_intent");
|
expect(carryover?.followupSelectionMode).toBe("carry_previous_intent");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("switches from documents to suggested bank operations on a pronoun follow-up with payment cue", () => {
|
||||||
|
const policy = buildPolicy({
|
||||||
|
findLastAddressAssistantItem: () => ({
|
||||||
|
text: "Собран список документов по контрагенту Жуковка 51.",
|
||||||
|
debug: {
|
||||||
|
detected_intent: "list_documents_by_counterparty",
|
||||||
|
extracted_filters: {
|
||||||
|
counterparty: "Р–СѓРєРѕРІРєР° 51"
|
||||||
|
},
|
||||||
|
anchor_type: "counterparty",
|
||||||
|
anchor_value_resolved: "РўРЎР– \\Р–СѓРєРѕРІРєР° 51\\"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
buildAddressFollowupOffer: () => ({
|
||||||
|
enabled: true,
|
||||||
|
source_intent: "list_documents_by_counterparty",
|
||||||
|
suggested_intents: ["bank_operations_by_counterparty", "list_contracts_by_counterparty"]
|
||||||
|
}),
|
||||||
|
hasAddressFollowupContextSignal: () => true,
|
||||||
|
hasReferentialPointer: () => true
|
||||||
|
});
|
||||||
|
|
||||||
|
const carryover = policy.resolveAddressFollowupCarryoverContext("а по нему платежи?", [], null, null, null);
|
||||||
|
|
||||||
|
expect(carryover?.followupContext?.previous_intent).toBe("bank_operations_by_counterparty");
|
||||||
|
expect(carryover?.followupContext?.target_intent).toBe("bank_operations_by_counterparty");
|
||||||
|
expect(carryover?.followupContext?.previous_anchor_type).toBe("counterparty");
|
||||||
|
expect(carryover?.followupSelectionMode).toBe("switch_to_suggested_intent");
|
||||||
|
expect(carryover?.hasSuggestedIntentPivotSignal).toBe(true);
|
||||||
|
|
||||||
|
const contract = policy.buildAddressDialogContinuationContractV2(
|
||||||
|
"а по нему платежи?",
|
||||||
|
"Покажи платежи, связанные с этим объектом",
|
||||||
|
carryover,
|
||||||
|
{
|
||||||
|
predecomposeContract: {
|
||||||
|
intent: "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(contract.decision).toBe("switch_to_suggested");
|
||||||
|
expect(contract.target_intent).toBe("bank_operations_by_counterparty");
|
||||||
|
expect(contract.decision_reasons).toContain("suggested_intent_followup_pivot");
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps root-scoped carryover for foreign accounting pivot over inventory drilldown", () => {
|
it("keeps root-scoped carryover for foreign accounting pivot over inventory drilldown", () => {
|
||||||
const policy = buildPolicy({
|
const policy = buildPolicy({
|
||||||
findLastAddressAssistantItem: () => ({
|
findLastAddressAssistantItem: () => ({
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue