ARCH: стабилизировать continuity и защитить exact-ответы от discovery
This commit is contained in:
parent
b542b65b81
commit
429bd3d8ec
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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)) &&
|
||||
|
|
|
|||
|
|
@ -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,22 +154,30 @@ function createAssistantTurnMeaningPolicy(deps = {}) {
|
|||
? "receivables"
|
||||
: explicitIntentCandidate?.startsWith("payables_")
|
||||
? "payables"
|
||||
: explicitIntentCandidate?.startsWith("inventory_")
|
||||
? "inventory"
|
||||
: explicitIntentCandidate?.includes("counterparty")
|
||||
? "counterparty"
|
||||
: counterpartyTurnover?.family
|
||||
: explicitIntentCandidate?.startsWith("vat_")
|
||||
? "vat"
|
||||
: explicitIntentCandidate?.startsWith("inventory_")
|
||||
? "inventory"
|
||||
: explicitIntentCandidate?.includes("counterparty")
|
||||
? "counterparty"
|
||||
: null;
|
||||
: counterpartyTurnover?.family
|
||||
? "counterparty"
|
||||
: null;
|
||||
const askedActionFamily = explicitIntentCandidate === "receivables_confirmed_as_of_date" ||
|
||||
explicitIntentCandidate === "payables_confirmed_as_of_date" ||
|
||||
explicitIntentCandidate === "inventory_on_hand_as_of_date"
|
||||
? "confirmed_snapshot"
|
||||
: explicitIntentCandidate === "list_documents_by_counterparty"
|
||||
? "list_documents"
|
||||
: counterpartyTurnover?.family
|
||||
? "counterparty_value_or_turnover"
|
||||
: null;
|
||||
: 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
|
||||
? "counterparty_value_or_turnover"
|
||||
: null;
|
||||
const staleReplayForbidden = Boolean(unsupportedFamily || (counterpartyTurnover?.entity && !explicitIntentCandidate));
|
||||
return {
|
||||
schema_version: "assistant_turn_meaning_v1",
|
||||
|
|
|
|||
|
|
@ -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)) &&
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue