409 lines
20 KiB
JavaScript
409 lines
20 KiB
JavaScript
"use strict";
|
||
Object.defineProperty(exports, "__esModule", { value: true });
|
||
exports.ROUTE_DISCIPLINE_RULE_TABLE = void 0;
|
||
exports.simulateDeterministicRouting = simulateDeterministicRouting;
|
||
exports.toRouteHintSummary = toRouteHintSummary;
|
||
exports.toRouterInput = toRouterInput;
|
||
function toRouteHintSummaryV1(normalized) {
|
||
return {
|
||
mode: "legacy_v1",
|
||
intent_class: normalized.intent_class,
|
||
route_hint: normalized.route_hint,
|
||
confidence: normalized.confidence.route_hint,
|
||
decision_flags: {
|
||
needs_cross_entity_join: normalized.requires.needs_cross_entity_join,
|
||
needs_causal_chain: normalized.requires.needs_causal_chain,
|
||
needs_exact_object_trace: normalized.requires.needs_exact_object_trace,
|
||
needs_ranking: normalized.requires.needs_ranking,
|
||
needs_anomaly_summary: normalized.requires.needs_anomaly_summary,
|
||
needs_runtime_truth: normalized.requires.needs_runtime_truth,
|
||
needs_period_cut: normalized.requires.needs_period_cut,
|
||
needs_evidence: normalized.requires.needs_evidence
|
||
},
|
||
period_scope: normalized.period_scope,
|
||
entities: {
|
||
domain_entities: normalized.domain_entities,
|
||
accounts_mentioned: normalized.accounts_mentioned,
|
||
documents_mentioned: normalized.documents_mentioned,
|
||
registers_mentioned: normalized.registers_mentioned
|
||
}
|
||
};
|
||
}
|
||
const ACCOUNT_HINT_PATTERN = /(?:\b(?:account|acct|schet|счет|сч)\s*[:#]?\s*(?:[1-9][0-9](?:[./-][0-9]{1,2})?)|\b(?:19|20|21|23|25|26|28|29|44|51|60|62|68)\b)/i;
|
||
const PERIOD_PATTERN = /\b20\d{2}(?:[-./](?:0[1-9]|1[0-2]))?\b/i;
|
||
const SYMPTOM_MARKER_PATTERN = /(?:\bsymptom\b|\banomaly\b|\bproblem\b|\bissue\b|\btail\b|\bhanging\b|\bblocked\b|\bincomplete\b|remains?\s+open|not\s+(?:confirmed|observed|resolved|closed)|не\s+(?:подтвержден|закрыт|наблюдается)|хвост|сбой|проблем)/i;
|
||
const LIFECYCLE_MARKER_PATTERN = /(?:\blifecycle\b|\bchain\b|\btransition\b|\bstep\b|\btrace\b|цепоч|этап|переход|связк|где\s+разрыв)/i;
|
||
const CHAIN_BREAK_PATTERN = /(?:\bbreak\b|\bbroken\b|\bgap\b|missing\s+(?:transition|step|link)|chain\s+break|разрыв|обрыв|нет\s+переход|не\s+дошл|не\s+наблюд)/i;
|
||
const PERIOD_IMPACT_PATTERN = /(?:period\s*close|month\s*close|month-end|residual|allocation|20[/-]44|закрыти|остатк|распредел|конец\s+месяц)/i;
|
||
const CAUSAL_PATTERN = /(?:\bwhy\b|\bbecause\b|\breason\b|explain\s+mechanism|почему|объясни|механизм|причин)/i;
|
||
const AMBIGUITY_PATTERN = /(?:\bmaybe\b|\bperhaps\b|not\s+sure|i\s+only\s+know|part\s+may\s+be\s+missing|возможно|может\s+быть|не\s+уверен|не\s+знаю|часть\s+цепочки\s+не\s+подтвержд)/i;
|
||
const TRANSLIT_PROBLEM_PATTERN = /(?:raschet|oplata|zakryt|nds|vychet|zatrat|ostatok|cepoch|perehod|pochemu|prichin|period)/i;
|
||
const DOMAIN_LEXICAL_ANCHOR_PATTERN = /(?:\b(?:settlement|payment|bank|supplier|customer|vat|nds|invoice|register|book|period\s*close|month\s*close|close\s*operation|allocation|residual|cost|expenses?)\b|оплат|расчет|ндс|сч[её]С‚.?фактур|книг[аи]|затрат|закрыт|остатк)/i;
|
||
exports.ROUTE_DISCIPLINE_RULE_TABLE = [
|
||
{
|
||
query_class: "exact_object_trace",
|
||
required_route: "live_mcp_drilldown",
|
||
allowed_fallback: ["no_route"],
|
||
forbidden_fallback: ["store_canonical", "hybrid_store_plus_live", "store_feature_risk", "batch_refresh_then_store"],
|
||
description: "Exact object trace queries always run via live drilldown."
|
||
},
|
||
{
|
||
query_class: "ranking_or_period_summary",
|
||
required_route: "batch_refresh_then_store",
|
||
allowed_fallback: ["no_route"],
|
||
forbidden_fallback: ["store_canonical", "hybrid_store_plus_live"],
|
||
description: "Ranking and period summary queries require analytical batch path."
|
||
},
|
||
{
|
||
query_class: "symptom_first",
|
||
required_route: "hybrid_store_plus_live",
|
||
allowed_fallback: ["no_route"],
|
||
forbidden_fallback: ["store_canonical"],
|
||
description: "Symptom-first intents are deterministically promoted to hybrid path."
|
||
},
|
||
{
|
||
query_class: "lifecycle_first",
|
||
required_route: "hybrid_store_plus_live",
|
||
allowed_fallback: ["no_route"],
|
||
forbidden_fallback: ["store_canonical"],
|
||
description: "Lifecycle-first intents are deterministically promoted to hybrid path."
|
||
},
|
||
{
|
||
query_class: "chain_break",
|
||
required_route: "hybrid_store_plus_live",
|
||
allowed_fallback: ["no_route"],
|
||
forbidden_fallback: ["store_canonical"],
|
||
description: "Chain-break intents are deterministically promoted to hybrid path."
|
||
},
|
||
{
|
||
query_class: "period_impact",
|
||
required_route: "hybrid_store_plus_live",
|
||
allowed_fallback: ["no_route"],
|
||
forbidden_fallback: ["store_canonical"],
|
||
description: "Period-impact problem intents are deterministically promoted to hybrid path."
|
||
},
|
||
{
|
||
query_class: "causal_query",
|
||
required_route: "hybrid_store_plus_live",
|
||
allowed_fallback: ["no_route"],
|
||
forbidden_fallback: ["store_canonical"],
|
||
description: "Causal/mechanism intents are deterministically promoted to hybrid path."
|
||
},
|
||
{
|
||
query_class: "mixed_ambiguity",
|
||
required_route: "hybrid_store_plus_live",
|
||
allowed_fallback: ["no_route"],
|
||
forbidden_fallback: ["store_canonical"],
|
||
description: "Mixed ambiguity keeps hybrid as primary route, with explicit no-route fallback."
|
||
},
|
||
{
|
||
query_class: "rule_check_without_symptom",
|
||
required_route: "store_feature_risk",
|
||
allowed_fallback: ["no_route"],
|
||
forbidden_fallback: ["store_canonical"],
|
||
description: "Rule checks without symptom/lifecycle signals run via risk profile path."
|
||
},
|
||
{
|
||
query_class: "canonical_fact_lookup",
|
||
required_route: "store_canonical",
|
||
allowed_fallback: ["no_route"],
|
||
forbidden_fallback: ["hybrid_store_plus_live"],
|
||
description: "Only plain factual lookups are allowed to stay on canonical path."
|
||
}
|
||
];
|
||
const ROUTE_DISCIPLINE_RULE_MAP = new Map(exports.ROUTE_DISCIPLINE_RULE_TABLE.map((item) => [item.query_class, item]));
|
||
function mergedFragmentText(fragment) {
|
||
return `${fragment.raw_fragment_text ?? ""} ${fragment.normalized_fragment_text ?? ""}`.toLowerCase();
|
||
}
|
||
function hasLifecycleDomainHint(fragment, lowerText) {
|
||
const accountHints = Array.isArray(fragment.account_hints) ? fragment.account_hints.map((item) => String(item)) : [];
|
||
if (accountHints.some((item) => /^(97|01|02|08|19|20|21|23|25|26|28|29|44|68(?:\.\d+)?|51|60|62)$/.test(item))) {
|
||
return true;
|
||
}
|
||
return (fragment.candidate_labels.includes("anomaly_probe") ||
|
||
fragment.candidate_labels.includes("period_close_risk") ||
|
||
PERIOD_IMPACT_PATTERN.test(lowerText));
|
||
}
|
||
function hasSymptomSignal(fragment, lowerText) {
|
||
return (fragment.flags.asks_for_anomaly_scan ||
|
||
fragment.candidate_labels.includes("anomaly_probe") ||
|
||
fragment.candidate_labels.includes("period_close_risk") ||
|
||
SYMPTOM_MARKER_PATTERN.test(lowerText) ||
|
||
TRANSLIT_PROBLEM_PATTERN.test(lowerText));
|
||
}
|
||
function hasLifecycleSignal(fragment, lowerText) {
|
||
return (fragment.flags.asks_for_chain_explanation ||
|
||
fragment.flags.mentions_period_close_context ||
|
||
LIFECYCLE_MARKER_PATTERN.test(lowerText) ||
|
||
hasLifecycleDomainHint(fragment, lowerText));
|
||
}
|
||
function hasChainBreakSignal(lowerText) {
|
||
return CHAIN_BREAK_PATTERN.test(lowerText);
|
||
}
|
||
function hasPeriodImpactSignal(lowerText) {
|
||
return PERIOD_IMPACT_PATTERN.test(lowerText);
|
||
}
|
||
function hasCausalSignal(lowerText) {
|
||
return CAUSAL_PATTERN.test(lowerText);
|
||
}
|
||
function hasAmbiguitySignal(fragment, lowerText) {
|
||
return (AMBIGUITY_PATTERN.test(lowerText) ||
|
||
fragment.confidence === "low" ||
|
||
fragment.domain_relevance === "unclear" ||
|
||
fragment.business_scope === "unclear");
|
||
}
|
||
function hasAccountOrPeriodAnchor(fragment, lowerText) {
|
||
return fragment.account_hints.length > 0 || ACCOUNT_HINT_PATTERN.test(lowerText) || PERIOD_PATTERN.test(lowerText);
|
||
}
|
||
function resolveRouteClass(fragment) {
|
||
const lowerText = mergedFragmentText(fragment);
|
||
const symptomSignal = hasSymptomSignal(fragment, lowerText);
|
||
const lifecycleSignal = hasLifecycleSignal(fragment, lowerText);
|
||
const chainBreakSignal = hasChainBreakSignal(lowerText);
|
||
const periodImpactSignal = hasPeriodImpactSignal(lowerText);
|
||
const causalSignal = hasCausalSignal(lowerText);
|
||
const ambiguitySignal = hasAmbiguitySignal(fragment, lowerText);
|
||
const accountOrPeriodAnchor = hasAccountOrPeriodAnchor(fragment, lowerText);
|
||
if (fragment.flags.asks_for_exact_object_trace) {
|
||
return ROUTE_DISCIPLINE_RULE_MAP.get("exact_object_trace");
|
||
}
|
||
if (fragment.flags.asks_for_ranking_or_top || fragment.flags.asks_for_period_summary) {
|
||
return ROUTE_DISCIPLINE_RULE_MAP.get("ranking_or_period_summary");
|
||
}
|
||
if (ambiguitySignal && (symptomSignal || lifecycleSignal || chainBreakSignal || periodImpactSignal || causalSignal)) {
|
||
return ROUTE_DISCIPLINE_RULE_MAP.get("mixed_ambiguity");
|
||
}
|
||
if (chainBreakSignal) {
|
||
return ROUTE_DISCIPLINE_RULE_MAP.get("chain_break");
|
||
}
|
||
if (periodImpactSignal && accountOrPeriodAnchor) {
|
||
return ROUTE_DISCIPLINE_RULE_MAP.get("period_impact");
|
||
}
|
||
if (lifecycleSignal) {
|
||
return ROUTE_DISCIPLINE_RULE_MAP.get("lifecycle_first");
|
||
}
|
||
if (symptomSignal) {
|
||
return ROUTE_DISCIPLINE_RULE_MAP.get("symptom_first");
|
||
}
|
||
if (causalSignal && accountOrPeriodAnchor) {
|
||
return ROUTE_DISCIPLINE_RULE_MAP.get("causal_query");
|
||
}
|
||
if (fragment.flags.asks_for_rule_check) {
|
||
return ROUTE_DISCIPLINE_RULE_MAP.get("rule_check_without_symptom");
|
||
}
|
||
return ROUTE_DISCIPLINE_RULE_MAP.get("canonical_fact_lookup");
|
||
}
|
||
function shouldPromoteFromNoRoute(fragment, rule) {
|
||
if (rule.required_route === "store_canonical") {
|
||
return false;
|
||
}
|
||
if (explicitNoRouteReason(fragment) === "out_of_scope") {
|
||
return false;
|
||
}
|
||
const lowerText = mergedFragmentText(fragment);
|
||
const hasProblemSignal = hasSymptomSignal(fragment, lowerText) ||
|
||
hasLifecycleSignal(fragment, lowerText) ||
|
||
hasChainBreakSignal(lowerText) ||
|
||
hasPeriodImpactSignal(lowerText) ||
|
||
hasCausalSignal(lowerText);
|
||
const hasAnchor = hasAccountOrPeriodAnchor(fragment, lowerText) ||
|
||
fragment.candidate_labels.includes("cross_entity") ||
|
||
DOMAIN_LEXICAL_ANCHOR_PATTERN.test(lowerText);
|
||
return hasProblemSignal && hasAnchor;
|
||
}
|
||
function reasonForNoRoute(noRouteReason) {
|
||
if (noRouteReason === "out_of_scope") {
|
||
return "Fragment is out-of-scope for company-specific accounting contour.";
|
||
}
|
||
if (noRouteReason === "missing_mapping") {
|
||
return "Fragment is in-scope but route mapping is currently missing.";
|
||
}
|
||
if (noRouteReason === "unsupported_fragment_type") {
|
||
return "Fragment type is not supported by the current deterministic route map.";
|
||
}
|
||
return "Fragment requires clarification or is too underspecified for safe routing.";
|
||
}
|
||
function explicitRouteStatus(fragment) {
|
||
return "route_status" in fragment ? fragment.route_status : null;
|
||
}
|
||
function explicitNoRouteReason(fragment) {
|
||
return "no_route_reason" in fragment ? fragment.no_route_reason : null;
|
||
}
|
||
function executionReadiness(fragment) {
|
||
return "execution_readiness" in fragment ? fragment.execution_readiness : null;
|
||
}
|
||
function clarificationReason(fragment) {
|
||
return "clarification_reason" in fragment ? fragment.clarification_reason : null;
|
||
}
|
||
function softAssumptions(fragment) {
|
||
return "soft_assumption_used" in fragment ? fragment.soft_assumption_used : [];
|
||
}
|
||
function buildNoRouteDecision(fragment, noRouteReason) {
|
||
return {
|
||
fragment_id: fragment.fragment_id,
|
||
domain_relevance: fragment.domain_relevance,
|
||
business_scope: fragment.business_scope,
|
||
candidate_labels: fragment.candidate_labels,
|
||
decision_flags: fragment.flags,
|
||
execution_readiness: executionReadiness(fragment),
|
||
clarification_reason: clarificationReason(fragment),
|
||
soft_assumption_used: softAssumptions(fragment),
|
||
route_status: "no_route",
|
||
no_route_reason: noRouteReason ?? "insufficient_specificity",
|
||
route: "no_route",
|
||
reason: reasonForNoRoute(noRouteReason)
|
||
};
|
||
}
|
||
function decideRouteForFragment(fragment) {
|
||
const status = explicitRouteStatus(fragment);
|
||
const noRouteReason = explicitNoRouteReason(fragment);
|
||
const readiness = executionReadiness(fragment);
|
||
const clarification = clarificationReason(fragment);
|
||
const soft = softAssumptions(fragment);
|
||
const routeRule = resolveRouteClass(fragment);
|
||
if (fragment.domain_relevance === "out_of_scope") {
|
||
return buildNoRouteDecision(fragment, "out_of_scope");
|
||
}
|
||
if (status === "no_route" || readiness === "needs_clarification" || readiness === "no_route") {
|
||
if (shouldPromoteFromNoRoute(fragment, routeRule)) {
|
||
return {
|
||
fragment_id: fragment.fragment_id,
|
||
domain_relevance: fragment.domain_relevance,
|
||
business_scope: fragment.business_scope,
|
||
candidate_labels: fragment.candidate_labels,
|
||
decision_flags: fragment.flags,
|
||
execution_readiness: readiness,
|
||
clarification_reason: clarification,
|
||
soft_assumption_used: soft,
|
||
route_status: "routed",
|
||
no_route_reason: null,
|
||
route: routeRule.required_route,
|
||
reason: `${routeRule.description} Query class: ${routeRule.query_class}. Promoted from no-route by anchor/symptom guardrail.`
|
||
};
|
||
}
|
||
return buildNoRouteDecision(fragment, noRouteReason);
|
||
}
|
||
if (status === "routed" || status === null) {
|
||
return {
|
||
fragment_id: fragment.fragment_id,
|
||
domain_relevance: fragment.domain_relevance,
|
||
business_scope: fragment.business_scope,
|
||
candidate_labels: fragment.candidate_labels,
|
||
decision_flags: fragment.flags,
|
||
execution_readiness: readiness,
|
||
clarification_reason: clarification,
|
||
soft_assumption_used: soft,
|
||
route_status: "routed",
|
||
no_route_reason: null,
|
||
route: routeRule.required_route,
|
||
reason: `${routeRule.description} Query class: ${routeRule.query_class}. Allowed fallback: ${routeRule.allowed_fallback.join(", ")}. Forbidden fallback: ${routeRule.forbidden_fallback.join(", ")}.`
|
||
};
|
||
}
|
||
return buildNoRouteDecision(fragment, "missing_mapping");
|
||
}
|
||
function fallbackMessageFor(type) {
|
||
if (type === "out_of_scope") {
|
||
return "Я работаю только с данными и бухгалтерским контуром текущей компании. Запрос вне доступной предметной области.";
|
||
}
|
||
if (type === "clarification") {
|
||
return "Могу проверить это в контуре компании, но нужно уточнить период, документ, счет или участок учета.";
|
||
}
|
||
if (type === "partial") {
|
||
return "Обработаю только часть запроса, которая относится к данным компании. Остальное выходит за пределы доступного контура.";
|
||
}
|
||
return null;
|
||
}
|
||
function simulateDeterministicRouting(normalized) {
|
||
const decisions = normalized.fragments.map((fragment) => decideRouteForFragment(fragment));
|
||
const inScopeCount = decisions.filter((item) => item.domain_relevance === "in_scope").length;
|
||
const outOfScopeCount = decisions.filter((item) => item.domain_relevance === "out_of_scope").length;
|
||
const unclearCount = decisions.filter((item) => item.domain_relevance === "unclear").length;
|
||
const routedInScopeCount = decisions.filter((item) => item.domain_relevance === "in_scope" && item.route !== "no_route").length;
|
||
const clarificationInScopeCount = decisions.filter((item) => item.domain_relevance === "in_scope" && item.execution_readiness === "needs_clarification").length;
|
||
const noRouteInScopeCount = decisions.filter((item) => item.domain_relevance === "in_scope" && item.route === "no_route").length;
|
||
let fallbackType = "none";
|
||
if (!normalized.message_in_scope || inScopeCount === 0) {
|
||
fallbackType = outOfScopeCount > 0 && unclearCount === 0 ? "out_of_scope" : "clarification";
|
||
}
|
||
else if (routedInScopeCount === 0 && clarificationInScopeCount > 0) {
|
||
fallbackType = "clarification";
|
||
}
|
||
else if (routedInScopeCount === 0 && noRouteInScopeCount > 0) {
|
||
fallbackType = "clarification";
|
||
}
|
||
else if ((inScopeCount > 0 && outOfScopeCount > 0) || (routedInScopeCount > 0 && noRouteInScopeCount > 0)) {
|
||
fallbackType = "partial";
|
||
}
|
||
return {
|
||
mode: "deterministic_v2",
|
||
message_in_scope: normalized.message_in_scope,
|
||
scope_confidence: normalized.scope_confidence,
|
||
planner: {
|
||
total_fragments: normalized.fragments.length,
|
||
in_scope_fragments: inScopeCount,
|
||
out_of_scope_fragments: outOfScopeCount,
|
||
discarded_fragments: normalized.discarded_fragments.length,
|
||
contains_multiple_tasks: normalized.contains_multiple_tasks
|
||
},
|
||
decisions,
|
||
fallback: {
|
||
type: fallbackType,
|
||
message: fallbackMessageFor(fallbackType)
|
||
}
|
||
};
|
||
}
|
||
function toRouteHintSummary(normalized) {
|
||
if (normalized.schema_version === "normalized_query_v2" ||
|
||
normalized.schema_version === "normalized_query_v2_0_1" ||
|
||
normalized.schema_version === "normalized_query_v2_0_2") {
|
||
return simulateDeterministicRouting(normalized);
|
||
}
|
||
return toRouteHintSummaryV1(normalized);
|
||
}
|
||
function toRouterInput(normalized) {
|
||
if (normalized.schema_version === "normalized_query_v2" ||
|
||
normalized.schema_version === "normalized_query_v2_0_1" ||
|
||
normalized.schema_version === "normalized_query_v2_0_2") {
|
||
return {
|
||
mode: "deterministic_v2",
|
||
message_in_scope: normalized.message_in_scope,
|
||
scope_confidence: normalized.scope_confidence,
|
||
contains_multiple_tasks: normalized.contains_multiple_tasks,
|
||
fragments: normalized.fragments.map((fragment) => ({
|
||
fragment_id: fragment.fragment_id,
|
||
domain_relevance: fragment.domain_relevance,
|
||
business_scope: fragment.business_scope,
|
||
execution_readiness: "execution_readiness" in fragment ? fragment.execution_readiness : null,
|
||
clarification_reason: "clarification_reason" in fragment ? fragment.clarification_reason : null,
|
||
soft_assumption_used: "soft_assumption_used" in fragment ? fragment.soft_assumption_used : [],
|
||
route_status: "route_status" in fragment ? fragment.route_status : null,
|
||
no_route_reason: "no_route_reason" in fragment ? fragment.no_route_reason : null,
|
||
flags: fragment.flags,
|
||
candidate_labels: fragment.candidate_labels,
|
||
confidence: fragment.confidence
|
||
}))
|
||
};
|
||
}
|
||
return {
|
||
mode: "legacy_v1",
|
||
intent_class: normalized.intent_class,
|
||
decision_flags: {
|
||
needs_cross_entity_join: normalized.requires.needs_cross_entity_join,
|
||
needs_causal_chain: normalized.requires.needs_causal_chain,
|
||
needs_exact_object_trace: normalized.requires.needs_exact_object_trace,
|
||
needs_ranking: normalized.requires.needs_ranking,
|
||
needs_anomaly_summary: normalized.requires.needs_anomaly_summary,
|
||
needs_runtime_truth: normalized.requires.needs_runtime_truth
|
||
},
|
||
route_hint: normalized.route_hint,
|
||
confidence: normalized.confidence.overall,
|
||
entities: {
|
||
domain_entities: normalized.domain_entities,
|
||
accounts_mentioned: normalized.accounts_mentioned,
|
||
documents_mentioned: normalized.documents_mentioned,
|
||
registers_mentioned: normalized.registers_mentioned
|
||
},
|
||
period_scope: normalized.period_scope
|
||
};
|
||
}
|