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 &&
|
turnInput?.should_run_discovery === true &&
|
||||||
(source === "address_lane" || source === "address_exact" || source === "address_query_runtime_v1"));
|
(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) {
|
function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
||||||
const currentReply = String(input.currentReply ?? "");
|
const currentReply = String(input.currentReply ?? "");
|
||||||
const currentReplySource = toNonEmptyString(input.currentReplySource) ?? toNonEmptyString(input.livingChatSource) ?? "unknown";
|
const currentReplySource = toNonEmptyString(input.currentReplySource) ?? toNonEmptyString(input.livingChatSource) ?? "unknown";
|
||||||
|
|
@ -99,6 +149,9 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
||||||
const discoveryReadyChatCandidate = isDiscoveryReadyChatCandidate(input, entryPoint);
|
const discoveryReadyChatCandidate = isDiscoveryReadyChatCandidate(input, entryPoint);
|
||||||
const discoveryReadyDeepCandidate = isDiscoveryReadyDeepCandidate(input, entryPoint);
|
const discoveryReadyDeepCandidate = isDiscoveryReadyDeepCandidate(input, entryPoint);
|
||||||
const discoveryReadyAddressCandidate = isDiscoveryReadyAddressCandidate(input, entryPoint);
|
const discoveryReadyAddressCandidate = isDiscoveryReadyAddressCandidate(input, entryPoint);
|
||||||
|
const alignedFactualAddressReply = hasAlignedFactualAddressReply(input, entryPoint);
|
||||||
|
const matchedFactualAddressContinuationTarget = hasMatchedFactualAddressContinuationTarget(input);
|
||||||
|
const fullConfirmedFactualAddressReply = hasFullConfirmedFactualAddressReply(input, entryPoint);
|
||||||
if (!entryPoint) {
|
if (!entryPoint) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_policy_no_entry_point");
|
pushReason(reasonCodes, "mcp_discovery_response_policy_no_entry_point");
|
||||||
}
|
}
|
||||||
|
|
@ -114,6 +167,15 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
||||||
if (!discoveryReadyAddressCandidate) {
|
if (!discoveryReadyAddressCandidate) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_policy_not_discovery_ready_address_candidate");
|
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)) {
|
if (!ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status)) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_status_not_allowed");
|
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_status_not_allowed");
|
||||||
}
|
}
|
||||||
|
|
@ -128,6 +190,9 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
||||||
}
|
}
|
||||||
const canApply = Boolean(entryPoint) &&
|
const canApply = Boolean(entryPoint) &&
|
||||||
(unsupportedBoundary || discoveryReadyChatCandidate || discoveryReadyDeepCandidate || discoveryReadyAddressCandidate) &&
|
(unsupportedBoundary || discoveryReadyChatCandidate || discoveryReadyDeepCandidate || discoveryReadyAddressCandidate) &&
|
||||||
|
!alignedFactualAddressReply &&
|
||||||
|
!matchedFactualAddressContinuationTarget &&
|
||||||
|
!fullConfirmedFactualAddressReply &&
|
||||||
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
|
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
|
||||||
candidate.eligible_for_future_hot_runtime &&
|
candidate.eligible_for_future_hot_runtime &&
|
||||||
Boolean(toNonEmptyString(candidate.reply_text)) &&
|
Boolean(toNonEmptyString(candidate.reply_text)) &&
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,10 @@ const SUPPORTED_ADDRESS_INTENTS = new Set([
|
||||||
"payables_confirmed_as_of_date",
|
"payables_confirmed_as_of_date",
|
||||||
"list_documents_by_counterparty",
|
"list_documents_by_counterparty",
|
||||||
"customer_revenue_and_payments",
|
"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) {
|
function fallbackCompactWhitespace(value) {
|
||||||
return String(value ?? "").replace(/\s+/g, " ").trim();
|
return String(value ?? "").replace(/\s+/g, " ").trim();
|
||||||
|
|
@ -85,8 +88,23 @@ function detectCounterpartyTurnoverFamily(text) {
|
||||||
"\u0434\u043e\u0445\u043e\u0434",
|
"\u0434\u043e\u0445\u043e\u0434",
|
||||||
"\u0431\u044b\u043b",
|
"\u0431\u044b\u043b",
|
||||||
"\u0431\u044b\u043b\u0430",
|
"\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",
|
"turnover",
|
||||||
"revenue"
|
"revenue",
|
||||||
|
"time",
|
||||||
|
"year",
|
||||||
|
"period",
|
||||||
|
"month",
|
||||||
|
"quarter"
|
||||||
]);
|
]);
|
||||||
const entity = rawEntity && !ignored.has(rawEntity) ? rawEntity : null;
|
const entity = rawEntity && !ignored.has(rawEntity) ? rawEntity : null;
|
||||||
return {
|
return {
|
||||||
|
|
@ -136,22 +154,30 @@ function createAssistantTurnMeaningPolicy(deps = {}) {
|
||||||
? "receivables"
|
? "receivables"
|
||||||
: explicitIntentCandidate?.startsWith("payables_")
|
: explicitIntentCandidate?.startsWith("payables_")
|
||||||
? "payables"
|
? "payables"
|
||||||
: explicitIntentCandidate?.startsWith("inventory_")
|
: explicitIntentCandidate?.startsWith("vat_")
|
||||||
? "inventory"
|
? "vat"
|
||||||
: explicitIntentCandidate?.includes("counterparty")
|
: explicitIntentCandidate?.startsWith("inventory_")
|
||||||
? "counterparty"
|
? "inventory"
|
||||||
: counterpartyTurnover?.family
|
: explicitIntentCandidate?.includes("counterparty")
|
||||||
? "counterparty"
|
? "counterparty"
|
||||||
: null;
|
: counterpartyTurnover?.family
|
||||||
|
? "counterparty"
|
||||||
|
: null;
|
||||||
const askedActionFamily = explicitIntentCandidate === "receivables_confirmed_as_of_date" ||
|
const askedActionFamily = explicitIntentCandidate === "receivables_confirmed_as_of_date" ||
|
||||||
explicitIntentCandidate === "payables_confirmed_as_of_date" ||
|
explicitIntentCandidate === "payables_confirmed_as_of_date" ||
|
||||||
explicitIntentCandidate === "inventory_on_hand_as_of_date"
|
explicitIntentCandidate === "inventory_on_hand_as_of_date"
|
||||||
? "confirmed_snapshot"
|
? "confirmed_snapshot"
|
||||||
: explicitIntentCandidate === "list_documents_by_counterparty"
|
: explicitIntentCandidate === "vat_liability_confirmed_for_tax_period"
|
||||||
? "list_documents"
|
? "confirmed_tax_period"
|
||||||
: counterpartyTurnover?.family
|
: explicitIntentCandidate === "vat_payable_confirmed_as_of_date"
|
||||||
? "counterparty_value_or_turnover"
|
? "confirmed_snapshot"
|
||||||
: null;
|
: 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));
|
const staleReplayForbidden = Boolean(unsupportedFamily || (counterpartyTurnover?.entity && !explicitIntentCandidate));
|
||||||
return {
|
return {
|
||||||
schema_version: "assistant_turn_meaning_v1",
|
schema_version: "assistant_turn_meaning_v1",
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ export type AssistantMcpDiscoveryResponsePolicyDecision = "apply_candidate" | "k
|
||||||
export interface ApplyAssistantMcpDiscoveryResponsePolicyInput {
|
export interface ApplyAssistantMcpDiscoveryResponsePolicyInput {
|
||||||
currentReply: string;
|
currentReply: string;
|
||||||
currentReplySource?: string | null;
|
currentReplySource?: string | null;
|
||||||
|
currentReplyType?: string | null;
|
||||||
livingChatSource?: string | null;
|
livingChatSource?: string | null;
|
||||||
modeDecisionReason?: string | null;
|
modeDecisionReason?: string | null;
|
||||||
addressRuntimeMeta?: Record<string, unknown> | 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(
|
export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||||
input: ApplyAssistantMcpDiscoveryResponsePolicyInput
|
input: ApplyAssistantMcpDiscoveryResponsePolicyInput
|
||||||
): AssistantMcpDiscoveryResponsePolicyResult {
|
): AssistantMcpDiscoveryResponsePolicyResult {
|
||||||
|
|
@ -164,6 +228,9 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||||
const discoveryReadyChatCandidate = isDiscoveryReadyChatCandidate(input, entryPoint);
|
const discoveryReadyChatCandidate = isDiscoveryReadyChatCandidate(input, entryPoint);
|
||||||
const discoveryReadyDeepCandidate = isDiscoveryReadyDeepCandidate(input, entryPoint);
|
const discoveryReadyDeepCandidate = isDiscoveryReadyDeepCandidate(input, entryPoint);
|
||||||
const discoveryReadyAddressCandidate = isDiscoveryReadyAddressCandidate(input, entryPoint);
|
const discoveryReadyAddressCandidate = isDiscoveryReadyAddressCandidate(input, entryPoint);
|
||||||
|
const alignedFactualAddressReply = hasAlignedFactualAddressReply(input, entryPoint);
|
||||||
|
const matchedFactualAddressContinuationTarget = hasMatchedFactualAddressContinuationTarget(input);
|
||||||
|
const fullConfirmedFactualAddressReply = hasFullConfirmedFactualAddressReply(input, entryPoint);
|
||||||
|
|
||||||
if (!entryPoint) {
|
if (!entryPoint) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_policy_no_entry_point");
|
pushReason(reasonCodes, "mcp_discovery_response_policy_no_entry_point");
|
||||||
|
|
@ -180,6 +247,15 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||||
if (!discoveryReadyAddressCandidate) {
|
if (!discoveryReadyAddressCandidate) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_policy_not_discovery_ready_address_candidate");
|
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)) {
|
if (!ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status)) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_status_not_allowed");
|
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_status_not_allowed");
|
||||||
}
|
}
|
||||||
|
|
@ -196,6 +272,9 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||||
const canApply =
|
const canApply =
|
||||||
Boolean(entryPoint) &&
|
Boolean(entryPoint) &&
|
||||||
(unsupportedBoundary || discoveryReadyChatCandidate || discoveryReadyDeepCandidate || discoveryReadyAddressCandidate) &&
|
(unsupportedBoundary || discoveryReadyChatCandidate || discoveryReadyDeepCandidate || discoveryReadyAddressCandidate) &&
|
||||||
|
!alignedFactualAddressReply &&
|
||||||
|
!matchedFactualAddressContinuationTarget &&
|
||||||
|
!fullConfirmedFactualAddressReply &&
|
||||||
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
|
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
|
||||||
candidate.eligible_for_future_hot_runtime &&
|
candidate.eligible_for_future_hot_runtime &&
|
||||||
Boolean(toNonEmptyString(candidate.reply_text)) &&
|
Boolean(toNonEmptyString(candidate.reply_text)) &&
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,10 @@ const SUPPORTED_ADDRESS_INTENTS = new Set([
|
||||||
"payables_confirmed_as_of_date",
|
"payables_confirmed_as_of_date",
|
||||||
"list_documents_by_counterparty",
|
"list_documents_by_counterparty",
|
||||||
"customer_revenue_and_payments",
|
"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) {
|
function fallbackCompactWhitespace(value) {
|
||||||
|
|
@ -89,8 +92,23 @@ function detectCounterpartyTurnoverFamily(text) {
|
||||||
"\u0434\u043e\u0445\u043e\u0434",
|
"\u0434\u043e\u0445\u043e\u0434",
|
||||||
"\u0431\u044b\u043b",
|
"\u0431\u044b\u043b",
|
||||||
"\u0431\u044b\u043b\u0430",
|
"\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",
|
"turnover",
|
||||||
"revenue"
|
"revenue",
|
||||||
|
"time",
|
||||||
|
"year",
|
||||||
|
"period",
|
||||||
|
"month",
|
||||||
|
"quarter"
|
||||||
]);
|
]);
|
||||||
const entity = rawEntity && !ignored.has(rawEntity) ? rawEntity : null;
|
const entity = rawEntity && !ignored.has(rawEntity) ? rawEntity : null;
|
||||||
return {
|
return {
|
||||||
|
|
@ -146,6 +164,8 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
|
||||||
? "receivables"
|
? "receivables"
|
||||||
: explicitIntentCandidate?.startsWith("payables_")
|
: explicitIntentCandidate?.startsWith("payables_")
|
||||||
? "payables"
|
? "payables"
|
||||||
|
: explicitIntentCandidate?.startsWith("vat_")
|
||||||
|
? "vat"
|
||||||
: explicitIntentCandidate?.startsWith("inventory_")
|
: explicitIntentCandidate?.startsWith("inventory_")
|
||||||
? "inventory"
|
? "inventory"
|
||||||
: explicitIntentCandidate?.includes("counterparty")
|
: explicitIntentCandidate?.includes("counterparty")
|
||||||
|
|
@ -158,6 +178,12 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
|
||||||
explicitIntentCandidate === "payables_confirmed_as_of_date" ||
|
explicitIntentCandidate === "payables_confirmed_as_of_date" ||
|
||||||
explicitIntentCandidate === "inventory_on_hand_as_of_date"
|
explicitIntentCandidate === "inventory_on_hand_as_of_date"
|
||||||
? "confirmed_snapshot"
|
? "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"
|
: explicitIntentCandidate === "list_documents_by_counterparty"
|
||||||
? "list_documents"
|
? "list_documents"
|
||||||
: counterpartyTurnover?.family
|
: counterpartyTurnover?.family
|
||||||
|
|
|
||||||
|
|
@ -113,11 +113,17 @@ describe("assistant MCP discovery response policy", () => {
|
||||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||||
currentReply: "stale exact route answer",
|
currentReply: "stale exact route answer",
|
||||||
currentReplySource: "address_query_runtime_v1",
|
currentReplySource: "address_query_runtime_v1",
|
||||||
|
currentReplyType: "factual",
|
||||||
addressRuntimeMeta: {
|
addressRuntimeMeta: {
|
||||||
|
detected_intent: "list_documents_by_counterparty",
|
||||||
assistant_mcp_discovery_entry_point_v1: entryPoint({
|
assistant_mcp_discovery_entry_point_v1: entryPoint({
|
||||||
turn_input: {
|
turn_input: {
|
||||||
adapter_status: "ready",
|
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");
|
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", () => {
|
it("keeps address lane answers when discovery was not requested for the current turn", () => {
|
||||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||||
currentReply: "supported exact route answer",
|
currentReply: "supported exact route answer",
|
||||||
|
|
|
||||||
|
|
@ -1013,4 +1013,123 @@ describe("assistantTransitionPolicy", () => {
|
||||||
|
|
||||||
expect(carryover).toBeNull();
|
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 { createAssistantTurnMeaningPolicy } from "../src/services/assistantTurnMeaningPolicy";
|
||||||
import { resolveAddressIntent } from "../src/services/addressIntentResolver";
|
import { resolveAddressIntent } from "../src/services/addressIntentResolver";
|
||||||
|
|
||||||
function buildPolicy() {
|
function buildPolicy(overrides: Record<string, unknown> = {}) {
|
||||||
return createAssistantTurnMeaningPolicy({
|
return createAssistantTurnMeaningPolicy({
|
||||||
compactWhitespace: (value: string) => String(value ?? "").replace(/\s+/g, " ").trim(),
|
compactWhitespace: (value: string) => String(value ?? "").replace(/\s+/g, " ").trim(),
|
||||||
repairAddressMojibake: (value: string) => value,
|
repairAddressMojibake: (value: string) => value,
|
||||||
|
|
@ -13,7 +13,8 @@ function buildPolicy() {
|
||||||
}
|
}
|
||||||
const text = String(value).trim();
|
const text = String(value).trim();
|
||||||
return text.length > 0 ? text : null;
|
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