114 lines
3.8 KiB
Python
114 lines
3.8 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import asdict, dataclass
|
|
from typing import Any
|
|
|
|
from router.query_classifier import RouteDecisionFlags
|
|
from router.store_sufficiency import StoreSufficiencyResult
|
|
|
|
|
|
ALL_ROUTES = [
|
|
"live_mcp_drilldown",
|
|
"store_canonical",
|
|
"store_feature_risk",
|
|
"hybrid_store_plus_live",
|
|
"batch_refresh_then_store",
|
|
]
|
|
|
|
|
|
@dataclass
|
|
class RouteSelectionResult:
|
|
chosen_route: str
|
|
candidate_routes: list[str]
|
|
rejected_routes: dict[str, str]
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return asdict(self)
|
|
|
|
|
|
def choose_route(
|
|
flags: RouteDecisionFlags,
|
|
suff: StoreSufficiencyResult,
|
|
*,
|
|
parsed_as_trend_or_risk: bool,
|
|
) -> RouteSelectionResult:
|
|
rejected: dict[str, str] = {}
|
|
|
|
if flags.needs_exact_object_trace:
|
|
rejected["store_canonical"] = "exact_trace_requires_live"
|
|
rejected["store_feature_risk"] = "exact_trace_requires_live"
|
|
rejected["hybrid_store_plus_live"] = "exact_trace_prefers_direct_live"
|
|
rejected["batch_refresh_then_store"] = "exact_trace_not_batch"
|
|
return RouteSelectionResult(
|
|
chosen_route="live_mcp_drilldown",
|
|
candidate_routes=list(ALL_ROUTES),
|
|
rejected_routes=rejected,
|
|
)
|
|
|
|
heavy_shape = (
|
|
flags.needs_full_period_aggregation
|
|
or flags.needs_ranking
|
|
or (flags.needs_anomaly_summary and not parsed_as_trend_or_risk)
|
|
)
|
|
if heavy_shape:
|
|
aggregate_ok = (
|
|
flags.precomputed_aggregate_available
|
|
and suff.freshness_ok
|
|
and suff.aggregate_level_ok
|
|
and (not flags.needs_ranking or suff.ranking_ready)
|
|
)
|
|
if not aggregate_ok:
|
|
rejected["store_feature_risk"] = "aggregate_not_sufficient"
|
|
rejected["store_canonical"] = "wrong_query_shape"
|
|
rejected["live_mcp_drilldown"] = "heavy_query_not_live"
|
|
return RouteSelectionResult(
|
|
chosen_route="batch_refresh_then_store",
|
|
candidate_routes=list(ALL_ROUTES),
|
|
rejected_routes=rejected,
|
|
)
|
|
|
|
if flags.needs_cross_entity_join and flags.needs_causal_chain:
|
|
if not suff.explanation_ready:
|
|
rejected["store_canonical"] = "cross_entity_causal_needs_live_stitching"
|
|
return RouteSelectionResult(
|
|
chosen_route="hybrid_store_plus_live",
|
|
candidate_routes=list(ALL_ROUTES),
|
|
rejected_routes=rejected,
|
|
)
|
|
|
|
if parsed_as_trend_or_risk:
|
|
if suff.feature_sufficient and not flags.needs_runtime_truth:
|
|
rejected["store_canonical"] = "trend_risk_query_prefers_feature"
|
|
return RouteSelectionResult(
|
|
chosen_route="store_feature_risk",
|
|
candidate_routes=list(ALL_ROUTES),
|
|
rejected_routes=rejected,
|
|
)
|
|
rejected["store_feature_risk"] = "feature_or_freshness_not_sufficient"
|
|
|
|
if (
|
|
suff.canonical_sufficient
|
|
and not flags.needs_causal_chain
|
|
and not flags.ambiguous_object_scope
|
|
and not flags.needs_runtime_truth
|
|
):
|
|
if flags.needs_cross_entity_join and not suff.explanation_ready:
|
|
rejected["store_canonical"] = "cross_entity_needs_explanation_stitching"
|
|
else:
|
|
return RouteSelectionResult(
|
|
chosen_route="store_canonical",
|
|
candidate_routes=list(ALL_ROUTES),
|
|
rejected_routes=rejected,
|
|
)
|
|
|
|
if not suff.canonical_sufficient:
|
|
rejected["store_canonical"] = "canonical_not_sufficient"
|
|
if flags.ambiguous_object_scope:
|
|
rejected["store_canonical"] = "ambiguous_scope_requires_hybrid_or_feature"
|
|
|
|
return RouteSelectionResult(
|
|
chosen_route="hybrid_store_plus_live",
|
|
candidate_routes=list(ALL_ROUTES),
|
|
rejected_routes=rejected,
|
|
)
|